fdb2 1.0.37 → 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 (106) hide show
  1. package/README.md +41 -0
  2. package/dist/public/explorer.css +517 -478
  3. package/dist/public/explorer.js +1114 -872
  4. package/dist/public/index.css +87 -27
  5. package/dist/public/index.js +97 -49
  6. package/dist/public/layout.js +2 -2
  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 -101
  74. package/dist/server/service/database/mssql.service.js.map +1 -1
  75. package/dist/server/service/database/mssql.service.ts +28 -122
  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 -137
  79. package/dist/server/service/database/mysql.service.js.map +1 -1
  80. package/dist/server/service/database/mysql.service.ts +11 -173
  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 -104
  84. package/dist/server/service/database/oracle.service.js.map +1 -1
  85. package/dist/server/service/database/oracle.service.ts +11 -127
  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 -112
  89. package/dist/server/service/database/postgres.service.js.map +1 -1
  90. package/dist/server/service/database/postgres.service.ts +11 -133
  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.map +1 -1
  97. package/dist/server/service/database/sql-type.helper.js +9 -47
  98. package/dist/server/service/database/sql-type.helper.js.map +1 -1
  99. package/dist/server/service/database/sql-type.helper.ts +11 -53
  100. package/dist/server/service/database/sqlite.service.d.ts +0 -5
  101. package/dist/server/service/database/sqlite.service.d.ts.map +1 -1
  102. package/dist/server/service/database/sqlite.service.js +11 -147
  103. package/dist/server/service/database/sqlite.service.js.map +1 -1
  104. package/dist/server/service/database/sqlite.service.ts +11 -177
  105. package/package.json +1 -1
  106. package/server.js +1 -1
