fdb2 1.0.37 → 1.0.39

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 +47 -0
  2. package/bin/fdb2.js +78 -3
  3. package/dist/public/explorer.css +517 -478
  4. package/dist/public/explorer.js +1114 -872
  5. package/dist/public/index.css +87 -27
  6. package/dist/public/index.js +97 -49
  7. package/dist/public/layout.js +2 -2
  8. package/dist/public/vue.js +1 -1
  9. package/dist/server/core/schema/__tests__/canonical.test.ts +195 -0
  10. package/dist/server/core/schema/__tests__/ddl-emitter.test.ts +456 -0
  11. package/dist/server/core/schema/__tests__/diff.test.ts +242 -0
  12. package/dist/server/core/schema/__tests__/roundtrip.property.test.ts +403 -0
  13. package/dist/server/core/schema/canonical.d.ts +50 -0
  14. package/dist/server/core/schema/canonical.d.ts.map +1 -0
  15. package/dist/server/core/schema/canonical.js +212 -0
  16. package/dist/server/core/schema/canonical.js.map +1 -0
  17. package/dist/server/core/schema/canonical.ts +223 -0
  18. package/dist/server/core/schema/ddl-emitter.d.ts +68 -0
  19. package/dist/server/core/schema/ddl-emitter.d.ts.map +1 -0
  20. package/dist/server/core/schema/ddl-emitter.js +813 -0
  21. package/dist/server/core/schema/ddl-emitter.js.map +1 -0
  22. package/dist/server/core/schema/ddl-emitter.ts +925 -0
  23. package/dist/server/core/schema/diff.d.ts +16 -0
  24. package/dist/server/core/schema/diff.d.ts.map +1 -0
  25. package/dist/server/core/schema/diff.js +115 -0
  26. package/dist/server/core/schema/diff.js.map +1 -0
  27. package/dist/server/core/schema/diff.ts +134 -0
  28. package/dist/server/core/schema/type-defaults.d.ts +27 -0
  29. package/dist/server/core/schema/type-defaults.d.ts.map +1 -0
  30. package/dist/server/core/schema/type-defaults.js +61 -0
  31. package/dist/server/core/schema/type-defaults.js.map +1 -0
  32. package/dist/server/core/schema/type-defaults.ts +79 -0
  33. package/dist/server/index.d.ts +1 -1
  34. package/dist/server/index.d.ts.map +1 -1
  35. package/dist/server/index.js +347 -124
  36. package/dist/server/index.js.map +1 -1
  37. package/dist/server/index.ts +336 -161
  38. package/dist/server/model/ddl-spec.d.ts +163 -0
  39. package/dist/server/model/ddl-spec.d.ts.map +1 -0
  40. package/dist/server/model/ddl-spec.js +32 -0
  41. package/dist/server/model/ddl-spec.js.map +1 -0
  42. package/dist/server/model/ddl-spec.ts +175 -0
  43. package/dist/server/service/connection.service.d.ts +20 -0
  44. package/dist/server/service/connection.service.d.ts.map +1 -1
  45. package/dist/server/service/connection.service.js +99 -25
  46. package/dist/server/service/connection.service.js.map +1 -1
  47. package/dist/server/service/connection.service.ts +104 -25
  48. package/dist/server/service/database/base.service.d.ts +0 -4
  49. package/dist/server/service/database/base.service.d.ts.map +1 -1
  50. package/dist/server/service/database/base.service.js.map +1 -1
  51. package/dist/server/service/database/base.service.ts +0 -5
  52. package/dist/server/service/database/clickhouse.service.d.ts +0 -5
  53. package/dist/server/service/database/clickhouse.service.d.ts.map +1 -1
  54. package/dist/server/service/database/clickhouse.service.js +0 -49
  55. package/dist/server/service/database/clickhouse.service.js.map +1 -1
  56. package/dist/server/service/database/clickhouse.service.ts +0 -52
  57. package/dist/server/service/database/cockroachdb.service.d.ts +0 -5
  58. package/dist/server/service/database/cockroachdb.service.d.ts.map +1 -1
  59. package/dist/server/service/database/cockroachdb.service.js +11 -142
  60. package/dist/server/service/database/cockroachdb.service.js.map +1 -1
  61. package/dist/server/service/database/cockroachdb.service.ts +11 -161
  62. package/dist/server/service/database/database.service.d.ts +55 -2
  63. package/dist/server/service/database/database.service.d.ts.map +1 -1
  64. package/dist/server/service/database/database.service.js +246 -4
  65. package/dist/server/service/database/database.service.js.map +1 -1
  66. package/dist/server/service/database/database.service.ts +295 -7
  67. package/dist/server/service/database/mongodb.service.d.ts +0 -6
  68. package/dist/server/service/database/mongodb.service.d.ts.map +1 -1
  69. package/dist/server/service/database/mongodb.service.js +0 -8
  70. package/dist/server/service/database/mongodb.service.js.map +1 -1
  71. package/dist/server/service/database/mongodb.service.ts +0 -8
  72. package/dist/server/service/database/mssql.service.d.ts +8 -4
  73. package/dist/server/service/database/mssql.service.d.ts.map +1 -1
  74. package/dist/server/service/database/mssql.service.js +27 -101
  75. package/dist/server/service/database/mssql.service.js.map +1 -1
  76. package/dist/server/service/database/mssql.service.ts +28 -122
  77. package/dist/server/service/database/mysql.service.d.ts +0 -4
  78. package/dist/server/service/database/mysql.service.d.ts.map +1 -1
  79. package/dist/server/service/database/mysql.service.js +11 -137
  80. package/dist/server/service/database/mysql.service.js.map +1 -1
  81. package/dist/server/service/database/mysql.service.ts +11 -173
  82. package/dist/server/service/database/oracle.service.d.ts +0 -4
  83. package/dist/server/service/database/oracle.service.d.ts.map +1 -1
  84. package/dist/server/service/database/oracle.service.js +11 -104
  85. package/dist/server/service/database/oracle.service.js.map +1 -1
  86. package/dist/server/service/database/oracle.service.ts +11 -127
  87. package/dist/server/service/database/postgres.service.d.ts +0 -4
  88. package/dist/server/service/database/postgres.service.d.ts.map +1 -1
  89. package/dist/server/service/database/postgres.service.js +11 -112
  90. package/dist/server/service/database/postgres.service.js.map +1 -1
  91. package/dist/server/service/database/postgres.service.ts +11 -133
  92. package/dist/server/service/database/sap.service.d.ts +0 -4
  93. package/dist/server/service/database/sap.service.d.ts.map +1 -1
  94. package/dist/server/service/database/sap.service.js +11 -137
  95. package/dist/server/service/database/sap.service.js.map +1 -1
  96. package/dist/server/service/database/sap.service.ts +11 -158
  97. package/dist/server/service/database/sql-type.helper.d.ts.map +1 -1
  98. package/dist/server/service/database/sql-type.helper.js +9 -47
  99. package/dist/server/service/database/sql-type.helper.js.map +1 -1
  100. package/dist/server/service/database/sql-type.helper.ts +11 -53
  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 -147
  104. package/dist/server/service/database/sqlite.service.js.map +1 -1
  105. package/dist/server/service/database/sqlite.service.ts +11 -177
  106. package/package.json +1 -1
  107. package/server.js +1 -1