@@ -0,0 +1,925 @@
1
+ /**
2
+ * 方言 DDL 生成器:ChangeSet → 各数据库的 DDL 语句序列。
3
+ *
4
+ * 与历史上"前端拼 SQL / 各方言 service 各自拼 ALTER"的关键差异:
5
+ * 1. 单一出口——预览与执行共用 emitAlterDdl,天然"所见即所得",
6
+ * 杜绝旧实现里"前端预览 A、后端执行 B"的分叉;
7
+ * 2. 渲染输入是 CanonicalColumn(defaultKind 三态 + effectiveAutoIncrement clamp),
8
+ * MySQL MODIFY 重建整列定义时不会把 DEFAULT '' / DEFAULT 0 写丢;
9
+ * 3. MySQL 主键/自增的鸡蛋问题有确定性编排:
10
+ * 去自增的 MODIFY → DROP PRIMARY KEY → ADD PRIMARY KEY → 其余列语句
11
+ * (AUTO_INCREMENT 列必须被索引;先 ADD PK 再 MODIFY 加自增);
12
+ * 4. SQLite rebuild 是纯函数:固定临时表名 + 先 DROP IF EXISTS,
13
+ * 同一输入永远得到同一语句序列(可测试、可幂等重试)。
14
+ *
15
+ * 纯函数不变式:emitAlterDdl(kind, c, c) === []。
16
+ */
17
+ import type {
18
+ CanonicalColumn,
19
+ CanonicalTable,
20
+ DdlCapability,
21
+ TableChangeSet
22
+ } from '../../model/ddl-spec';
23
+ import type { DialectKind } from '../dialect';
24
+ import { diffTable } from './diff';
25
+ import { typeParamDefault } from './type-defaults';
26
+ import { isEmptyChangeSet } from '../../model/ddl-spec';
27
+
28
+ // ---------------------------------------------------------------- 引用与转义
29
+
30
+ function quote(kind: DialectKind, name: string): string {
31
+ const n = String(name);
32
+ switch (kind) {
33
+ case 'mysql':
34
+ return '`' + n.replace(/`/g, '``') + '`';
35
+ case 'clickhouse':
36
+ // 与 CAPS 声明的 quoteStyle: 'backtick' 一致(ClickHouse 默认反引号)
37
+ return '`' + n.replace(/`/g, '``') + '`';
38
+ case 'mssql':
39
+ return '[' + n.replace(/]/g, ']]') + ']';
40
+ case 'oracle':
41
+ // Oracle 未加引号的标识符默认大写;沿用前端既有行为统一大写
42
+ return '"' + n.toUpperCase().replace(/"/g, '""') + '"';
43
+ default:
44
+ return '"' + n.replace(/"/g, '""') + '"';
45
+ }
46
+ }
47
+
48
+ function esc(s: string): string {
49
+ return String(s).replace(/'/g, "''");
50
+ }
51
+
52
+ // ---------------------------------------------------------------- 类型与默认值渲染
53
+
54
+
55
+
56
+ /**
57
+ * CanonicalColumn → 类型串(canonical 化的逆投影;自定义类型全串原样)
58
+ *
59
+ * kind 必填:仅填了基础类型名(VARCHAR)而未填长度时,MySQL 等方言下必须补默认参数,
60
+ * 否则会生成 `VARCHAR` 这类非法类型(ER_PARSE_ERROR)。默认参数见 type-defaults。
61
+ */
62
+ export function renderType(col: CanonicalColumn, kind: DialectKind): string {
63
+ // 含 '(' 或引号 → canonicalizeType 判定为自定义全串(ENUM('a','b') 等),原样输出
64
+ if (/[(']/.test(col.baseType)) return col.baseType;
65
+
66
+ let length = col.length;
67
+ let precision = col.precision;
68
+ let scale = col.scale;
69
+
70
+ // 三项都没指定时才套默认;已填任意一项即尊重用户显式输入
71
+ if (length === undefined && precision === undefined && scale === undefined) {
72
+ const d = typeParamDefault(kind, col.baseType);
73
+ if (d) {
74
+ if (d.length !== undefined) length = d.length;
75
+ if (d.precision !== undefined) precision = d.precision;
76
+ if (d.scale !== undefined) scale = d.scale;
77
+ }
78
+ }
79
+
80
+ if (precision !== undefined && scale !== undefined) {
81
+ return `${col.baseType}(${precision},${scale})`;
82
+ }
83
+ if (precision !== undefined) return `${col.baseType}(${precision})`;
84
+ if (length !== undefined) return `${col.baseType}(${length})`;
85
+ return col.baseType;
86
+ }
87
+
88
+ /** literal 是否按数字/布尔裸输出(不加引号) */
89
+ function isBareLiteral(text: string): boolean {
90
+ if (/^-?\d+(\.\d+)?$/.test(text)) return true;
91
+ const up = text.toUpperCase();
92
+ return up === 'TRUE' || up === 'FALSE';
93
+ }
94
+
95
+ /** 默认值的 DEFAULT 子句片段(不带 DEFAULT 关键字);null 表示无默认值 */
96
+ export function renderDefault(col: CanonicalColumn): string | null {
97
+ switch (col.defaultKind) {
98
+ case 'none':
99
+ return null;
100
+ case 'null':
101
+ return 'NULL';
102
+ case 'expression':
103
+ return col.defaultText;
104
+ case 'literal':
105
+ return isBareLiteral(col.defaultText) ? col.defaultText : `'${esc(col.defaultText)}'`;
106
+ default:
107
+ return null;
108
+ }
109
+ }
110
+
111
+ // ---------------------------------------------------------------- 能力声明
112
+
113
+ const CAPS: Record<DialectKind, DdlCapability> = {
114
+ mysql: {
115
+ createTable: true, alterTable: true, dropTable: true, renameTable: true,
116
+ alterColumn: true, renameColumn: true, alterPrimaryKey: true,
117
+ tableComment: true, columnComment: true,
118
+ alterStrategy: 'native', quoteStyle: 'backtick'
119
+ },
120
+ postgres: {
121
+ createTable: true, alterTable: true, dropTable: true, renameTable: true,
122
+ alterColumn: true, renameColumn: true, alterPrimaryKey: true,
123
+ tableComment: true, columnComment: true,
124
+ alterStrategy: 'native', quoteStyle: 'double'
125
+ },
126
+ cockroachdb: {
127
+ createTable: true, alterTable: true, dropTable: true, renameTable: true,
128
+ alterColumn: true, renameColumn: true, alterPrimaryKey: false,
129
+ tableComment: true, columnComment: true,
130
+ alterStrategy: 'native', quoteStyle: 'double'
131
+ },
132
+ sqlite: {
133
+ createTable: true, alterTable: true, dropTable: true, renameTable: true,
134
+ alterColumn: false, renameColumn: true,
135
+ // SQLite 原生 ALTER 不支持改主键,但 alterStrategy='rebuild' 会整表重建,
136
+ // 主键变更可以正确落地并保留数据 → 按真实能力声明为支持(旧值 false 会误禁 UI)
137
+ alterPrimaryKey: true,
138
+ tableComment: false, columnComment: false,
139
+ alterStrategy: 'rebuild', quoteStyle: 'double'
140
+ },
141
+ oracle: {
142
+ createTable: true, alterTable: true, dropTable: true, renameTable: true,
143
+ alterColumn: true, renameColumn: true, alterPrimaryKey: true,
144
+ tableComment: true, columnComment: true,
145
+ alterStrategy: 'native', quoteStyle: 'double'
146
+ },
147
+ mssql: {
148
+ createTable: true, alterTable: true, dropTable: true, renameTable: true,
149
+ // 主键约束名由 SQL Server 自动生成(PK__<table>__<hash>),变更集里推不出来,
150
+ // 但可以在生成语句前回查 sys.key_constraints 拿到真实名(见 getPrimaryKeyConstraintName)
151
+ // → 声明为支持;确实定位不到时由 buildAlterPlan 报明确错误,而不是静默无语句
152
+ alterColumn: true, renameColumn: true, alterPrimaryKey: true,
153
+ tableComment: true, columnComment: true,
154
+ alterStrategy: 'native', quoteStyle: 'bracket'
155
+ },
156
+ mongodb: {
157
+ createTable: false, alterTable: false, dropTable: true, renameTable: false,
158
+ alterColumn: false, renameColumn: false, alterPrimaryKey: false,
159
+ tableComment: false, columnComment: false,
160
+ alterStrategy: 'unsupported', quoteStyle: 'double'
161
+ },
162
+ sap: {
163
+ createTable: true, alterTable: false, dropTable: true, renameTable: true,
164
+ alterColumn: false, renameColumn: false, alterPrimaryKey: false,
165
+ tableComment: false, columnComment: false,
166
+ alterStrategy: 'unsupported', quoteStyle: 'double'
167
+ },
168
+ clickhouse: {
169
+ createTable: true, alterTable: true, dropTable: true, renameTable: true,
170
+ alterColumn: true, renameColumn: false, alterPrimaryKey: false,
171
+ tableComment: true, columnComment: true,
172
+ alterStrategy: 'native', quoteStyle: 'backtick'
173
+ }
174
+ };
175
+
176
+ export function ddlCapabilities(kind: DialectKind): DdlCapability {
177
+ return { ...(CAPS[kind] ?? CAPS.mysql) };
178
+ }
179
+
180
+ /**
181
+ * 生成 ALTER 所需的、无法从变更集推断的运行时信息。
182
+ * mssqlPkConstraintName:SQL Server 主键约束名是自动生成的(PK__<table>__<hash>),
183
+ * 必须回查系统表获得;缺失时 DROP CONSTRAINT 必然失败,由调用方先判为错误。
184
+ */
185
+ export interface AlterDdlOptions {
186
+ mssqlPkConstraintName?: string;
187
+ }
188
+
189
+ // ---------------------------------------------------------------- ALTER 主入口
190
+
191
+ /**
192
+ * 结构变更 DDL:内部完成 diff 并按方言渲染。
193
+ * 语句顺序有语义(MySQL PK/AI 编排、SQLite rebuild),调用方必须按序执行。
194
+ */
195
+ export function emitAlterDdl(
196
+ kind: DialectKind,
197
+ original: CanonicalTable,
198
+ desired: CanonicalTable,
199
+ opts?: AlterDdlOptions
200
+ ): string[] {
201
+ if (CAPS[kind]?.alterStrategy === 'unsupported') return [];
202
+ const cs = diffTable(original, desired);
203
+ if (isEmptyChangeSet(cs)) return []; // SQLite rebuild 等策略下"未修改"绝不能触发重建
204
+ switch (kind) {
205
+ case 'mysql':
206
+ return emitMysqlAlter(cs, desired);
207
+ case 'postgres':
208
+ case 'cockroachdb':
209
+ return emitPgAlter(kind, cs, desired);
210
+ case 'sqlite':
211
+ // 对齐旧实现:仅新增列时走 ADD COLUMN 快速路径,避免整表重建拷贝数据
212
+ if (cs.added.length > 0 && cs.renamed.length === 0 && cs.modified.length === 0 &&
213
+ cs.dropped.length === 0 && !cs.primaryKey && !cs.rename && !cs.comment && !cs.orderChanged) {
214
+ return cs.added.map((col) => {
215
+ let def = `${quote('sqlite', col.name)} ${renderType(col, 'sqlite')}`;
216
+ if (!col.nullable) def += ' NOT NULL';
217
+ const dv = renderDefault(col);
218
+ if (dv !== null) def += ` DEFAULT ${dv}`;
219
+ return `ALTER TABLE ${quote('sqlite', desired.name)} ADD COLUMN ${def}`;
220
+ });
221
+ }
222
+ return emitSqliteRebuild(original, desired, cs);
223
+ case 'oracle':
224
+ return emitOracleAlter(cs, desired);
225
+ case 'mssql':
226
+ return emitMssqlAlter(cs, desired, opts?.mssqlPkConstraintName);
227
+ case 'clickhouse':
228
+ return emitClickHouseAlter(cs, desired);
229
+ default:
230
+ return [];
231
+ }
232
+ }
233
+
234
+ /**
235
+ * 表名等价判断:按方言的大小写语义比较。
236
+ * PG/CockroachDB 会把裸标识符落成小写、Oracle/SAP 落成大写、SQLite/MSSQL 一般不敏感,
237
+ * 只有 ClickHouse/MongoDB 的标识符严格区分大小写——统一用 === 会漏判(PG 下 `Users` 与 `users` 冲突)。
238
+ */
239
+ export function tableNameEquals(kind: DialectKind, a: string, b: string): boolean {
240
+ if (a === b) return true;
241
+ if (kind === 'clickhouse' || kind === 'mongodb') return false;
242
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
243
+ }
244
+
245
+ // ---------------------------------------------------------------- CREATE TABLE
246
+
247
+ /** emitCreateDdl 的可选参数 */
248
+ export interface EmitCreateOptions {
249
+ /** 输出 CREATE TABLE IF NOT EXISTS(导出/同步场景) */
250
+ ifNotExists?: boolean;
251
+ /** mssql 三段名前缀(数据库名)——对齐方言 createTable 的 [db].[dbo].[table] 行为 */
252
+ mssqlDatabase?: string;
253
+ /** clickhouse 表引擎(默认 MergeTree) */
254
+ engine?: string;
255
+ /** 表名覆盖(SQLite rebuild 复用同一渲染,固定临时表名) */
256
+ tableNameOverride?: string;
257
+ }
258
+
259
+ /**
260
+ * 建表 DDL:所有运行时建表(前端创建表 / syncTable 方言 createTable / SQLite rebuild 临时表)
261
+ * 的单一出口。输入 CanonicalTable(经 canonicalizeTableSpec / canonicalizeTableEntity 投影),
262
+ * 方言差异集中在此——顺带修复旧方言 createTable 的共知缺陷:
263
+ * - PG/CRDB 曾输出 `"col" int SERIAL` 与内联 `COMMENT`(均为语法错误);
264
+ * - SQLite 曾丢失主键、AUTOINCREMENT 脱离 PRIMARY KEY 单独输出;
265
+ * - 默认值用 `if (defaultValue)` 假值兜底(DEFAULT 0 丢失)且一律加引号(`DEFAULT '5'`)。
266
+ */
267
+ export function emitCreateDdl(
268
+ kind: DialectKind,
269
+ table: CanonicalTable,
270
+ opts: EmitCreateOptions = {}
271
+ ): string[] {
272
+ const name = opts.tableNameOverride ?? table.name;
273
+ const ine = opts.ifNotExists ? ' IF NOT EXISTS' : '';
274
+ switch (kind) {
275
+ case 'mysql':
276
+ return [emitMysqlCreate(table, name, ine)];
277
+ case 'postgres':
278
+ case 'cockroachdb':
279
+ return emitPgCreate(kind, table, name, ine);
280
+ case 'sqlite':
281
+ return [emitSqliteCreate(name, ine, table)];
282
+ case 'oracle':
283
+ return emitOracleCreate(table, name, ine);
284
+ case 'mssql':
285
+ return emitMssqlCreate(table, name, ine, opts.mssqlDatabase);
286
+ case 'clickhouse':
287
+ return [emitClickhouseCreate(table, name, ine, opts.engine ?? 'MergeTree')];
288
+ case 'sap':
289
+ return [emitSapCreate(table, name)];
290
+ default:
291
+ // mongodb 等非 SQL 方言由调用方按 caps.createTable 拒绝
292
+ return [];
293
+ }
294
+ }
295
+
296
+ function emitMysqlCreate(table: CanonicalTable, name: string, ine: string): string {
297
+ const q = (n: string) => quote('mysql', n);
298
+ const defs = table.columns.map((col) => mysqlColumnDef('mysql', col));
299
+ if (table.primaryKey.length > 0) {
300
+ defs.push(`PRIMARY KEY (${table.primaryKey.map(q).join(', ')})`);
301
+ }
302
+ // 表选项对齐旧前端 create 行为:InnoDB + utf8mb4(方言旧版缺失引擎项)
303
+ let sql = `CREATE TABLE${ine} ${q(name)} (\n ${defs.join(',\n ')}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;
304
+ if (table.comment) sql += ` COMMENT='${esc(table.comment)}'`;
305
+ return sql;
306
+ }
307
+
308
+ /** PG/CRDB 列定义(CREATE 与 ALTER ADD COLUMN 共用) */
309
+ function pgColumnDef(kind: DialectKind, col: CanonicalColumn): string {
310
+ const q = (n: string) => quote(kind, n);
311
+ let def = `${q(col.name)} ${renderType(col, kind)}`;
312
+ if (!col.nullable) def += ' NOT NULL';
313
+ const dv = renderDefault(col);
314
+ if (dv !== null) def += ` DEFAULT ${dv}`;
315
+ if (col.effectiveAutoIncrement && !/SERIAL/i.test(renderType(col, kind))) {
316
+ def += ' GENERATED ALWAYS AS IDENTITY';
317
+ }
318
+ return def;
319
+ }
320
+
321
+ function emitPgCreate(kind: DialectKind, table: CanonicalTable, name: string, ine: string): string[] {
322
+ const q = (n: string) => quote(kind, n);
323
+ const defs = table.columns.map((col) => pgColumnDef(kind, col));
324
+ if (table.primaryKey.length > 0) {
325
+ defs.push(`PRIMARY KEY (${table.primaryKey.map(q).join(', ')})`);
326
+ }
327
+ const out = [`CREATE TABLE${ine} ${q(name)} (\n ${defs.join(',\n ')}\n)`];
328
+ if (table.comment) out.push(`COMMENT ON TABLE ${q(name)} IS '${esc(table.comment)}'`);
329
+ for (const col of table.columns) {
330
+ if (col.comment) out.push(`COMMENT ON COLUMN ${q(name)}.${q(col.name)} IS '${esc(col.comment)}'`);
331
+ }
332
+ return out;
333
+ }
334
+
335
+ function emitSqliteCreate(name: string, ine: string, table: CanonicalTable): string {
336
+ const q = (n: string) => quote('sqlite', n);
337
+ // 单列主键内联(INTEGER PRIMARY KEY AUTOINCREMENT 隐式 rowid 主键);多列用表级约束
338
+ const singlePk = table.primaryKey.length === 1 ? table.primaryKey[0]! : null;
339
+ const defs = table.columns.map((col) => {
340
+ // SQLite 的 AUTOINCREMENT 要求列类型字面为 INTEGER(INT/BIGINT 均不合法)
341
+ const isRowidPk = singlePk === col.name && col.effectiveAutoIncrement;
342
+ const type = isRowidPk ? 'INTEGER' : renderType(col, 'sqlite');
343
+ let def = `${q(col.name)} ${type}`;
344
+ if (!col.nullable) def += ' NOT NULL';
345
+ const dv = renderDefault(col);
346
+ if (dv !== null) def += ` DEFAULT ${dv}`;
347
+ if (singlePk === col.name) {
348
+ def += col.effectiveAutoIncrement ? ' PRIMARY KEY AUTOINCREMENT' : ' PRIMARY KEY';
349
+ }
350
+ return def;
351
+ });
352
+ if (!singlePk && table.primaryKey.length > 0) {
353
+ defs.push(`PRIMARY KEY (${table.primaryKey.map(q).join(', ')})`);
354
+ }
355
+ return `CREATE TABLE${ine} ${q(name)} (\n ${defs.join(',\n ')}\n)`;
356
+ }
357
+
358
+ function emitOracleCreate(table: CanonicalTable, name: string, ine: string): string[] {
359
+ const q = (n: string) => quote('oracle', n);
360
+ const defs = table.columns.map((col) => oracleColumnDef(col));
361
+ if (table.primaryKey.length > 0) {
362
+ defs.push(`PRIMARY KEY (${table.primaryKey.map(q).join(', ')})`);
363
+ }
364
+ const out = [`CREATE TABLE${ine} ${q(name)} (\n ${defs.join(',\n ')}\n)`];
365
+ if (table.comment) out.push(`COMMENT ON TABLE ${q(name)} IS '${esc(table.comment)}'`);
366
+ for (const col of table.columns) {
367
+ if (col.comment) out.push(`COMMENT ON COLUMN ${q(name)}.${q(col.name)} IS '${esc(col.comment)}'`);
368
+ }
369
+ return out;
370
+ }
371
+
372
+ function emitMssqlCreate(
373
+ table: CanonicalTable,
374
+ name: string,
375
+ ine: string,
376
+ mssqlDatabase?: string
377
+ ): string[] {
378
+ const q = (n: string) => quote('mssql', n);
379
+ const qualified = mssqlDatabase ? `[${mssqlDatabase}].[dbo].${q(name)}` : q(name);
380
+ const defs = table.columns.map((col) => {
381
+ let def = `${q(col.name)} ${renderType(col, 'mssql')}`;
382
+ if (!col.nullable) def += ' NOT NULL';
383
+ const dv = renderDefault(col);
384
+ if (dv !== null) def += ` DEFAULT ${dv}`;
385
+ if (col.effectiveAutoIncrement) def += ' IDENTITY(1,1)';
386
+ return def;
387
+ });
388
+ if (table.primaryKey.length > 0) {
389
+ defs.push(`PRIMARY KEY (${table.primaryKey.map(q).join(', ')})`);
390
+ }
391
+ const out = [`CREATE TABLE${ine} ${qualified} (\n ${defs.join(',\n ')}\n)`];
392
+ if (table.comment) out.push(mssqlTableComment(name, table.comment));
393
+ for (const col of table.columns) {
394
+ if (col.comment) out.push(mssqlColumnComment(name, col.name, col.comment));
395
+ }
396
+ return out;
397
+ }
398
+
399
+ function mssqlTableComment(table: string, comment: string): string {
400
+ return (
401
+ `EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'${esc(comment)}', ` +
402
+ `@level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'${esc(table)}'`
403
+ );
404
+ }
405
+
406
+ function emitClickhouseCreate(
407
+ table: CanonicalTable,
408
+ name: string,
409
+ ine: string,
410
+ engine: string
411
+ ): string {
412
+ const q = (n: string) => quote('clickhouse', n);
413
+ const defs = table.columns.map((col) => {
414
+ // ClickHouse 的可空性由 Nullable(...) 表达,裸类型即不可空;
415
+ // 不能同时写 NOT NULL(与 Nullable 冲突且是 no-op)。已是 Nullable(...) 的不重复包装。
416
+ const base = renderType(col, 'clickhouse');
417
+ const type = col.nullable && !/^Nullable\(/i.test(base) ? `Nullable(${base})` : base;
418
+ let def = `${q(col.name)} ${type}`;
419
+ const dv = renderDefault(col);
420
+ if (dv !== null) def += ` DEFAULT ${dv}`;
421
+ if (col.comment) def += ` COMMENT '${esc(col.comment)}'`;
422
+ return def;
423
+ });
424
+ // ClickHouse 的主键语义必须落在 ENGINE 之后的 ORDER BY(主键默认即排序键)
425
+ let sql = `CREATE TABLE${ine} ${q(name)} (\n ${defs.join(',\n ')}\n) ENGINE = ${engine}`;
426
+ sql += table.primaryKey.length > 0
427
+ ? ` ORDER BY (${table.primaryKey.map(q).join(', ')})`
428
+ : ' ORDER BY tuple()';
429
+ if (table.comment) sql += ` COMMENT '${esc(table.comment)}'`;
430
+ return sql;
431
+ }
432
+
433
+ /** SAP HANA:CREATE COLUMN TABLE(方言 createTable 既有形态,含内联 COMMENT 与 IDENTITY) */
434
+ function emitSapCreate(table: CanonicalTable, name: string): string {
435
+ const q = (n: string) => quote('sap', n);
436
+ const defs = table.columns.map((col) => {
437
+ let def = `${q(col.name)} ${renderType(col, 'sap')}`;
438
+ if (!col.nullable) def += ' NOT NULL';
439
+ const dv = renderDefault(col);
440
+ if (dv !== null) def += ` DEFAULT ${dv}`;
441
+ if (col.effectiveAutoIncrement) def += ' GENERATED BY DEFAULT AS IDENTITY';
442
+ if (col.comment) def += ` COMMENT '${esc(col.comment)}'`;
443
+ return def;
444
+ });
445
+ if (table.primaryKey.length > 0) {
446
+ defs.push(`PRIMARY KEY (${table.primaryKey.map(q).join(', ')})`);
447
+ }
448
+ // 表注释为 HANA 的表级 COMMENT 后缀(对齐旧方言 createTable 行为)
449
+ let sql = `CREATE COLUMN TABLE ${q(name)} (\n ${defs.join(',\n ')}\n)`;
450
+ if (table.comment) sql += ` COMMENT '${esc(table.comment)}'`;
451
+ return sql;
452
+ }
453
+
454
+ // ---------------------------------------------------------------- MySQL
455
+
456
+ /** 整列定义(CREATE / CHANGE COLUMN / MODIFY COLUMN 共用) */
457
+ function mysqlColumnDef(kind: DialectKind, col: CanonicalColumn): string {
458
+ let def = `${quote(kind, col.name)} ${renderType(col, kind)}`;
459
+ if (!col.nullable) def += ' NOT NULL';
460
+ const dv = renderDefault(col);
461
+ if (dv !== null) def += ` DEFAULT ${dv}`;
462
+ if (col.effectiveAutoIncrement) def += ' AUTO_INCREMENT';
463
+ if (col.comment) def += ` COMMENT '${esc(col.comment)}'`;
464
+ return def;
465
+ }
466
+
467
+ function emitMysqlAlter(cs: TableChangeSet, desired: CanonicalTable): string[] {
468
+ const q = (n: string) => quote('mysql', n);
469
+ const out: string[] = [];
470
+ const byName = new Map(desired.columns.map((c) => [c.name, c]));
471
+ const table = cs.rename ? cs.rename.from : desired.name;
472
+
473
+ const newPkSet = new Set(cs.primaryKey ? cs.primaryKey.to : desired.primaryKey);
474
+
475
+ // ---- 1) 去自增:MySQL 的 AUTO_INCREMENT 列"必须是一个键"。
476
+ // 主键被替换/删除前必须先降级,否则随后的 DROP PRIMARY KEY 会让表出现
477
+ //「无主键 + 仍有自增列」的中间态 → ER_WRONG_AUTO_KEY (1075)。
478
+ const downgraded = new Set<string>();
479
+ const downgrade = (col: CanonicalColumn) => {
480
+ if (downgraded.has(col.name)) return;
481
+ downgraded.add(col.name);
482
+ out.push(`ALTER TABLE ${q(table)} MODIFY COLUMN ${mysqlColumnDef('mysql', { ...col, effectiveAutoIncrement: false })}`);
483
+ };
484
+ for (const m of cs.modified) {
485
+ if (m.before.effectiveAutoIncrement && !m.after.effectiveAutoIncrement) downgrade(m.after);
486
+ }
487
+ // 兜底:目标态仍标自增却不在新主键中的列(UI 已禁止,直传 spec 时可能发生)
488
+ if (cs.primaryKey) {
489
+ for (const col of desired.columns) {
490
+ if (col.effectiveAutoIncrement && !newPkSet.has(col.name)) downgrade(col);
491
+ }
492
+ }
493
+
494
+ // ---- 2) 新增列(先于主键变更:新主键可能包含这些列)。
495
+ // 自增单独延后:ADD COLUMN 时键还不存在,带 AUTO_INCREMENT 同样触发 ER_WRONG_AUTO_KEY。
496
+ for (const col of cs.added) {
497
+ out.push(`ALTER TABLE ${q(table)} ADD COLUMN ${mysqlColumnDef('mysql', { ...col, effectiveAutoIncrement: false })}`);
498
+ }
499
+ const addedNeedAi = new Set(cs.added.filter((c) => c.effectiveAutoIncrement).map((c) => c.name));
500
+
501
+ // ---- 3) 主键变更:DROP 与 ADD 必须合并进同一条 ALTER 语句。
502
+ // 拆成两条时第一条执行完表处于无主键中间态,若表上仍有自增列即报错。
503
+ if (cs.primaryKey) {
504
+ const parts: string[] = [];
505
+ if (cs.primaryKey.from.length > 0) parts.push('DROP PRIMARY KEY');
506
+ if (cs.primaryKey.to.length > 0) {
507
+ parts.push(`ADD PRIMARY KEY (${cs.primaryKey.to.map(q).join(', ')})`);
508
+ }
509
+ if (parts.length > 0) out.push(`ALTER TABLE ${q(table)} ${parts.join(', ')}`);
510
+ }
511
+
512
+ // ---- 4) 加自增:主键就位之后才能声明 AUTO_INCREMENT
513
+ const upgraded = new Set<string>();
514
+ const upgrade = (col: CanonicalColumn) => {
515
+ if (upgraded.has(col.name)) return;
516
+ upgraded.add(col.name);
517
+ out.push(`ALTER TABLE ${q(table)} MODIFY COLUMN ${mysqlColumnDef('mysql', col)}`);
518
+ };
519
+ for (const name of addedNeedAi) {
520
+ const col = byName.get(name);
521
+ if (col?.effectiveAutoIncrement) upgrade(col);
522
+ }
523
+ for (const m of cs.modified) {
524
+ if (!m.before.effectiveAutoIncrement && m.after.effectiveAutoIncrement) upgrade(m.after);
525
+ }
526
+
527
+ // ---- 5) 改名(含定义变化时合并为一条 CHANGE COLUMN——保留列位置与数据)
528
+ for (const r of cs.renamed) {
529
+ const after = byName.get(r.to);
530
+ if (!after) continue;
531
+ if (r.definitionChanged) {
532
+ out.push(`ALTER TABLE ${q(table)} CHANGE COLUMN ${q(r.from)} ${mysqlColumnDef('mysql', after)}`);
533
+ } else {
534
+ out.push(`ALTER TABLE ${q(table)} RENAME COLUMN ${q(r.from)} TO ${q(r.to)}`);
535
+ }
536
+ }
537
+
538
+ // ---- 6) 其余列定义修改(自增的升/降级已在 1)/4) 处理)
539
+ for (const m of cs.modified) {
540
+ if (downgraded.has(m.name) || upgraded.has(m.name)) continue;
541
+ out.push(`ALTER TABLE ${q(table)} MODIFY COLUMN ${mysqlColumnDef('mysql', m.after)}`);
542
+ }
543
+
544
+ // ---- 7) 删除列(先于表重命名,使用旧表名)
545
+ for (const name of cs.dropped) {
546
+ out.push(`ALTER TABLE ${q(table)} DROP COLUMN ${q(name)}`);
547
+ }
548
+
549
+ // 7) 表注释(新表名语义,但 MySQL 表未改名时无歧义;改名场景随 8) 生效)
550
+ if (cs.comment) {
551
+ out.push(`ALTER TABLE ${q(cs.rename ? cs.rename.to : table)} COMMENT='${esc(cs.comment.to)}'`);
552
+ }
553
+
554
+ // 8) 表重命名最后执行(避免前述语句引用失效)
555
+ if (cs.rename) {
556
+ out.push(`RENAME TABLE ${q(cs.rename.from)} TO ${q(cs.rename.to)}`);
557
+ }
558
+ return out;
559
+ }
560
+
561
+ // ---------------------------------------------------------------- PostgreSQL / CockroachDB
562
+
563
+ function emitPgAlter(kind: DialectKind, cs: TableChangeSet, desired: CanonicalTable): string[] {
564
+ const q = (n: string) => quote(kind, n);
565
+ const out: string[] = [];
566
+ const byName = new Map(desired.columns.map((c) => [c.name, c]));
567
+ const table = cs.rename ? cs.rename.from : desired.name;
568
+
569
+ // 表注释
570
+ if (cs.comment) {
571
+ out.push(`COMMENT ON TABLE ${q(table)} IS '${esc(cs.comment.to)}'`);
572
+ }
573
+
574
+ // 改名(改名后定义变化用新名继续 ALTER;RENAME 必须先于其它 ALTER 执行)
575
+ for (const r of cs.renamed) {
576
+ out.push(`ALTER TABLE ${q(table)} RENAME COLUMN ${q(r.from)} TO ${q(r.to)}`);
577
+ const after = byName.get(r.to);
578
+ if (after && r.definitionChanged) {
579
+ pushPgColumnAlters(out, kind, table, after, r.modifiedFields);
580
+ if (r.modifiedFields.includes('comment')) {
581
+ pushPgColumnComment(out, kind, table, after);
582
+ }
583
+ }
584
+ }
585
+
586
+ // 新增列
587
+ for (const col of cs.added) {
588
+ out.push(`ALTER TABLE ${q(table)} ADD COLUMN ${pgColumnDef(kind, col)}`);
589
+ if (col.comment) pushPgColumnComment(out, kind, table, col);
590
+ }
591
+
592
+ // 修改列:按 fields 精确输出(单归属——不再无条件输出三条冗余语句)
593
+ for (const m of cs.modified) {
594
+ pushPgColumnAlters(out, kind, table, m.after, m.fields);
595
+ if (m.fields.includes('comment')) {
596
+ pushPgColumnComment(out, kind, table, m.after);
597
+ }
598
+ }
599
+
600
+ // 删除列
601
+ for (const name of cs.dropped) {
602
+ out.push(`ALTER TABLE ${q(table)} DROP COLUMN ${q(name)}`);
603
+ }
604
+
605
+ // 主键
606
+ if (cs.primaryKey) {
607
+ if (cs.primaryKey.from.length > 0) {
608
+ out.push(`ALTER TABLE ${q(table)} DROP CONSTRAINT ${q(`${table}_pkey`)}`);
609
+ }
610
+ if (cs.primaryKey.to.length > 0) {
611
+ out.push(`ALTER TABLE ${q(table)} ADD CONSTRAINT ${q(`${table}_pkey`)} PRIMARY KEY (${cs.primaryKey.to.map(q).join(', ')})`);
612
+ }
613
+ }
614
+
615
+ // 表重命名最后执行
616
+ if (cs.rename) {
617
+ out.push(`ALTER TABLE ${q(cs.rename.from)} RENAME TO ${q(cs.rename.to)}`);
618
+ }
619
+ return out;
620
+ }
621
+
622
+ function pushPgColumnAlters(
623
+ out: string[],
624
+ kind: DialectKind,
625
+ table: string,
626
+ col: CanonicalColumn,
627
+ fields: TableChangeSet['modified'][number]['fields']
628
+ ): void {
629
+ const q = (n: string) => quote(kind, n);
630
+ if (fields.includes('type')) {
631
+ out.push(`ALTER TABLE ${q(table)} ALTER COLUMN ${q(col.name)} SET DATA TYPE ${renderType(col, kind)}`);
632
+ }
633
+ if (fields.includes('nullable')) {
634
+ out.push(
635
+ col.nullable
636
+ ? `ALTER TABLE ${q(table)} ALTER COLUMN ${q(col.name)} DROP NOT NULL`
637
+ : `ALTER TABLE ${q(table)} ALTER COLUMN ${q(col.name)} SET NOT NULL`
638
+ );
639
+ }
640
+ if (fields.includes('default')) {
641
+ const dv = renderDefault(col);
642
+ out.push(
643
+ dv === null
644
+ ? `ALTER TABLE ${q(table)} ALTER COLUMN ${q(col.name)} DROP DEFAULT`
645
+ : `ALTER TABLE ${q(table)} ALTER COLUMN ${q(col.name)} SET DEFAULT ${dv}`
646
+ );
647
+ }
648
+ // autoIncrement 字段变化(PG 需要序列迁移,渲染层按 IDENTITY 表达)
649
+ if (fields.includes('autoIncrement')) {
650
+ if (col.effectiveAutoIncrement && !/SERIAL/i.test(renderType(col, kind))) {
651
+ out.push(`ALTER TABLE ${q(table)} ALTER COLUMN ${q(col.name)} ADD GENERATED ALWAYS AS IDENTITY`);
652
+ } else {
653
+ out.push(`ALTER TABLE ${q(table)} ALTER COLUMN ${q(col.name)} DROP IDENTITY IF EXISTS`);
654
+ }
655
+ }
656
+ }
657
+
658
+ function pushPgColumnComment(
659
+ out: string[],
660
+ kind: DialectKind,
661
+ table: string,
662
+ col: CanonicalColumn
663
+ ): void {
664
+ const q = (n: string) => quote(kind, n);
665
+ out.push(`COMMENT ON COLUMN ${q(table)}.${q(col.name)} IS '${esc(col.comment)}'`);
666
+ }
667
+
668
+ // ---------------------------------------------------------------- SQLite(重建表)
669
+
670
+ function emitSqliteRebuild(
671
+ original: CanonicalTable,
672
+ desired: CanonicalTable,
673
+ cs: TableChangeSet
674
+ ): string[] {
675
+ const q = (n: string) => quote('sqlite', n);
676
+ const table = cs.rename ? cs.rename.from : desired.name;
677
+ const temp = `_rebuild_${table}`;
678
+ const renameToOld = new Map(cs.renamed.map((r) => [r.to, r.from]));
679
+
680
+ // CREATE:按 desired 全量列建临时表——与前端建表共用 emitCreateDdl(单一出口)
681
+ const createSql = emitCreateDdl('sqlite', desired, { tableNameOverride: temp })[0] ?? '';
682
+
683
+ // INSERT ... SELECT:改名列从旧名取值;新列用默认值/NULL 占位;删除列不迁移
684
+ const pairs = desired.columns.map((col) => {
685
+ const oldName = renameToOld.get(col.name);
686
+ if (oldName) return { target: col.name, expr: q(oldName) };
687
+ const originalCol = original.columns.find((c) => c.name === col.name);
688
+ if (originalCol) return { target: col.name, expr: q(col.name) };
689
+ const dv = renderDefault(col);
690
+ return { target: col.name, expr: dv ?? 'NULL' };
691
+ });
692
+
693
+ const out: string[] = [];
694
+ out.push('-- SQLite 不支持直接 ALTER 列/主键,以下为重建表序列(请置于事务中执行)');
695
+ out.push('PRAGMA foreign_keys = OFF');
696
+ out.push(`DROP TABLE IF EXISTS ${q(temp)}`);
697
+ out.push(createSql);
698
+ out.push(`INSERT INTO ${q(temp)} (${pairs.map((p) => q(p.target)).join(', ')}) SELECT ${pairs.map((p) => p.expr).join(', ')} FROM ${q(table)}`);
699
+ out.push(`DROP TABLE ${q(table)}`);
700
+ out.push(`ALTER TABLE ${q(temp)} RENAME TO ${q(desired.name)}`);
701
+ out.push('PRAGMA foreign_keys = ON');
702
+ return out;
703
+ }
704
+
705
+ // ---------------------------------------------------------------- Oracle
706
+
707
+ function emitOracleAlter(cs: TableChangeSet, desired: CanonicalTable): string[] {
708
+ const q = (n: string) => quote('oracle', n);
709
+ const out: string[] = [];
710
+ const byName = new Map(desired.columns.map((c) => [c.name, c]));
711
+ const table = cs.rename ? cs.rename.from : desired.name;
712
+
713
+ if (cs.comment) {
714
+ out.push(`COMMENT ON TABLE ${q(table)} IS '${esc(cs.comment.to)}'`);
715
+ }
716
+
717
+ for (const r of cs.renamed) {
718
+ out.push(`ALTER TABLE ${q(table)} RENAME COLUMN ${q(r.from)} TO ${q(r.to)}`);
719
+ const after = byName.get(r.to);
720
+ if (after && r.definitionChanged) {
721
+ out.push(`ALTER TABLE ${q(table)} MODIFY (${oracleColumnDef(after)})`);
722
+ if (r.modifiedFields.includes('comment')) {
723
+ out.push(`COMMENT ON COLUMN ${q(table)}.${q(after.name)} IS '${esc(after.comment)}'`);
724
+ }
725
+ }
726
+ }
727
+
728
+ for (const col of cs.added) {
729
+ out.push(`ALTER TABLE ${q(table)} ADD (${oracleColumnDef(col)})`);
730
+ if (col.comment) {
731
+ out.push(`COMMENT ON COLUMN ${q(table)}.${q(col.name)} IS '${esc(col.comment)}'`);
732
+ }
733
+ }
734
+
735
+ for (const m of cs.modified) {
736
+ out.push(`ALTER TABLE ${q(table)} MODIFY (${oracleColumnDef(m.after)})`);
737
+ if (m.fields.includes('comment')) {
738
+ out.push(`COMMENT ON COLUMN ${q(table)}.${q(m.after.name)} IS '${esc(m.after.comment)}'`);
739
+ }
740
+ }
741
+
742
+ for (const name of cs.dropped) {
743
+ out.push(`ALTER TABLE ${q(table)} DROP COLUMN ${q(name)}`);
744
+ }
745
+
746
+ if (cs.primaryKey) {
747
+ if (cs.primaryKey.from.length > 0) {
748
+ out.push(`ALTER TABLE ${q(table)} DROP CONSTRAINT ${q(`${table}_pkey`)}`);
749
+ }
750
+ if (cs.primaryKey.to.length > 0) {
751
+ out.push(`ALTER TABLE ${q(table)} ADD CONSTRAINT ${q(`${table}_pkey`)} PRIMARY KEY (${cs.primaryKey.to.map(q).join(', ')})`);
752
+ }
753
+ }
754
+
755
+ if (cs.rename) {
756
+ out.push(`ALTER TABLE ${q(cs.rename.from)} RENAME TO ${q(cs.rename.to)}`);
757
+ }
758
+ return out;
759
+ }
760
+
761
+ function oracleColumnDef(col: CanonicalColumn): string {
762
+ let def = `${quote('oracle', col.name)} ${renderType(col, 'oracle')}`;
763
+ if (!col.nullable) def += ' NOT NULL';
764
+ const dv = renderDefault(col);
765
+ if (dv !== null) def += ` DEFAULT ${dv}`;
766
+ if (col.effectiveAutoIncrement && /NUMBER|NUMERIC|DEC/i.test(renderType(col, 'oracle'))) {
767
+ def += ' GENERATED BY DEFAULT AS IDENTITY';
768
+ }
769
+ return def;
770
+ }
771
+
772
+ // ---------------------------------------------------------------- ClickHouse
773
+
774
+ /**
775
+ * ClickHouse:ADD / DROP / MODIFY / RENAME COLUMN。
776
+ *
777
+ * 注意两点与通用方言的差异:
778
+ * - 可空性由 Nullable(...) 表达,裸类型即不可空,不能写 NOT NULL(与 emitClickhouseCreate 一致);
779
+ * - 主键=排序键(ORDER BY),不是普通约束,变更需 MODIFY ORDER BY 且限制很多,
780
+ * 能力表已声明 alterPrimaryKey=false,调用方会先报明确错误,故这里不臆造主键语句。
781
+ */
782
+ function emitClickHouseAlter(cs: TableChangeSet, desired: CanonicalTable): string[] {
783
+ const q = (n: string) => quote('clickhouse', n);
784
+ const out: string[] = [];
785
+ const byName = new Map(desired.columns.map((c) => [c.name, c]));
786
+ const table = cs.rename ? cs.rename.from : desired.name;
787
+
788
+ const defOf = (col: CanonicalColumn): string => {
789
+ const base = renderType(col, 'clickhouse');
790
+ const type = col.nullable && !/^Nullable\(/i.test(base) ? `Nullable(${base})` : base;
791
+ let def = `${q(col.name)} ${type}`;
792
+ const dv = renderDefault(col);
793
+ if (dv !== null) def += ` DEFAULT ${dv}`;
794
+ if (col.comment) def += ` COMMENT '${esc(col.comment)}'`;
795
+ return def;
796
+ };
797
+
798
+ for (const col of cs.added) {
799
+ out.push(`ALTER TABLE ${q(table)} ADD COLUMN ${defOf(col)}`);
800
+ }
801
+
802
+ // 重命名:先改名,再补一条 MODIFY COLUMN 应用同时发生的定义变化
803
+ for (const r of cs.renamed) {
804
+ out.push(`ALTER TABLE ${q(table)} RENAME COLUMN ${q(r.from)} TO ${q(r.to)}`);
805
+ const after = byName.get(r.to);
806
+ if (after && r.definitionChanged) {
807
+ out.push(`ALTER TABLE ${q(table)} MODIFY COLUMN ${defOf(after)}`);
808
+ }
809
+ }
810
+
811
+ for (const m of cs.modified) {
812
+ out.push(`ALTER TABLE ${q(table)} MODIFY COLUMN ${defOf(m.after)}`);
813
+ }
814
+
815
+ for (const name of cs.dropped) {
816
+ out.push(`ALTER TABLE ${q(table)} DROP COLUMN ${q(name)}`);
817
+ }
818
+
819
+ if (cs.comment) {
820
+ out.push(`ALTER TABLE ${q(table)} MODIFY COMMENT '${esc(cs.comment.to)}'`);
821
+ }
822
+
823
+ if (cs.rename) {
824
+ out.push(`RENAME TABLE ${q(cs.rename.from)} TO ${q(cs.rename.to)}`);
825
+ }
826
+
827
+ return out;
828
+ }
829
+
830
+ // ---------------------------------------------------------------- SQL Server
831
+
832
+ function emitMssqlAlter(
833
+ cs: TableChangeSet,
834
+ desired: CanonicalTable,
835
+ pkConstraintName?: string
836
+ ): string[] {
837
+ const q = (n: string) => quote('mssql', n);
838
+ const out: string[] = [];
839
+ const byName = new Map(desired.columns.map((c) => [c.name, c]));
840
+ const table = cs.rename ? cs.rename.from : desired.name;
841
+ const newPkSet = new Set(desired.primaryKey);
842
+
843
+ if (cs.comment) {
844
+ out.push(mssqlTableComment(table, cs.comment.to));
845
+ }
846
+
847
+ for (const r of cs.renamed) {
848
+ out.push(`EXEC sp_rename N'dbo.${esc(table)}.${esc(r.from)}', N'${esc(r.to)}', N'COLUMN'`);
849
+ const after = byName.get(r.to);
850
+ if (after && r.definitionChanged) {
851
+ pushMssqlColumnAlter(out, table, after);
852
+ }
853
+ }
854
+
855
+ for (const col of cs.added) {
856
+ let def = `ALTER TABLE ${q(table)} ADD ${q(col.name)} ${renderType(col, 'mssql')}`;
857
+ if (!col.nullable) def += ' NOT NULL';
858
+ const dv = renderDefault(col);
859
+ if (dv !== null) def += ` DEFAULT ${dv}`;
860
+ out.push(def);
861
+ if (col.effectiveAutoIncrement) {
862
+ // IDENTITY 不能 ALTER 添加,仅提示
863
+ out.push(`-- 列 ${col.name} 需要自增:SQL Server 不支持对已有列追加 IDENTITY`);
864
+ }
865
+ if (col.comment) {
866
+ out.push(mssqlColumnComment(table, col.name, col.comment));
867
+ }
868
+ }
869
+
870
+ for (const m of cs.modified) {
871
+ if (m.fields.includes('type') || m.fields.includes('nullable')) {
872
+ pushMssqlColumnAlter(out, table, m.after);
873
+ }
874
+ if (m.fields.includes('default')) {
875
+ const dv = renderDefault(m.after);
876
+ const cname = `DF_${table}_${m.after.name}`;
877
+ if (dv === null) {
878
+ out.push(`ALTER TABLE ${q(table)} DROP CONSTRAINT ${q(cname)}`);
879
+ } else {
880
+ out.push(`ALTER TABLE ${q(table)} ADD CONSTRAINT ${q(cname)} DEFAULT ${dv} FOR ${q(m.after.name)}`);
881
+ }
882
+ }
883
+ if (m.fields.includes('autoIncrement')) {
884
+ out.push(`-- 列 ${m.after.name} 自增变更:SQL Server 不支持对已有列追加/移除 IDENTITY`);
885
+ }
886
+ if (m.fields.includes('comment')) {
887
+ out.push(mssqlColumnComment(table, m.after.name, m.after.comment));
888
+ }
889
+ }
890
+
891
+ for (const name of cs.dropped) {
892
+ out.push(`ALTER TABLE ${q(table)} DROP COLUMN ${q(name)}`);
893
+ }
894
+
895
+ if (cs.primaryKey) {
896
+ if (cs.primaryKey.from.length > 0) {
897
+ // 删除旧主键必须用系统表查到的真实约束名;拿不到时调用方已判为错误,不会走到这里
898
+ out.push(`ALTER TABLE ${q(table)} DROP CONSTRAINT ${q(pkConstraintName || `${table}_pkey`)}`);
899
+ }
900
+ if (cs.primaryKey.to.length > 0) {
901
+ // 新建主键的约束名由我们指定,后续变更即可按同名回查到
902
+ out.push(`ALTER TABLE ${q(table)} ADD CONSTRAINT ${q(`${table}_pkey`)} PRIMARY KEY (${cs.primaryKey.to.map(q).join(', ')})`);
903
+ }
904
+ }
905
+
906
+ if (cs.rename) {
907
+ out.push(`EXEC sp_rename N'dbo.${esc(cs.rename.from)}', N'${esc(cs.rename.to)}'`);
908
+ }
909
+ void newPkSet;
910
+ return out;
911
+ }
912
+
913
+ function pushMssqlColumnAlter(out: string[], table: string, col: CanonicalColumn): void {
914
+ const q = (n: string) => quote('mssql', n);
915
+ out.push(
916
+ `ALTER TABLE ${q(table)} ALTER COLUMN ${q(col.name)} ${renderType(col, 'mssql')} ${col.nullable ? 'NULL' : 'NOT NULL'}`
917
+ );
918
+ }
919
+
920
+ function mssqlColumnComment(table: string, column: string, comment: string): string {
921
+ return (
922
+ `EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'${esc(comment)}', @level0type=N'SCHEMA', @level0name=N'dbo', ` +
923
+ `@level1type=N'TABLE', @level1name=N'${esc(table)}', @level2type=N'COLUMN', @level2name=N'${esc(column)}'`
924
+ );
925
+ }