@@ -1,19 +1,308 @@
1
1
  import * as http from "node:http";
2
+ import * as https from "node:https";
2
3
  import { ConnectionService } from './service/connection.service';
3
4
  import { DatabaseService } from './service/database/database.service';
4
5
 
6
+ // 远程代理相关常量
7
+ const REMOTE_ID_SEP = '::';
8
+ const REMOTE_TOKEN_HEADER = 'x-db-tool-token';
9
+ // 代理请求标记:用于识别"本请求本身来自代理",避免远程地址指向自身(或互指)时无限递归合并
10
+ const REMOTE_PROXY_HEADER = 'x-db-tool-proxy';
11
+
12
+ // 永不转发的路由:只作用于本地连接配置(无远程对应物或需要本地语义)
13
+ const LOCAL_ONLY_ROUTES = new Set<string>([
14
+ '/api/database/getConnection',
15
+ '/api/database/addConnection',
16
+ '/api/database/getSupportedDatabaseTypes'
17
+ ]);
18
+
19
+ // 连接配置类路由:复合 id 时转发到远程,但允许直接操作「远程代理配置」本身
20
+ const CONNECTION_CONFIG_ROUTES = new Set<string>([
21
+ '/api/database/getConnections',
22
+ '/api/database/getConnection',
23
+ '/api/database/addConnection',
24
+ '/api/database/updateConnection',
25
+ '/api/database/deleteConnection',
26
+ '/api/database/testConnection',
27
+ '/api/database/getSupportedDatabaseTypes'
28
+ ]);
29
+
30
+ function isRemoteConnection(conn: any): boolean {
31
+ return !!conn && conn.type === 'remote';
32
+ }
33
+
34
+ /** 从请求中取出被操作连接的 id(body 或路径段) */
35
+ function extractConnId(pathname: string, body: any): string | undefined {
36
+ if (pathname.startsWith('/api/database/truncateTable/') || pathname.startsWith('/api/database/dropTable/')) {
37
+ const parts = pathname.split('/').filter(Boolean);
38
+ return parts[3];
39
+ }
40
+ if (body && typeof body.id === 'string') return body.id;
41
+ return undefined;
42
+ }
43
+
44
+ /**
45
+ * 拆分复合 id:proxyId::remoteConnId。
46
+ * remoteConnId 自身可能仍是复合 id(多级代理),必须保留剩余部分,不能用 split 截断。
47
+ */
48
+ function splitProxyId(connId: string): { proxyId: string; remoteConnId: string } | null {
49
+ const idx = connId.indexOf(REMOTE_ID_SEP);
50
+ if (idx <= 0) return null;
51
+ return {
52
+ proxyId: connId.slice(0, idx),
53
+ remoteConnId: connId.slice(idx + REMOTE_ID_SEP.length)
54
+ };
55
+ }
56
+
57
+ /** 判断是否为指向远程的复合 id(proxyId::remoteConnId) */
58
+ async function resolveRemoteProxy(pathname: string, body: any): Promise<{ proxyConn: any; remoteConnId: string } | null> {
59
+ if (LOCAL_ONLY_ROUTES.has(pathname)) return null;
60
+ const connId = extractConnId(pathname, body);
61
+ if (!connId || !connId.includes(REMOTE_ID_SEP)) return null;
62
+ const parts = splitProxyId(connId);
63
+ if (!parts) return null;
64
+ const proxyConn = await connectionService.getConnectionById(parts.proxyId);
65
+ if (!proxyConn || !isRemoteConnection(proxyConn)) return null;
66
+ return { proxyConn, remoteConnId: parts.remoteConnId };
67
+ }
68
+
69
+ /** 解析远程响应体(远程服务统一返回 { ret, msg, data } 信封) */
70
+ function parseRemotePayload(text: string): any {
71
+ if (!text) return {};
72
+ try {
73
+ return JSON.parse(text);
74
+ } catch {
75
+ return {};
76
+ }
77
+ }
78
+
79
+ /** 通过 Node 18+ 全局 fetch 调用远程服务 */
80
+ async function postRemoteByFetch(
81
+ url: string,
82
+ body: any,
83
+ headers: Record<string, string>,
84
+ timeoutMs: number
85
+ ): Promise<any> {
86
+ const controller = new AbortController();
87
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
88
+ try {
89
+ const resp = await fetch(url, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json', ...headers },
92
+ body: JSON.stringify(body ?? {}),
93
+ signal: controller.signal
94
+ });
95
+ const text = await resp.text();
96
+ const payload = parseRemotePayload(text);
97
+ if (!resp.ok) {
98
+ throw new Error(payload?.msg || `远程服务返回 HTTP ${resp.status}`);
99
+ }
100
+ return payload;
101
+ } catch (e: any) {
102
+ if (e?.name === 'AbortError') throw new Error('远程服务请求超时');
103
+ throw e;
104
+ } finally {
105
+ clearTimeout(timer);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 低版本 Node 没有全局 fetch 时的回退实现。
111
+ * VSCode 1.75-1.84 的扩展宿主内置 Node 16(无全局 fetch),扩展声明支持 ^1.75.0,故保留此路径。
112
+ */
113
+ function postRemoteByNodeHttp(
114
+ url: string,
115
+ body: any,
116
+ headers: Record<string, string>,
117
+ timeoutMs: number
118
+ ): Promise<any> {
119
+ return new Promise((resolve, reject) => {
120
+ let target: URL;
121
+ try {
122
+ target = new URL(url);
123
+ } catch {
124
+ reject(new Error(`远程服务地址无效: ${url}`));
125
+ return;
126
+ }
127
+ const payload = Buffer.from(JSON.stringify(body ?? {}));
128
+ const lib = target.protocol === 'https:' ? https : http;
129
+ const req = lib.request(
130
+ {
131
+ hostname: target.hostname,
132
+ port: target.port || (target.protocol === 'https:' ? 443 : 80),
133
+ path: (target.pathname || '/') + (target.search || ''),
134
+ method: 'POST',
135
+ headers: { ...headers, 'Content-Length': payload.length }
136
+ },
137
+ (res) => {
138
+ let raw = '';
139
+ res.setEncoding('utf8');
140
+ res.on('data', (chunk: string) => { raw += chunk; });
141
+ res.on('end', () => {
142
+ const data = parseRemotePayload(raw);
143
+ const status = res.statusCode || 0;
144
+ if (status < 200 || status >= 300) {
145
+ reject(new Error(data?.msg || `远程服务返回 HTTP ${status}`));
146
+ return;
147
+ }
148
+ resolve(data);
149
+ });
150
+ }
151
+ );
152
+ req.setTimeout(timeoutMs, () => { req.destroy(new Error('远程服务请求超时')); });
153
+ req.on('error', reject);
154
+ req.write(payload);
155
+ req.end();
156
+ });
157
+ }
158
+
159
+ /** 统一的远程调用入口:优先全局 fetch,低版本回退 node:http(s);返回 { ret, msg, data } 信封 */
160
+ async function postRemote(
161
+ url: string,
162
+ body: any,
163
+ headers: Record<string, string>,
164
+ timeoutMs: number
165
+ ): Promise<any> {
166
+ if (typeof fetch === 'function') {
167
+ return await postRemoteByFetch(url, body, headers, timeoutMs);
168
+ }
169
+ return await postRemoteByNodeHttp(url, body, headers, timeoutMs);
170
+ }
171
+
172
+ /** 构造带令牌与代理标记的请求头 */
173
+ function buildRemoteHeaders(proxyConn: any): Record<string, string> {
174
+ const headers: Record<string, string> = { [REMOTE_PROXY_HEADER]: '1' };
175
+ const token = proxyConn?.options?.remoteToken;
176
+ if (token) headers[REMOTE_TOKEN_HEADER] = String(token);
177
+ return headers;
178
+ }
179
+
180
+ /** 把请求原样转发到远程 db_tool 服务端(自动解包远程响应,避免二次包装) */
181
+ async function proxyToRemote(proxyConn: any, remoteConnId: string, pathname: string, body: any): Promise<any> {
182
+ const base = String(proxyConn.options?.remoteUrl || '').replace(/\/+$/, '');
183
+ if (!base) throw new Error('远程代理连接缺少 remoteUrl');
184
+ let targetPath = pathname;
185
+ if (pathname.startsWith('/api/database/truncateTable/') || pathname.startsWith('/api/database/dropTable/')) {
186
+ const parts = pathname.split('/').filter(Boolean);
187
+ // 不做 URL 编码:连接 id 由本地生成(仅含安全字符),而对端解析路径段时不做 decode,
188
+ // 编码会把 ':' 变成 %3A 导致对端无法解析复合 id。
189
+ parts[3] = remoteConnId;
190
+ targetPath = '/' + parts.join('/');
191
+ }
192
+ const targetBody = { ...body, id: remoteConnId };
193
+ const env = await postRemote(`${base}${targetPath}`, targetBody, buildRemoteHeaders(proxyConn), 60000);
194
+ if (env && typeof env === 'object' && env.ret && env.ret !== 0) {
195
+ throw new Error(env.msg || '远程服务执行失败');
196
+ }
197
+ return env?.data;
198
+ }
199
+
200
+ /** 拉取远程服务上的连接列表(用于网关模式合并,单层、过滤嵌套代理配置) */
201
+ async function fetchRemoteConnections(proxyConn: any): Promise<{ list: any[]; error?: string }> {
202
+ const base = String(proxyConn.options?.remoteUrl || '').replace(/\/+$/, '');
203
+ if (!base) return { list: [], error: '缺少 remoteUrl' };
204
+ try {
205
+ const env = await postRemote(`${base}/api/database/getConnections`, {}, buildRemoteHeaders(proxyConn), 10000);
206
+ if (env && env.ret && env.ret !== 0) {
207
+ console.warn('[remote] 获取远程连接列表失败:', env.msg);
208
+ return { list: [], error: env.msg || '远程服务返回错误' };
209
+ }
210
+ return { list: Array.isArray(env?.data) ? env.data : [] };
211
+ } catch (e: any) {
212
+ console.warn('[remote] 获取远程连接列表失败:', e?.message);
213
+ return { list: [], error: e?.message || '远程服务不可达' };
214
+ }
215
+ }
216
+
217
+ /** 网关模式:本地连接列表 = 本地连接 + 远程代理暴露的连接 */
218
+ async function handleGetConnectionsWithProxy(): Promise<any[]> {
219
+ const locals = await connectionService.getAllConnections();
220
+ // 并行拉取各远程服务,避免多个远程时串行等待
221
+ const remoteResults = await Promise.all(
222
+ locals.map(conn => (isRemoteConnection(conn) ? fetchRemoteConnections(conn) : Promise.resolve(null)))
223
+ );
224
+ const result: any[] = [];
225
+ locals.forEach((conn, index) => {
226
+ if (!isRemoteConnection(conn)) {
227
+ result.push(conn);
228
+ return;
229
+ }
230
+ const remote = remoteResults[index];
231
+ // 代理配置本身保留(用于编辑/删除),标记 _isProxy
232
+ result.push({ ...conn, _isProxy: true, _remoteError: remote?.error });
233
+ for (const rc of remote?.list || []) {
234
+ if (rc.type === 'remote' || rc._isProxy) continue; // 不暴露嵌套代理配置
235
+ const safe = { ...rc };
236
+ delete safe.password; // 不把远程库的密码下发到客户端
237
+ result.push({
238
+ ...safe,
239
+ id: `${conn.id}${REMOTE_ID_SEP}${rc.id}`,
240
+ _proxyId: conn.id,
241
+ _remote: true,
242
+ _remoteName: conn.name
243
+ });
244
+ }
245
+ });
246
+ return result;
247
+ }
248
+
249
+ /** 测试远程代理连接(探活远程服务) */
250
+ async function testRemoteConnection(conn: any): Promise<boolean> {
251
+ const base = String(conn.options?.remoteUrl || '').replace(/\/+$/, '');
252
+ if (!base) throw new Error('缺少 remoteUrl');
253
+ const env = await postRemote(`${base}/api/database/getConnections`, {}, buildRemoteHeaders(conn), 10000);
254
+ if (env && env.ret && env.ret !== 0) throw new Error(env.msg || '远程服务返回错误');
255
+ return true;
256
+ }
257
+
5
258
  // 初始化服务实例
6
259
  const connectionService = new ConnectionService();
7
260
  const databaseService = new DatabaseService();
8
261
 
9
262
 
10
263
  // 数据库相关路由处理
11
- export async function handleDatabaseRoutes(pathname: string, body: any) {
264
+ export async function handleDatabaseRoutes(pathname: string, body: any, headers?: any) {
12
265
  try {
13
- // /api/database/getConnections
266
+ // 远程访问令牌校验(仅当远程服务端设置了 DB_TOOL_REMOTE_TOKEN 且请求经由 HTTP 入口时生效)
267
+ const envToken = process.env.DB_TOOL_REMOTE_TOKEN;
268
+ if (envToken && headers) {
269
+ const incoming = String(headers[REMOTE_TOKEN_HEADER] || headers['x-db-tool-token'] || '');
270
+ if (incoming !== envToken) {
271
+ throw new Error('远程访问令牌无效');
272
+ }
273
+ }
274
+
275
+ // 网关模式:合并远程服务暴露的连接
14
276
  if (pathname === '/api/database/getConnections') {
15
- const result = await connectionService.getAllConnections();
16
- return result;
277
+ // 请求本身来自代理时不再二次合并,避免远程地址指向自身(或互指)造成无限递归
278
+ if (headers && headers[REMOTE_PROXY_HEADER]) {
279
+ return await connectionService.getAllConnections();
280
+ }
281
+ return await handleGetConnectionsWithProxy();
282
+ }
283
+
284
+ // 远程代理连接本身的测试:直接探活远程服务
285
+ if (pathname === '/api/database/testConnection' && body && isRemoteConnection(body)) {
286
+ return await testRemoteConnection(body);
287
+ }
288
+
289
+ // 复合 id(proxyId::remoteConnId)→ 转发到远程:
290
+ // 覆盖数据操作(查询/建表/改结构等)与连接配置的增删改
291
+ const proxy = await resolveRemoteProxy(pathname, body);
292
+ if (proxy) {
293
+ return await proxyToRemote(proxy.proxyConn, proxy.remoteConnId, pathname, body);
294
+ }
295
+
296
+ // 对「远程代理配置」本身做数据操作时给出明确提示(配置本身的增删改是允许的)
297
+ if (!CONNECTION_CONFIG_ROUTES.has(pathname)) {
298
+ const connId = extractConnId(pathname, body);
299
+ if (connId && !connId.includes(REMOTE_ID_SEP)) {
300
+ const conn = await connectionService.getConnectionById(connId);
301
+ if (conn && isRemoteConnection(conn)) {
302
+ if (pathname === '/api/database/closeConnection') return true; // 代理配置本身无需关闭
303
+ throw new Error(`请选择「${conn.name}」代理下的具体数据库连接进行操作`);
304
+ }
305
+ }
17
306
  }
18
307
 
19
308
  // /api/database/getConnection
@@ -245,22 +534,48 @@ export async function handleDatabaseRoutes(pathname: string, body: any) {
245
534
  return exportPath;
246
535
  }
247
536
 
248
- // /api/database/saveTableStructure
249
- if (pathname === '/api/database/saveTableStructure') {
250
- const { id, database, table, columns } = body;
251
- if (!id || !database || !table || !columns) throw Error('Missing parameters');
537
+ // /api/database/refreshTableCache(手动全量拉取表列表,忽略预览缓存)
538
+ if (pathname === '/api/database/refreshTableCache') {
539
+ const { id, database } = body;
540
+ if (!id || !database) throw Error('Missing parameters');
252
541
 
253
- const sql = generateTableSQL(table.name, columns, table);
254
- const result = await databaseService.executeQuery(id, sql, database);
255
- return { success: true, sql, result };
542
+ const result = await databaseService.refreshTableCache(id, database);
543
+ return result;
544
+ }
545
+
546
+ // /api/database/previewCreateTable(纯计算:目标态 TableSpec -> 建表 DDL 与方言能力)
547
+ if (pathname === '/api/database/previewCreateTable') {
548
+ const { id, database, spec } = body;
549
+ if (!id || !database || !spec) throw Error('Missing parameters');
550
+
551
+ const result = await databaseService.previewCreateTable(id, database, spec);
552
+ return result;
553
+ }
554
+
555
+ // /api/database/executeCreateTable(执行:后端检测重名后生成并执行建表语句)
556
+ if (pathname === '/api/database/executeCreateTable') {
557
+ const { id, database, spec } = body;
558
+ if (!id || !database || !spec) throw Error('Missing parameters');
559
+
560
+ const result = await databaseService.executeCreateTable(id, database, spec);
561
+ return result;
562
+ }
563
+
564
+ // /api/database/previewAlterTable(纯计算:目标态 TableSpec -> DDL 语句与变更集)
565
+ if (pathname === '/api/database/previewAlterTable') {
566
+ const { id, database, tableName, spec } = body;
567
+ if (!id || !database || !tableName || !spec) throw Error('Missing parameters');
568
+
569
+ const result = await databaseService.previewAlterTable(id, database, tableName, spec);
570
+ return result;
256
571
  }
257
572
 
258
- // /api/database/alterTable
259
- if (pathname === '/api/database/alterTable') {
260
- const { id, database, tableDiff } = body;
261
- if (!id || !database || !tableDiff) throw Error('Missing parameters');
573
+ // /api/database/executeAlterTable(执行:后端重读实时结构生成并执行,所见即所得)
574
+ if (pathname === '/api/database/executeAlterTable') {
575
+ const { id, database, tableName, spec } = body;
576
+ if (!id || !database || !tableName || !spec) throw Error('Missing parameters');
262
577
 
263
- const result = await databaseService.alterTable(id, database, tableDiff);
578
+ const result = await databaseService.executeAlterTable(id, database, tableName, spec);
264
579
  return result;
265
580
  }
266
581
 
@@ -378,7 +693,11 @@ export async function handleDatabaseRoutes(pathname: string, body: any) {
378
693
  // 使用数据库服务生成正确的CREATE VIEW语法
379
694
  const dataSource = await databaseService['connectionService'].getActiveConnection(id, database);
380
695
  const dbService = databaseService['getDatabaseService'](dataSource.options.type as string);
381
- const sql = `CREATE VIEW ${dbService['quoteIdentifier'](viewName)} AS ${definition}`;
696
+ // 防御:若传入的已是完整 CREATE VIEW 语句(SQLite/Oracle 的 getViewDefinition 形态),
697
+ // 直接执行,避免拼成 `CREATE VIEW v AS CREATE VIEW v AS ...`
698
+ const def = String(definition).trim();
699
+ const isFullCreate = /^CREATE\s+(?:OR\s+REPLACE\s+)?(?:ALGORITHM\s*=\s*\w+\s+)?VIEW\s/is.test(def);
700
+ const sql = isFullCreate ? def : `CREATE VIEW ${dbService['quoteIdentifier'](viewName)} AS ${def}`;
382
701
  const result = await databaseService.executeQuery(id, sql, database);
383
702
  return { success: true, sql, result };
384
703
  }
@@ -445,115 +764,6 @@ function convertToCSV(data: any[]): string {
445
764
  return [csvHeaders, ...csvRows].join('\n');
446
765
  }
447
766
 
448
- function generateTableSQL(tableName: string, columns: any[], tableInfo: any): string {
449
- let sql = `CREATE TABLE \`${tableName}\` (\n`;
450
-
451
- const primaryKeys = [] as Array<string>;
452
-
453
- columns.forEach((column, index) => {
454
- const columnDef = [] as Array<string>;
455
- columnDef.push(` \`${column.name}\``);
456
- columnDef.push(getColumnTypeDefinition(column));
457
-
458
- if (!column.nullable) {
459
- columnDef.push('NOT NULL');
460
- }
461
-
462
- if (column.defaultValue !== null && column.defaultValue !== '') {
463
- columnDef.push(`DEFAULT ${formatDefaultValue(column.defaultValue, column.type)}`);
464
- }
465
-
466
- if (column.isAutoIncrement) {
467
- columnDef.push('AUTO_INCREMENT');
468
- }
469
-
470
- if (column.comment) {
471
- columnDef.push(`COMMENT '${column.comment}'`);
472
- }
473
-
474
- sql += columnDef.join(' ');
475
-
476
- if (index < columns.length - 1) {
477
- sql += ',\n';
478
- }
479
-
480
- if (column.isPrimary) {
481
- primaryKeys.push(column.name);
482
- }
483
- });
484
-
485
- // 添加主键约束
486
- if (primaryKeys.length > 0) {
487
- sql += `,\n PRIMARY KEY (\`${primaryKeys.join('`, `')}\`)`;
488
- }
489
-
490
- sql += `\n) ENGINE=${tableInfo.engine} DEFAULT CHARSET=${tableInfo.charset} COLLATE=${tableInfo.collation}`;
491
-
492
- if (tableInfo.comment) {
493
- sql += ` COMMENT='${tableInfo.comment}'`;
494
- }
495
-
496
- sql += ';';
497
-
498
- return sql;
499
- }
500
-
501
- function generateAlterTableSQL(tableName: string, newColumns: any[], oldColumns: any[] = []): string[] {
502
- const statements = [] as Array<string>;
503
-
504
- // 简化实现:删除所有列,重新添加列
505
- // 注意:实际生产中需要更复杂的对比逻辑
506
- statements.push(`DROP TABLE IF EXISTS \`${tableName}_temp\`;`);
507
-
508
- let createTempSQL = `CREATE TABLE \`${tableName}_temp\` (\n`;
509
- const primaryKeys = [] as Array<string>;
510
-
511
- newColumns.forEach((column, index) => {
512
- const columnDef = [] as Array<string>;
513
- columnDef.push(` \`${column.name}\``);
514
- columnDef.push(getColumnTypeDefinition(column));
515
-
516
- if (!column.nullable) {
517
- columnDef.push('NOT NULL');
518
- }
519
-
520
- if (column.defaultValue !== null && column.defaultValue !== '') {
521
- columnDef.push(`DEFAULT ${formatDefaultValue(column.defaultValue, column.type)}`);
522
- }
523
-
524
- if (column.isAutoIncrement) {
525
- columnDef.push('AUTO_INCREMENT');
526
- }
527
-
528
- if (column.comment) {
529
- columnDef.push(`COMMENT '${column.comment}'`);
530
- }
531
-
532
- createTempSQL += columnDef.join(' ');
533
-
534
- if (index < newColumns.length - 1) {
535
- createTempSQL += ',\n';
536
- }
537
-
538
- if (column.isPrimary) {
539
- primaryKeys.push(column.name);
540
- }
541
- });
542
-
543
- if (primaryKeys.length > 0) {
544
- createTempSQL += `,\n PRIMARY KEY (\`${primaryKeys.join('`, `')}\`)`;
545
- }
546
-
547
- createTempSQL += '\n);';
548
- statements.push(createTempSQL);
549
-
550
- // 复制数据和重命名
551
- statements.push(`INSERT INTO \`${tableName}_temp\` SELECT * FROM \`${tableName}\`;`);
552
- statements.push(`DROP TABLE \`${tableName}\`;`);
553
- statements.push(`RENAME TABLE \`${tableName}_temp\` TO \`${tableName}\`;`);
554
-
555
- return statements;
556
- }
557
767
 
558
768
  function generateInsertSQL(dbType: string, tableName: string, data: any, dbService: any, columns?: any[]): string {
559
769
  // 如果提供了列信息,过滤掉自增字段
@@ -608,41 +818,6 @@ function generateDeleteSQL(dbType: string, tableName: string, where: any, dbServ
608
818
  return `DELETE FROM ${dbService.quoteIdentifier(tableName)} WHERE ${whereClause};`;
609
819
  }
610
820
 
611
- function getColumnTypeDefinition(column: any): string {
612
- let type = column.type.toUpperCase();
613
-
614
- if (needsLength(column.type) && column.length) {
615
- type += `(${column.length})`;
616
- }
617
-
618
- return type;
619
- }
620
-
621
- function needsLength(type: string): boolean {
622
- const typesNeedingLength = ['varchar', 'char', 'decimal', 'float', 'double'];
623
- return typesNeedingLength.some(t => type.startsWith(t));
624
- }
625
-
626
- function formatDefaultValue(value: string, type: string): string {
627
- if (value === '' || value === null || value === undefined) {
628
- return 'NULL';
629
- }
630
-
631
- if (isNumberType(type) || ['float', 'double', 'decimal'].includes(type)) {
632
- return value;
633
- }
634
-
635
- // 特殊关键字处理
636
- if (value.toString().toUpperCase() === 'CURRENT_TIMESTAMP' ||
637
- value.toString().toUpperCase() === 'NOW()' ||
638
- value.toString().toUpperCase() === 'CURRENT_DATE') {
639
- return value.toString().toUpperCase();
640
- }
641
-
642
- // 字符串类型需要加引号
643
- return `'${value.replace(/'/g, "''")}'`;
644
- }
645
-
646
821
  function formatValue(dbType: string, value: any): string {
647
822
  if (value === null || value === undefined) {
648
823
  return 'NULL';