fdb2 1.0.26 → 1.0.31

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 (80) hide show
  1. package/bin/fdb2.js +18 -2
  2. package/dist/public/.vite/manifest.json +4 -4
  3. package/dist/public/bootstrap.js +6 -5
  4. package/dist/public/explorer.css +513 -503
  5. package/dist/public/explorer.js +343 -218
  6. package/dist/public/favicon.ico +0 -0
  7. package/dist/public/index.css +100 -41
  8. package/dist/public/index.js +490 -263
  9. package/dist/public/layout.css +68 -68
  10. package/dist/public/layout.js +10 -10
  11. package/dist/public/rolldown-runtime.js +8 -1
  12. package/dist/public/vconsole.min.js +0 -26
  13. package/dist/public/vue.js +5238 -5192
  14. package/dist/server/cli/format.d.ts +6 -0
  15. package/dist/server/cli/format.d.ts.map +1 -0
  16. package/dist/server/cli/format.js +116 -0
  17. package/dist/server/cli/format.js.map +1 -0
  18. package/dist/server/cli/format.ts +104 -0
  19. package/dist/server/cli/main.d.ts +3 -0
  20. package/dist/server/cli/main.d.ts.map +1 -0
  21. package/dist/server/cli/main.js +710 -0
  22. package/dist/server/cli/main.js.map +1 -0
  23. package/dist/server/cli/main.ts +746 -0
  24. package/dist/server/cli/parse.d.ts +18 -0
  25. package/dist/server/cli/parse.d.ts.map +1 -0
  26. package/dist/server/cli/parse.js +150 -0
  27. package/dist/server/cli/parse.js.map +1 -0
  28. package/dist/server/cli/parse.ts +148 -0
  29. package/dist/server/core/column-spec.d.ts +29 -0
  30. package/dist/server/core/column-spec.d.ts.map +1 -0
  31. package/dist/server/core/column-spec.js +148 -0
  32. package/dist/server/core/column-spec.js.map +1 -0
  33. package/dist/server/core/column-spec.ts +147 -0
  34. package/dist/server/core/config.d.ts +34 -0
  35. package/dist/server/core/config.d.ts.map +1 -0
  36. package/dist/server/core/config.js +187 -0
  37. package/dist/server/core/config.js.map +1 -0
  38. package/dist/server/core/config.ts +165 -0
  39. package/dist/server/core/db-api.d.ts +169 -0
  40. package/dist/server/core/db-api.d.ts.map +1 -0
  41. package/dist/server/core/db-api.js +1078 -0
  42. package/dist/server/core/db-api.js.map +1 -0
  43. package/dist/server/core/db-api.ts +1181 -0
  44. package/dist/server/core/dialect.d.ts +44 -0
  45. package/dist/server/core/dialect.d.ts.map +1 -0
  46. package/dist/server/core/dialect.js +260 -0
  47. package/dist/server/core/dialect.js.map +1 -0
  48. package/dist/server/core/dialect.ts +293 -0
  49. package/dist/server/core/errors.d.ts +22 -0
  50. package/dist/server/core/errors.d.ts.map +1 -0
  51. package/dist/server/core/errors.js +61 -0
  52. package/dist/server/core/errors.js.map +1 -0
  53. package/dist/server/core/errors.ts +58 -0
  54. package/dist/server/core/filter-dsl.d.ts +39 -0
  55. package/dist/server/core/filter-dsl.d.ts.map +1 -0
  56. package/dist/server/core/filter-dsl.js +167 -0
  57. package/dist/server/core/filter-dsl.js.map +1 -0
  58. package/dist/server/core/filter-dsl.ts +180 -0
  59. package/dist/server/core/guard.d.ts +54 -0
  60. package/dist/server/core/guard.d.ts.map +1 -0
  61. package/dist/server/core/guard.js +207 -0
  62. package/dist/server/core/guard.js.map +1 -0
  63. package/dist/server/core/guard.ts +205 -0
  64. package/dist/server/core/query-builder.d.ts +72 -0
  65. package/dist/server/core/query-builder.d.ts.map +1 -0
  66. package/dist/server/core/query-builder.js +478 -0
  67. package/dist/server/core/query-builder.js.map +1 -0
  68. package/dist/server/core/query-builder.ts +502 -0
  69. package/dist/server/service/connection.service.d.ts.map +1 -1
  70. package/dist/server/service/connection.service.js +7 -3
  71. package/dist/server/service/connection.service.js.map +1 -1
  72. package/dist/server/service/connection.service.ts +5 -3
  73. package/dist/server/utils/data-dir.d.ts.map +1 -1
  74. package/dist/server/utils/data-dir.js +2 -0
  75. package/dist/server/utils/data-dir.js.map +1 -1
  76. package/dist/server/utils/data-dir.ts +2 -0
  77. package/electron/updater.js +12 -66
  78. package/package.json +22 -22
  79. package/public/favicon.ico +0 -0
  80. package/public/favicon.png +0 -0
@@ -0,0 +1,1078 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DbToolApi = void 0;
37
+ /**
38
+ * DbToolApi —— CLI 的显式接口层(单一事实来源)
39
+ * 所有命令在此实现,直接调用服务层(ConnectionService + 方言服务),
40
+ * 统一经过:列名白名单 → 参数化编译 → 护栏(只读/dry-run/确认/阈值)→ 审计。
41
+ */
42
+ const connection_service_1 = require("../service/connection.service");
43
+ const dialect_1 = require("./dialect");
44
+ const filter_dsl_1 = require("./filter-dsl");
45
+ const column_spec_1 = require("./column-spec");
46
+ const query_builder_1 = require("./query-builder");
47
+ const errors_1 = require("./errors");
48
+ const guard_1 = require("./guard");
49
+ const config_1 = require("./config");
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ class DbToolApi {
53
+ connectionService;
54
+ constructor() {
55
+ this.connectionService = new connection_service_1.ConnectionService();
56
+ }
57
+ // ============ 工具 ============
58
+ async allConnections() {
59
+ return this.connectionService.getAllConnections();
60
+ }
61
+ async resolveConnection(ref) {
62
+ const list = await this.allConnections();
63
+ const all = list.filter((c) => c.enabled !== false);
64
+ let target;
65
+ if (ref) {
66
+ target = list.find((c) => c.id === ref || c.name === ref);
67
+ if (!target) {
68
+ target = list.find((c) => c.name && c.name.toLowerCase() === String(ref).toLowerCase());
69
+ }
70
+ if (!target) {
71
+ const names = list.map((c) => c.name || c.id);
72
+ throw new errors_1.AppError('CONN_NOT_FOUND', `连接 ${ref} 不存在`, {
73
+ hint: names.length ? `可用连接: ${names.join(', ')}` : '还没有任何连接,请先执行 fdb2 conn add',
74
+ details: { available: names }
75
+ });
76
+ }
77
+ }
78
+ else {
79
+ const defaultId = (0, config_1.loadConfig)().defaultConnectionId;
80
+ if (defaultId) {
81
+ target = all.find((c) => c.id === defaultId) || list.find((c) => c.id === defaultId);
82
+ }
83
+ if (!target) {
84
+ if (all.length === 1)
85
+ target = all[0];
86
+ }
87
+ if (!target) {
88
+ const names = all.map((c) => c.name || c.id);
89
+ throw new errors_1.AppError('NO_DEFAULT_CONN', '未指定连接,且没有默认连接', {
90
+ hint: names.length ? `可用连接: ${names.join(', ')};请用 --conn 指定或用 fdb2 conn use <name> 设置默认` : '还没有任何连接,请先执行 fdb2 conn add'
91
+ });
92
+ }
93
+ }
94
+ return target;
95
+ }
96
+ async getDataSource(conn, database) {
97
+ return this.connectionService.getActiveConnection(conn.id, database || conn.database);
98
+ }
99
+ /** 动态加载方言 service(避免原生驱动在导入期全量加载) */
100
+ loadDialectService(type) {
101
+ const info = (0, dialect_1.getDialectInfo)(type);
102
+ const file = `../service/database/${info.serviceFile}.service`;
103
+ try {
104
+ const mod = require(file);
105
+ const Cls = mod[info.serviceClass];
106
+ if (!Cls)
107
+ throw new Error(`未找到类 ${info.serviceClass}`);
108
+ return new Cls();
109
+ }
110
+ catch (e) {
111
+ throw new errors_1.AppError('DRIVER_UNAVAILABLE', `无法加载 ${info.label} 驱动: ${e?.message || e}`, {
112
+ hint: `数据库类型 ${type} 对应的驱动可能未安装(fdb2 安装时该可选依赖安装失败),请尝试重新安装或改用其它数据库类型`
113
+ });
114
+ }
115
+ }
116
+ sanitizeConnection(conn) {
117
+ const { password, ...rest } = conn;
118
+ return { ...rest, password: password ? '***' : null, hasPassword: !!password };
119
+ }
120
+ async ensureTable(conn, database, table, service, dataSource) {
121
+ const tables = await service.getTables(dataSource, database);
122
+ if (!tables || !tables.some((t) => t.name === table)) {
123
+ const names = (tables || []).map((t) => t.name).slice(0, 50);
124
+ throw new errors_1.AppError('TABLE_NOT_FOUND', `表 ${table} 不存在于数据库 ${database}`, {
125
+ hint: names.length ? `可用表: ${names.join(', ')}` : '该数据库没有表',
126
+ details: { availableTables: names }
127
+ });
128
+ }
129
+ }
130
+ async getColumns(conn, database, table) {
131
+ const service = this.loadDialectService(conn.type);
132
+ const ds = await this.getDataSource(conn, database);
133
+ await this.ensureTable(conn, database, table, service, ds);
134
+ const cols = await service.getColumns(ds, database, table);
135
+ return (cols || []).map((c) => ({
136
+ name: c.name,
137
+ type: c.type,
138
+ isAutoIncrement: !!c.isAutoIncrement,
139
+ isPrimary: !!c.isPrimary,
140
+ nullable: c.nullable !== false
141
+ }));
142
+ }
143
+ // ============ 连接管理 ============
144
+ async listConnections() {
145
+ const list = await this.allConnections();
146
+ const defaultId = (0, config_1.loadConfig)().defaultConnectionId;
147
+ return {
148
+ defaultConnectionId: defaultId,
149
+ connections: list.map((c) => ({ ...this.sanitizeConnection(c), isDefault: c.id === defaultId }))
150
+ };
151
+ }
152
+ async getConnectionInfo(ref) {
153
+ const conn = await this.resolveConnection(ref);
154
+ return this.sanitizeConnection(conn);
155
+ }
156
+ async addConnection(input) {
157
+ const type = (0, dialect_1.normalizeDialectType)(input.type);
158
+ if (!type || !(0, dialect_1.isDialectSupported)(input.type)) {
159
+ throw new errors_1.AppError('PARAM_ERROR', `不支持的数据库类型: ${input.type}`, {
160
+ hint: `支持: ${(0, dialect_1.listDialects)().map((d) => `${d.type}(${d.label})`).join(', ')}`
161
+ });
162
+ }
163
+ if (!input.name || !String(input.name).trim())
164
+ throw new errors_1.AppError('PARAM_ERROR', '缺少连接名称 --name');
165
+ const info = (0, dialect_1.getDialectInfo)(type);
166
+ const conn = {
167
+ name: String(input.name).trim(),
168
+ type,
169
+ host: input.host || (type === 'sqlite' ? null : 'localhost'),
170
+ port: input.port ?? info.defaultPort,
171
+ database: input.database,
172
+ username: input.username || null,
173
+ password: input.password || null,
174
+ options: input.options || {}
175
+ };
176
+ if (!conn.database) {
177
+ throw new errors_1.AppError('PARAM_ERROR', '缺少数据库名/文件路径 --database');
178
+ }
179
+ const saved = await this.connectionService.addConnection(conn);
180
+ return this.sanitizeConnection(saved);
181
+ }
182
+ async updateConnection(ref, updates) {
183
+ const conn = await this.resolveConnection(ref);
184
+ const patch = {};
185
+ const allowed = ['name', 'host', 'port', 'database', 'username', 'password', 'enabled', 'options'];
186
+ for (const k of Object.keys(updates)) {
187
+ if (allowed.includes(k))
188
+ patch[k] = updates[k];
189
+ }
190
+ if (updates.type) {
191
+ const t = (0, dialect_1.normalizeDialectType)(updates.type);
192
+ if (!t)
193
+ throw new errors_1.AppError('PARAM_ERROR', `不支持的数据库类型: ${updates.type}`);
194
+ patch.type = t;
195
+ }
196
+ if (Object.keys(patch).length === 0)
197
+ throw new errors_1.AppError('PARAM_ERROR', '没有可更新的字段');
198
+ const saved = await this.connectionService.updateConnection(conn.id, patch);
199
+ return this.sanitizeConnection(saved);
200
+ }
201
+ async deleteConnection(ref) {
202
+ const conn = await this.resolveConnection(ref);
203
+ await this.connectionService.deleteConnection(conn.id);
204
+ const cfg = (0, config_1.loadConfig)();
205
+ if (cfg.defaultConnectionId === conn.id) {
206
+ (0, config_1.updateConfig)({ defaultConnectionId: null });
207
+ }
208
+ return { deleted: conn.name || conn.id };
209
+ }
210
+ async testConnectionByRef(ref) {
211
+ const conn = await this.resolveConnection(ref);
212
+ const ok = await this.connectionService.testConnection(conn);
213
+ (0, guard_1.audit)({ action: 'conn.test', conn: conn.name, ok, error: ok ? null : '连接测试失败' });
214
+ return { name: conn.name, ok };
215
+ }
216
+ async setDefaultConnection(ref) {
217
+ const conn = await this.resolveConnection(ref);
218
+ (0, config_1.updateConfig)({ defaultConnectionId: conn.id });
219
+ return { defaultConnection: conn.name || conn.id };
220
+ }
221
+ async currentConnection() {
222
+ const conn = await this.resolveConnection(null);
223
+ return { ...this.sanitizeConnection(conn), isDefault: true };
224
+ }
225
+ async types() {
226
+ const list = (0, dialect_1.listDialects)();
227
+ return { count: list.length, types: list };
228
+ }
229
+ // ============ 数据库 ============
230
+ async listDatabases(ref) {
231
+ const conn = await this.resolveConnection(ref);
232
+ const service = this.loadDialectService(conn.type);
233
+ const ds = await this.getDataSource(conn);
234
+ const dbs = await service.getDatabases(ds);
235
+ return { conn: conn.name, databases: dbs || [] };
236
+ }
237
+ async databaseInfo(ref, database) {
238
+ const conn = await this.resolveConnection(ref);
239
+ const service = this.loadDialectService(conn.type);
240
+ const ds = await this.getDataSource(conn, database);
241
+ const info = await service.getDatabaseInfo(ds, database);
242
+ return { conn: conn.name, database, info };
243
+ }
244
+ async createDatabaseCmd(ref, database, charset, opts) {
245
+ const conn = await this.resolveConnection(ref);
246
+ const service = this.loadDialectService(conn.type);
247
+ const ds = await this.getDataSource(conn);
248
+ const label = `CREATE DATABASE ${database}`;
249
+ const g = (0, guard_1.gateWrite)({ op: 'db.create', sql: label, params: [], destructive: false, dryRun: opts.dryRun, confirm: opts.confirm, write: opts.write, force: opts.force });
250
+ if (g.mode === 'dryrun') {
251
+ return { dryRun: true, sql: label, token: g.token, estimatedRows: 0 };
252
+ }
253
+ await service.createDatabase(ds, database, charset ? { charset } : undefined);
254
+ (0, guard_1.audit)({ action: 'db.create', conn: conn.name, database, sql: label, ok: true });
255
+ return { conn: conn.name, created: database, sql: label };
256
+ }
257
+ async dropDatabaseCmd(ref, database, opts) {
258
+ const conn = await this.resolveConnection(ref);
259
+ const protectedList = (0, config_1.getConfig)().protectedDatabases || [];
260
+ if (protectedList.map((s) => s.toLowerCase()).includes(String(database).toLowerCase())) {
261
+ throw new errors_1.AppError('WRITE_NOT_ALLOWED', `${database} 是系统库/受保护数据库,禁止删除`, {
262
+ hint: `受保护名单: ${protectedList.join(', ')}`
263
+ });
264
+ }
265
+ const service = this.loadDialectService(conn.type);
266
+ const ds = await this.getDataSource(conn, database);
267
+ const sql = `DROP DATABASE ${(0, dialect_1.quoteIdentifier)(conn.type, database)}`;
268
+ const g = (0, guard_1.gateWrite)({ op: 'db.drop', sql, params: [], destructive: true, ...opts });
269
+ if (g.mode === 'dryrun') {
270
+ return { dryRun: true, sql, token: g.token, estimatedRows: null };
271
+ }
272
+ await service.dropDatabase(ds, database);
273
+ (0, guard_1.audit)({ action: 'db.drop', conn: conn.name, database, sql, ok: true });
274
+ return { dropped: database, sql };
275
+ }
276
+ // ============ 表 ============
277
+ async listTables(ref, database, pattern) {
278
+ const conn = await this.resolveConnection(ref);
279
+ const service = this.loadDialectService(conn.type);
280
+ const ds = await this.getDataSource(conn, database);
281
+ let tables = await service.getTables(ds, database);
282
+ if (pattern) {
283
+ const re = new RegExp('^' + String(pattern).replace(/%/g, '.*').replace(/\*/g, '.*') + '$', 'i');
284
+ tables = (tables || []).filter((t) => re.test(t.name));
285
+ }
286
+ return {
287
+ conn: conn.name,
288
+ database,
289
+ tableCount: (tables || []).length,
290
+ tables: (tables || []).map((t) => ({
291
+ name: t.name,
292
+ type: t.type || null,
293
+ engine: t.engine || null,
294
+ rowCount: t.rowCount || 0,
295
+ size: t.dataSize || 0,
296
+ comment: t.comment || null
297
+ }))
298
+ };
299
+ }
300
+ async tableInfo(ref, database, table) {
301
+ const conn = await this.resolveConnection(ref);
302
+ const service = this.loadDialectService(conn.type);
303
+ const ds = await this.getDataSource(conn, database);
304
+ await this.ensureTable(conn, database, table, service, ds);
305
+ const info = await service.getTableInfo(ds, database, table);
306
+ return {
307
+ conn: conn.name,
308
+ database,
309
+ table: {
310
+ name: info.name,
311
+ engine: info.engine,
312
+ rowCount: info.rowCount,
313
+ size: info.dataSize,
314
+ comment: info.comment,
315
+ columns: info.columns || [],
316
+ indexes: info.indexes || [],
317
+ foreignKeys: info.foreignKeys || []
318
+ }
319
+ };
320
+ }
321
+ async createTableCmd(ref, database, table, specs, opts) {
322
+ const conn = await this.resolveConnection(ref);
323
+ if (!specs || specs.length === 0)
324
+ throw new errors_1.AppError('PARAM_ERROR', '建表至少需要一个 --column');
325
+ if ((0, dialect_1.getDialectInfo)(conn.type).kind === 'mongodb') {
326
+ throw new errors_1.AppError('UNSUPPORTED', 'MongoDB 文档模型暂不支持该命令(experimental)', { hint: 'MongoDB 为实验性支持,请使用桌面端管理集合结构' });
327
+ }
328
+ const columns = specs.map((s) => (0, column_spec_1.parseColumnSpec)(s));
329
+ const kind = (0, dialect_1.getDialectInfo)(conn.type).kind;
330
+ const q = (id) => (0, dialect_1.quoteIdentifier)(conn.type, id);
331
+ const tableName = q(table);
332
+ const lines = [];
333
+ const pkCols = [];
334
+ for (const c of columns) {
335
+ const parts = [q(c.name), typeFor(kind, c)];
336
+ if (c.isAutoIncrement) {
337
+ if (kind === 'mysql') {
338
+ parts.push('AUTO_INCREMENT');
339
+ }
340
+ else if (kind === 'postgres' || kind === 'cockroachdb') {
341
+ parts[1] = c.baseType.includes('big') ? 'BIGSERIAL' : c.baseType.includes('small') ? 'SMALLSERIAL' : 'SERIAL';
342
+ }
343
+ else if (kind === 'sqlite') {
344
+ parts[1] = 'INTEGER PRIMARY KEY AUTOINCREMENT';
345
+ pkCols.push(c.name);
346
+ }
347
+ else if (kind === 'mssql') {
348
+ parts.push('IDENTITY(1,1)');
349
+ }
350
+ else if (kind === 'oracle') {
351
+ parts.push('GENERATED BY DEFAULT AS IDENTITY');
352
+ }
353
+ else if (kind === 'sap') {
354
+ parts.push('GENERATED BY DEFAULT AS IDENTITY');
355
+ }
356
+ }
357
+ if (!c.nullable && !(kind === 'sqlite' && c.isAutoIncrement))
358
+ parts.push('NOT NULL');
359
+ if (c.hasDefault) {
360
+ parts.push(`DEFAULT ${formatDdlDefault(c.defaultValue)}`);
361
+ }
362
+ if (c.unique) {
363
+ parts.push('UNIQUE');
364
+ }
365
+ if (c.comment && (kind === 'mysql' || kind === 'mssql')) {
366
+ parts.push(`COMMENT '${String(c.comment).replace(/'/g, "''")}'`);
367
+ }
368
+ if (c.isPrimary)
369
+ pkCols.push(c.name);
370
+ lines.push(parts.join(' '));
371
+ }
372
+ if (pkCols.length > 1) {
373
+ lines.push(`PRIMARY KEY (${pkCols.map((c) => q(c)).join(', ')})`);
374
+ }
375
+ let sql = `CREATE TABLE ${tableName} (\n ${lines.join(',\n ')}\n)`;
376
+ if (kind === 'mysql') {
377
+ const suffix = [];
378
+ if (opts.engine)
379
+ suffix.push(`ENGINE=${opts.engine}`);
380
+ if (opts.charset)
381
+ suffix.push(`DEFAULT CHARSET=${opts.charset}`);
382
+ if (suffix.length)
383
+ sql += ` ${suffix.join(' ')}`;
384
+ }
385
+ if (opts.comment && kind === 'mysql') {
386
+ sql += ` COMMENT='${String(opts.comment).replace(/'/g, "''")}'`;
387
+ }
388
+ if (kind === 'sqlite') {
389
+ sql = sql.replace(/\)\s*$/, ')');
390
+ }
391
+ sql += ';';
392
+ const g = (0, guard_1.gateWrite)({ op: 'table.create', sql, params: [], destructive: false, yes: opts.yes, dryRun: opts.dryRun, confirm: opts.confirm, write: opts.write });
393
+ if (g.mode === 'dryrun') {
394
+ return { dryRun: true, sql, token: g.token, estimatedRows: 0 };
395
+ }
396
+ const service = this.loadDialectService(conn.type);
397
+ const ds = await this.getDataSource(conn, database);
398
+ await ds.query(sql);
399
+ (0, guard_1.audit)({ action: 'table.create', conn: conn.name, database, table, sql, ok: true });
400
+ return { created: table, sql };
401
+ }
402
+ async alterTableCmd(ref, database, table, opts) {
403
+ const conn = await this.resolveConnection(ref);
404
+ const type = conn.type;
405
+ const service = this.loadDialectService(type);
406
+ const ds = await this.getDataSource(conn, database);
407
+ await this.ensureTable(conn, database, table, service, ds);
408
+ const kind = (0, dialect_1.getDialectInfo)(type).kind;
409
+ const statements = [];
410
+ const q = (id) => (0, dialect_1.quoteIdentifier)(type, id);
411
+ const colDef = (c) => {
412
+ const parts = [q(c.name), c.type];
413
+ if (c.isAutoIncrement && kind === 'mysql')
414
+ parts.push('AUTO_INCREMENT');
415
+ if (!c.nullable)
416
+ parts.push('NOT NULL');
417
+ if (c.hasDefault)
418
+ parts.push(`DEFAULT ${formatDdlDefault(c.defaultValue)}`);
419
+ if (c.comment)
420
+ parts.push(`COMMENT '${String(c.comment).replace(/'/g, "''")}'`);
421
+ return parts.join(' ');
422
+ };
423
+ for (const s of opts.addColumn || []) {
424
+ const c = (0, column_spec_1.parseColumnSpec)(s);
425
+ statements.push(`ALTER TABLE ${q(table)} ADD COLUMN ${colDef(c)}`);
426
+ }
427
+ for (const s of opts.dropColumn || []) {
428
+ statements.push(`ALTER TABLE ${q(table)} DROP COLUMN ${q(s)}`);
429
+ }
430
+ for (const s of opts.modifyColumn || []) {
431
+ const c = (0, column_spec_1.parseColumnSpec)(s);
432
+ if (kind === 'sqlite') {
433
+ throw new errors_1.AppError('UNSUPPORTED', 'SQLite 不支持直接修改列,请用 add-column/drop-column 或重建表');
434
+ }
435
+ const kw = kind === 'mysql' ? 'MODIFY COLUMN' : kind === 'mssql' ? 'ALTER COLUMN' : 'ALTER COLUMN';
436
+ statements.push(`ALTER TABLE ${q(table)} ${kw} ${colDef(c)}`);
437
+ }
438
+ if (opts.renameTo) {
439
+ statements.push(`ALTER TABLE ${q(table)} RENAME TO ${q(opts.renameTo)}`);
440
+ }
441
+ if (statements.length === 0)
442
+ throw new errors_1.AppError('PARAM_ERROR', 'alter 需要 --add-column/--drop-column/--modify-column/--rename-to 之一');
443
+ const label = statements.join('; ');
444
+ const g = (0, guard_1.gateWrite)({ op: 'table.alter', sql: label, params: [], destructive: true, yes: opts.yes, dryRun: opts.dryRun, confirm: opts.confirm, write: opts.write });
445
+ if (g.mode === 'dryrun') {
446
+ return { dryRun: true, sql: statements, token: g.token, estimatedRows: null };
447
+ }
448
+ for (const s of statements) {
449
+ await ds.query(s);
450
+ }
451
+ (0, guard_1.audit)({ action: 'table.alter', conn: conn.name, database, table, sql: label, ok: true });
452
+ return { altered: table, sql: statements };
453
+ }
454
+ async dropTableCmd(ref, database, table, opts) {
455
+ const conn = await this.resolveConnection(ref);
456
+ const service = this.loadDialectService(conn.type);
457
+ const ds = await this.getDataSource(conn, database);
458
+ await this.ensureTable(conn, database, table, service, ds);
459
+ const action = opts.truncate ? 'TRUNCATE TABLE' : 'DROP TABLE';
460
+ const sql = `${action} ${(0, dialect_1.quoteIdentifier)(conn.type, table)}`;
461
+ const g = (0, guard_1.gateWrite)({ op: opts.truncate ? 'table.truncate' : 'table.drop', sql, params: [], destructive: true, ...opts });
462
+ if (g.mode === 'dryrun') {
463
+ return { dryRun: true, sql, token: g.token, estimatedRows: null };
464
+ }
465
+ await ds.query(sql);
466
+ (0, guard_1.audit)({ action: opts.truncate ? 'table.truncate' : 'table.drop', conn: conn.name, database, table, sql, ok: true });
467
+ return { [opts.truncate ? 'truncated' : 'dropped']: table, sql };
468
+ }
469
+ // ============ 索引 ============
470
+ async listIndexes(ref, database, table) {
471
+ const conn = await this.resolveConnection(ref);
472
+ const service = this.loadDialectService(conn.type);
473
+ const ds = await this.getDataSource(conn, database);
474
+ await this.ensureTable(conn, database, table, service, ds);
475
+ const indexes = await service.getIndexes(ds, database, table);
476
+ return { conn: conn.name, database, table, indexes: indexes || [] };
477
+ }
478
+ async createIndexCmd(ref, database, table, name, columns, opts) {
479
+ const conn = await this.resolveConnection(ref);
480
+ if (!name || !columns || columns.length === 0)
481
+ throw new errors_1.AppError('PARAM_ERROR', 'index create 需要 --name 和 --columns c1,c2');
482
+ const cols = await this.getColumns(conn, database, table);
483
+ const b = new query_builder_1.SqlBuilder(conn.type, cols);
484
+ for (const c of columns)
485
+ b.checkField(c);
486
+ const sql = `CREATE ${opts.unique ? 'UNIQUE ' : ''}INDEX ${(0, dialect_1.quoteIdentifier)(conn.type, name)} ON ${(0, dialect_1.quoteIdentifier)(conn.type, table)} (${columns.map((c) => (0, dialect_1.quoteIdentifier)(conn.type, c)).join(', ')})`;
487
+ const g = (0, guard_1.gateWrite)({ op: 'index.create', sql, params: [], destructive: false, yes: opts.yes, dryRun: opts.dryRun, confirm: opts.confirm, write: opts.write });
488
+ if (g.mode === 'dryrun')
489
+ return { dryRun: true, sql, token: g.token, estimatedRows: 0 };
490
+ const ds = await this.getDataSource(conn, database);
491
+ await ds.query(sql);
492
+ (0, guard_1.audit)({ action: 'index.create', conn: conn.name, database, table, sql, ok: true });
493
+ return { created: name, sql };
494
+ }
495
+ async dropIndexCmd(ref, database, table, name, opts) {
496
+ const conn = await this.resolveConnection(ref);
497
+ const kind = (0, dialect_1.getDialectInfo)(conn.type).kind;
498
+ const sql = kind === 'mysql'
499
+ ? `DROP INDEX ${(0, dialect_1.quoteIdentifier)(conn.type, name)} ON ${(0, dialect_1.quoteIdentifier)(conn.type, table)}`
500
+ : `DROP INDEX ${(0, dialect_1.quoteIdentifier)(conn.type, name)}`;
501
+ const g = (0, guard_1.gateWrite)({ op: 'index.drop', sql, params: [], destructive: true, yes: opts.yes, dryRun: opts.dryRun, confirm: opts.confirm, write: opts.write });
502
+ if (g.mode === 'dryrun')
503
+ return { dryRun: true, sql, token: g.token, estimatedRows: null };
504
+ const ds = await this.getDataSource(conn, database);
505
+ await ds.query(sql);
506
+ (0, guard_1.audit)({ action: 'index.drop', conn: conn.name, database, table, sql, ok: true });
507
+ return { dropped: name, sql };
508
+ }
509
+ // ============ 行数据 ============
510
+ async whereTree(filters, filterJson) {
511
+ let node = (0, filter_dsl_1.parseFilters)(filters || []);
512
+ if (filterJson) {
513
+ const j = (0, filter_dsl_1.parseFilterJson)(filterJson);
514
+ node = { type: 'and', children: [node, j] };
515
+ }
516
+ return node;
517
+ }
518
+ async rowsList(ref, database, table, opts) {
519
+ const conn = await this.resolveConnection(ref);
520
+ const maxRows = (0, config_1.getConfig)().maxRows ?? 1000;
521
+ const columns = await this.getColumns(conn, database, table);
522
+ const where = await this.whereTree(opts.filters, opts.filterJson);
523
+ const sorts = (0, query_builder_1.parseSorts)(opts.sorts || []);
524
+ const requested = opts.limit ?? 100;
525
+ const limit = Math.min(Math.max(1, Math.floor(requested)), maxRows);
526
+ const offset = Math.floor(opts.offset || 0);
527
+ const q = (0, query_builder_1.buildSelect)(conn.type, table, columns, { select: opts.select, where, sorts, limit, offset });
528
+ const ds = await this.getDataSource(conn, database);
529
+ const rows = await ds.query(q.sql, q.params);
530
+ let total = null;
531
+ if (opts.count) {
532
+ const cq = (0, query_builder_1.buildCount)(conn.type, table, columns, where);
533
+ const cr = await ds.query(cq.sql, cq.params);
534
+ total = cr && cr[0] ? Number(cr[0].total || 0) : rows.length;
535
+ }
536
+ (0, guard_1.audit)({
537
+ action: 'rows.list',
538
+ conn: conn.name,
539
+ database,
540
+ table,
541
+ sql: q.sql,
542
+ params: q.params.length ? q.params : null,
543
+ rows: rows.length,
544
+ ok: true
545
+ });
546
+ return {
547
+ conn: conn.name,
548
+ database,
549
+ table,
550
+ data: rows,
551
+ meta: {
552
+ rowCount: rows.length,
553
+ total,
554
+ truncated: requested > limit,
555
+ limit,
556
+ offset
557
+ }
558
+ };
559
+ }
560
+ async rowsCount(ref, database, table, opts) {
561
+ const conn = await this.resolveConnection(ref);
562
+ const columns = await this.getColumns(conn, database, table);
563
+ const where = await this.whereTree(opts.filters, opts.filterJson);
564
+ const q = (0, query_builder_1.buildCount)(conn.type, table, columns, where);
565
+ const ds = await this.getDataSource(conn, database);
566
+ const r = await ds.query(q.sql, q.params);
567
+ const total = r && r[0] ? Number(r[0].total || 0) : 0;
568
+ return { conn: conn.name, database, table, count: total, sql: q.sql };
569
+ }
570
+ async rowsInsert(ref, database, table, data, opts) {
571
+ const conn = await this.resolveConnection(ref);
572
+ if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
573
+ throw new errors_1.AppError('PARAM_ERROR', 'insert 需要 --data <json>');
574
+ }
575
+ const columns = await this.getColumns(conn, database, table);
576
+ const q = (0, query_builder_1.buildInsert)(conn.type, table, columns, data);
577
+ const g = (0, guard_1.gateWrite)({ op: 'rows.insert', sql: q.sql, params: q.params, estimatedRows: 1, dryRun: opts.dryRun, confirm: opts.confirm, write: opts.write });
578
+ if (g.mode === 'dryrun') {
579
+ return { dryRun: true, sql: q.sql, params: q.params, estimatedRows: 1, token: g.token };
580
+ }
581
+ const ds = await this.getDataSource(conn, database);
582
+ const result = await ds.query(q.sql, q.params);
583
+ (0, guard_1.audit)({ action: 'rows.insert', conn: conn.name, database, table, sql: q.sql, params: q.params.length ? q.params : null, rows: 1, ok: true });
584
+ return { inserted: 1, sql: q.sql, result };
585
+ }
586
+ async rowsUpdate(ref, database, table, setData, opts) {
587
+ const conn = await this.resolveConnection(ref);
588
+ if (!setData || Object.keys(setData).length === 0)
589
+ throw new errors_1.AppError('PARAM_ERROR', 'update 需要 --set k=v');
590
+ if (!opts.filters || opts.filters.length === 0) {
591
+ throw new errors_1.AppError('PARAM_ERROR', 'update 必须带过滤条件 --filter', { hint: '为防误操作,禁止无条件更新' });
592
+ }
593
+ const columns = await this.getColumns(conn, database, table);
594
+ const where = await this.whereTree(opts.filters, opts.filterJson);
595
+ const q = (0, query_builder_1.buildUpdate)(conn.type, table, columns, setData, where);
596
+ const ds = await this.getDataSource(conn, database);
597
+ const cq = (0, query_builder_1.buildCount)(conn.type, table, columns, where);
598
+ const cr = await ds.query(cq.sql, cq.params);
599
+ const estimated = cr && cr[0] ? Number(cr[0].total || 0) : 0;
600
+ const g = (0, guard_1.gateWrite)({ op: 'rows.update', sql: q.sql, params: q.params, estimatedRows: estimated, dryRun: opts.dryRun, confirm: opts.confirm, write: opts.write, force: opts.force });
601
+ if (g.mode === 'dryrun') {
602
+ return { dryRun: true, sql: q.sql, params: q.params, estimatedRows: estimated, token: g.token };
603
+ }
604
+ const result = await ds.query(q.sql, q.params);
605
+ (0, guard_1.audit)({ action: 'rows.update', conn: conn.name, database, table, sql: q.sql, params: q.params.length ? q.params : null, rows: estimated, ok: true });
606
+ return { updated: estimated, sql: q.sql, result };
607
+ }
608
+ async rowsDelete(ref, database, table, opts) {
609
+ const conn = await this.resolveConnection(ref);
610
+ if (!opts.filters || opts.filters.length === 0) {
611
+ throw new errors_1.AppError('PARAM_ERROR', 'delete 必须带过滤条件 --filter', { hint: '为防误操作,禁止无条件删除' });
612
+ }
613
+ const columns = await this.getColumns(conn, database, table);
614
+ const where = await this.whereTree(opts.filters, opts.filterJson);
615
+ const q = (0, query_builder_1.buildDelete)(conn.type, table, columns, where);
616
+ const ds = await this.getDataSource(conn, database);
617
+ const cq = (0, query_builder_1.buildCount)(conn.type, table, columns, where);
618
+ const cr = await ds.query(cq.sql, cq.params);
619
+ const estimated = cr && cr[0] ? Number(cr[0].total || 0) : 0;
620
+ const g = (0, guard_1.gateWrite)({ op: 'rows.delete', sql: q.sql, params: q.params, estimatedRows: estimated, dryRun: opts.dryRun, confirm: opts.confirm, write: opts.write, force: opts.force });
621
+ if (g.mode === 'dryrun') {
622
+ return { dryRun: true, sql: q.sql, params: q.params, estimatedRows: estimated, token: g.token };
623
+ }
624
+ const result = await ds.query(q.sql, q.params);
625
+ (0, guard_1.audit)({ action: 'rows.delete', conn: conn.name, database, table, sql: q.sql, params: q.params.length ? q.params : null, rows: estimated, ok: true });
626
+ return { deleted: estimated, sql: q.sql, result };
627
+ }
628
+ // ============ 视图 / 存储过程 ============
629
+ async listViews(ref, database) {
630
+ const conn = await this.resolveConnection(ref);
631
+ const service = this.loadDialectService(conn.type);
632
+ const ds = await this.getDataSource(conn, database);
633
+ const views = await service.getViews(ds, database);
634
+ return { conn: conn.name, database, views: views || [] };
635
+ }
636
+ async getViewDefinitionCmd(ref, database, name) {
637
+ const conn = await this.resolveConnection(ref);
638
+ const service = this.loadDialectService(conn.type);
639
+ const ds = await this.getDataSource(conn, database);
640
+ const definition = await service.getViewDefinition(ds, database, name);
641
+ return { conn: conn.name, database, view: name, definition };
642
+ }
643
+ async createViewCmd(ref, database, name, definition, opts) {
644
+ const conn = await this.resolveConnection(ref);
645
+ if (!definition)
646
+ throw new errors_1.AppError('PARAM_ERROR', 'create view 需要 --as "<SELECT ...>"');
647
+ const sql = `CREATE VIEW ${(0, dialect_1.quoteIdentifier)(conn.type, name)} AS ${definition}`;
648
+ const g = (0, guard_1.gateWrite)({ op: 'view.create', sql, params: [], destructive: false, ...opts });
649
+ if (g.mode === 'dryrun')
650
+ return { dryRun: true, sql, token: g.token, estimatedRows: 0 };
651
+ const ds = await this.getDataSource(conn, database);
652
+ await ds.query(sql);
653
+ (0, guard_1.audit)({ action: 'view.create', conn: conn.name, database, sql, ok: true });
654
+ return { created: name, sql };
655
+ }
656
+ async dropViewCmd(ref, database, name, opts) {
657
+ const conn = await this.resolveConnection(ref);
658
+ const sql = `DROP VIEW ${(0, dialect_1.quoteIdentifier)(conn.type, name)}`;
659
+ const g = (0, guard_1.gateWrite)({ op: 'view.drop', sql, params: [], destructive: true, ...opts });
660
+ if (g.mode === 'dryrun')
661
+ return { dryRun: true, sql, token: g.token, estimatedRows: null };
662
+ const ds = await this.getDataSource(conn, database);
663
+ await ds.query(sql);
664
+ (0, guard_1.audit)({ action: 'view.drop', conn: conn.name, database, sql, ok: true });
665
+ return { dropped: name, sql };
666
+ }
667
+ async listProcedures(ref, database) {
668
+ const conn = await this.resolveConnection(ref);
669
+ const service = this.loadDialectService(conn.type);
670
+ const ds = await this.getDataSource(conn, database);
671
+ const procedures = await service.getProcedures(ds, database);
672
+ return { conn: conn.name, database, procedures: procedures || [] };
673
+ }
674
+ async getProcedureDefinitionCmd(ref, database, name) {
675
+ const conn = await this.resolveConnection(ref);
676
+ const service = this.loadDialectService(conn.type);
677
+ const ds = await this.getDataSource(conn, database);
678
+ const definition = await service.getProcedureDefinition(ds, database, name);
679
+ return { conn: conn.name, database, procedure: name, definition };
680
+ }
681
+ async dropProcedureCmd(ref, database, name, opts) {
682
+ const conn = await this.resolveConnection(ref);
683
+ const sql = `DROP PROCEDURE ${(0, dialect_1.quoteIdentifier)(conn.type, name)}`;
684
+ const g = (0, guard_1.gateWrite)({ op: 'proc.drop', sql, params: [], destructive: true, ...opts });
685
+ if (g.mode === 'dryrun')
686
+ return { dryRun: true, sql, token: g.token, estimatedRows: null };
687
+ const ds = await this.getDataSource(conn, database);
688
+ await ds.query(sql);
689
+ (0, guard_1.audit)({ action: 'proc.drop', conn: conn.name, database, sql, ok: true });
690
+ return { dropped: name, sql };
691
+ }
692
+ // ============ 导入导出 ============
693
+ async exportRowsFile(ref, database, table, format, outFile, opts = {}) {
694
+ const conn = await this.resolveConnection(ref);
695
+ const fmt = (format || 'json').toLowerCase();
696
+ const columns = await this.getColumns(conn, database, table);
697
+ const where = await this.whereTree(opts.filters, opts.filterJson);
698
+ const maxRows = (0, config_1.getConfig)().maxRows ?? 1000;
699
+ const limit = opts.limit ? Math.min(Math.floor(opts.limit), maxRows * 10) : maxRows;
700
+ const q = (0, query_builder_1.buildSelect)(conn.type, table, columns, { where, limit });
701
+ const ds = await this.getDataSource(conn, database);
702
+ const rows = await ds.query(q.sql, q.params);
703
+ const file = outFile || `${table}_export.${fmt === 'sql' ? 'sql' : fmt}`;
704
+ const abs = path.resolve(file);
705
+ const dir = path.dirname(abs);
706
+ if (!fs.existsSync(dir))
707
+ fs.mkdirSync(dir, { recursive: true });
708
+ if (fmt === 'json') {
709
+ fs.writeFileSync(abs, JSON.stringify(rows, null, 2), 'utf8');
710
+ }
711
+ else if (fmt === 'csv') {
712
+ fs.writeFileSync(abs, toCsv(rows), 'utf8');
713
+ }
714
+ else if (fmt === 'sql') {
715
+ const lines = [];
716
+ const colNames = columns.map((c) => c.name);
717
+ for (const row of rows) {
718
+ const vals = colNames
719
+ .map((c) => (row[c] === null || row[c] === undefined ? 'NULL' : `'${String(row[c]).replace(/'/g, "''")}'`))
720
+ .join(', ');
721
+ lines.push(`INSERT INTO ${(0, dialect_1.quoteIdentifier)(conn.type, table)} (${colNames.map((c) => (0, dialect_1.quoteIdentifier)(conn.type, c)).join(', ')}) VALUES (${vals});`);
722
+ }
723
+ fs.writeFileSync(abs, lines.join('\n'), 'utf8');
724
+ }
725
+ else {
726
+ throw new errors_1.AppError('PARAM_ERROR', `不支持的导出格式 ${format},支持 json/csv/sql`, { hint: 'xlsx 暂不支持,请用 json/csv' });
727
+ }
728
+ (0, guard_1.audit)({ action: 'export.rows', conn: conn.name, database, table, sql: q.sql, params: q.params.length ? q.params : null, rows: rows.length, ok: true });
729
+ return { conn: conn.name, database, table, format: fmt, path: abs, rowCount: rows.length };
730
+ }
731
+ async exportSchemaCmd(ref, database, outFile) {
732
+ const conn = await this.resolveConnection(ref);
733
+ const service = this.loadDialectService(conn.type);
734
+ const ds = await this.getDataSource(conn, database);
735
+ const schema = await service.exportSchema(ds, database);
736
+ if (outFile) {
737
+ const abs = path.resolve(outFile);
738
+ const dir = path.dirname(abs);
739
+ if (!fs.existsSync(dir))
740
+ fs.mkdirSync(dir, { recursive: true });
741
+ fs.writeFileSync(abs, schema, 'utf8');
742
+ return { conn: conn.name, database, path: abs };
743
+ }
744
+ return { conn: conn.name, database, schema };
745
+ }
746
+ async importFile(ref, database, table, file, opts) {
747
+ const conn = await this.resolveConnection(ref);
748
+ if (!fs.existsSync(file))
749
+ throw new errors_1.AppError('PARAM_ERROR', `文件不存在: ${file}`);
750
+ const fmt = (opts.format || path.extname(file).replace('.', '').toLowerCase() || 'csv');
751
+ const columns = await this.getColumns(conn, database, table);
752
+ const columnNames = new Set(columns.map((c) => c.name));
753
+ let rows;
754
+ if (fmt === 'json') {
755
+ rows = JSON.parse(fs.readFileSync(file, 'utf8'));
756
+ if (!Array.isArray(rows))
757
+ rows = [rows];
758
+ }
759
+ else if (fmt === 'csv') {
760
+ rows = parseCsv(fs.readFileSync(file, 'utf8'));
761
+ }
762
+ else {
763
+ throw new errors_1.AppError('PARAM_ERROR', `不支持的导入格式 ${fmt},支持 csv/json`);
764
+ }
765
+ if (opts.limit)
766
+ rows = rows.slice(0, Math.floor(opts.limit));
767
+ const clean = rows.map((r) => {
768
+ const out = {};
769
+ for (const k of Object.keys(r)) {
770
+ const key = k.trim();
771
+ if (!columnNames.has(key))
772
+ continue;
773
+ const meta = columns.find((c) => c.name === key);
774
+ out[key] = coerce(r[key], meta && meta.type ? meta.type : undefined);
775
+ }
776
+ return out;
777
+ });
778
+ const label = `IMPORT ${rows.length} rows into ${table}`;
779
+ const g = (0, guard_1.gateWrite)({ op: 'import.file', sql: label, params: [], estimatedRows: rows.length, dryRun: opts.dryRun, confirm: opts.confirm, write: opts.write, force: opts.force, yes: opts.yes });
780
+ if (g.mode === 'dryrun') {
781
+ return { dryRun: true, estimatedRows: rows.length, token: g.token, sql: label };
782
+ }
783
+ const ds = await this.getDataSource(conn, database);
784
+ const service = this.loadDialectService(conn.type);
785
+ await service.bulkInsert(ds, database, table, clean);
786
+ (0, guard_1.audit)({ action: 'import.file', conn: conn.name, database, table, sql: label, rows: rows.length, ok: true });
787
+ return { imported: rows.length, table };
788
+ }
789
+ async backupCmd(ref, database, outFile) {
790
+ const conn = await this.resolveConnection(ref);
791
+ const service = this.loadDialectService(conn.type);
792
+ const ds = await this.getDataSource(conn, database);
793
+ const result = await service.backupDatabase(ds, database, outFile ? { path: path.dirname(path.resolve(outFile)) } : undefined);
794
+ (0, guard_1.audit)({ action: 'backup', conn: conn.name, database, sql: `BACKUP ${database}`, ok: true });
795
+ return { conn: conn.name, database, backupPath: result };
796
+ }
797
+ async restoreCmd(ref, database, filePath, opts) {
798
+ const conn = await this.resolveConnection(ref);
799
+ const label = `RESTORE ${database} FROM ${filePath}`;
800
+ const g = (0, guard_1.gateWrite)({ op: 'restore', sql: label, params: [], destructive: true, ...opts });
801
+ if (g.mode === 'dryrun')
802
+ return { dryRun: true, sql: label, token: g.token, estimatedRows: null };
803
+ const service = this.loadDialectService(conn.type);
804
+ const ds = await this.getDataSource(conn, database);
805
+ await service.restoreDatabase(ds, database, filePath);
806
+ (0, guard_1.audit)({ action: 'restore', conn: conn.name, database, sql: label, ok: true });
807
+ return { restored: database };
808
+ }
809
+ // ============ 运维 ============
810
+ async statsCmd(ref, database) {
811
+ const conn = await this.resolveConnection(ref);
812
+ const service = this.loadDialectService(conn.type);
813
+ const ds = await this.getDataSource(conn, database);
814
+ const tables = await service.getTables(ds, database);
815
+ const size = await service.getDatabaseSize(ds, database);
816
+ return {
817
+ conn: conn.name,
818
+ database,
819
+ stats: {
820
+ tableCount: tables.length,
821
+ size,
822
+ tables: tables.map((t) => ({ name: t.name, rowCount: t.rowCount || 0, size: t.dataSize || 0 }))
823
+ }
824
+ };
825
+ }
826
+ async maintenanceCmd(ref, database, action, opts) {
827
+ const conn = await this.resolveConnection(ref);
828
+ if ((0, dialect_1.getDialectInfo)(conn.type).kind !== 'mysql') {
829
+ throw new errors_1.AppError('UNSUPPORTED', `${action} 仅支持 MySQL`);
830
+ }
831
+ const service = this.loadDialectService(conn.type);
832
+ const ds = await this.getDataSource(conn, database);
833
+ const tables = await service.getTables(ds, database);
834
+ const keyword = action.toUpperCase();
835
+ const statements = tables.map((t) => `${keyword} TABLE ${(0, dialect_1.quoteIdentifier)(conn.type, t.name)}`);
836
+ const label = statements.join('; ');
837
+ const g = (0, guard_1.gateWrite)({ op: `ops.${action}`, sql: label, params: [], destructive: true, ...opts });
838
+ if (g.mode === 'dryrun')
839
+ return { dryRun: true, sql: statements, token: g.token, estimatedRows: null };
840
+ const results = [];
841
+ for (const s of statements) {
842
+ try {
843
+ const r = await ds.query(s);
844
+ results.push({ sql: s, success: true, result: r });
845
+ }
846
+ catch (e) {
847
+ results.push({ sql: s, success: false, error: e?.message || String(e) });
848
+ }
849
+ }
850
+ (0, guard_1.audit)({ action: `ops.${action}`, conn: conn.name, database, sql: label, ok: true });
851
+ return { conn: conn.name, database, action, results };
852
+ }
853
+ async logsCmd(ref, database, limit) {
854
+ const conn = await this.resolveConnection(ref);
855
+ const service = this.loadDialectService(conn.type);
856
+ const ds = await this.getDataSource(conn, database || undefined);
857
+ const logs = await service.viewLogs(ds, database || undefined, Math.min(Math.max(1, limit || 100), 1000));
858
+ return { conn: conn.name, database: database || null, logs: logs || [] };
859
+ }
860
+ // ============ SQL 逃生舱 ============
861
+ /** 粗粒度判断单语句 DELETE/UPDATE 是否无条件(无 WHERE),用于强制 --yes */
862
+ isUnconditionalDml(sql) {
863
+ const s = String(sql).trim().replace(/;+\s*$/, '');
864
+ if (/^delete\b/i.test(s) || /^update\b/i.test(s)) {
865
+ return !/\bwhere\b/i.test(s);
866
+ }
867
+ return false;
868
+ }
869
+ async runSql(ref, database, sql, opts) {
870
+ const conn = await this.resolveConnection(ref);
871
+ if (!sql || !String(sql).trim())
872
+ throw new errors_1.AppError('PARAM_ERROR', 'SQL 不能为空');
873
+ const cls = (0, query_builder_1.classifySql)(sql);
874
+ const isWrite = cls.kind === 'write' || cls.kind === 'ddl' || !!cls.hasWrite;
875
+ if (cls.kind === 'multi' && !opts.allowMulti) {
876
+ throw new errors_1.AppError('MULTI_STATEMENT_BLOCKED', '默认禁止多语句 SQL', { hint: '确认需要时请附加 --allow-multi' });
877
+ }
878
+ if (isWrite && !opts.write) {
879
+ throw new errors_1.AppError('WRITE_NOT_ALLOWED', '默认只允许只读 SQL', { hint: '确认需要写入时请附加 --write' });
880
+ }
881
+ if (isWrite) {
882
+ // 含 DDL 或单语句无条件 DELETE/UPDATE 视为破坏性,需 --yes;写语句需确认令牌
883
+ const destructive = cls.kind === 'ddl' || !!cls.destructive || (cls.kind === 'write' && this.isUnconditionalDml(sql));
884
+ const g = (0, guard_1.gateWrite)({ op: 'sql.write', sql, params: [], destructive, yes: opts.yes, dryRun: opts.dryRun, confirm: opts.confirm, write: true });
885
+ if (g.mode === 'dryrun') {
886
+ return { dryRun: true, sql, token: g.token, estimatedRows: null };
887
+ }
888
+ }
889
+ const ds = await this.getDataSource(conn, database || undefined);
890
+ let result;
891
+ if (cls.kind === 'select') {
892
+ const maxRows = (0, config_1.getConfig)().maxRows ?? 1000;
893
+ const effective = sql.trim().replace(/;+\s*$/, '');
894
+ const limited = opts.limit ? Math.min(Math.floor(opts.limit), maxRows) : maxRows;
895
+ if (cls.kind === 'select') {
896
+ result = await ds.query(effective);
897
+ if (Array.isArray(result) && result.length > limited) {
898
+ result = result.slice(0, limited);
899
+ }
900
+ }
901
+ else {
902
+ result = await ds.query(sql);
903
+ }
904
+ }
905
+ else {
906
+ result = await ds.query(sql);
907
+ }
908
+ (0, guard_1.audit)({ action: 'sql.exec', conn: conn.name, database, sql, kind: cls.kind, rows: Array.isArray(result) ? result.length : undefined, ok: true });
909
+ return {
910
+ conn: conn.name,
911
+ database: database || null,
912
+ kind: cls.kind,
913
+ data: Array.isArray(result) ? result : result,
914
+ meta: { rowCount: Array.isArray(result) ? result.length : null }
915
+ };
916
+ }
917
+ // ============ 工具命令 ============
918
+ async configShow() {
919
+ return { dataDir: (0, config_1.dataDir)(), config: (0, config_1.getConfig)(), auditLog: getDataLogPath() };
920
+ }
921
+ async configSet(key, value) {
922
+ const cfg = (0, config_1.getConfig)();
923
+ const parsed = coerceConfigValue(value);
924
+ if (key === 'readonly') {
925
+ (0, config_1.updateConfig)({ readonly: !!parsed });
926
+ }
927
+ else if (key === 'defaultConnectionId') {
928
+ (0, config_1.updateConfig)({ defaultConnectionId: parsed == null ? null : String(parsed) });
929
+ }
930
+ else if (key === 'maxRows') {
931
+ (0, config_1.updateConfig)({ maxRows: Math.max(1, Math.floor(Number(parsed) || 1000)) });
932
+ }
933
+ else if (key === 'confirmThreshold') {
934
+ (0, config_1.updateConfig)({ confirmThreshold: Math.max(0, Math.floor(Number(parsed) || 100)) });
935
+ }
936
+ else if (key === 'protectedDatabases') {
937
+ (0, config_1.updateConfig)({ protectedDatabases: String(value).split(',').map((s) => s.trim()).filter(Boolean) });
938
+ }
939
+ else if (['readonly', 'defaultConnectionId', 'maxRows', 'confirmThreshold', 'protectedDatabases'].includes(key)) {
940
+ (0, config_1.updateConfig)({ [key]: parsed });
941
+ }
942
+ else {
943
+ throw new errors_1.AppError('PARAM_ERROR', `未知配置项: ${key}`, { hint: '可用: readonly / maxRows / confirmThreshold / protectedDatabases / defaultConnectionId' });
944
+ }
945
+ return { key, value, config: (0, config_1.getConfig)() };
946
+ }
947
+ async auditLog(limit) {
948
+ return { logs: (0, config_1.readAudit)(Math.min(Math.max(1, limit || 100), 500)) };
949
+ }
950
+ async setupInfo() {
951
+ const conns = await this.allConnections();
952
+ const cfg = (0, config_1.getConfig)();
953
+ return {
954
+ dataDir: (0, config_1.dataDir)(),
955
+ connections: conns.length,
956
+ defaultConnectionId: cfg.defaultConnectionId || null,
957
+ readonly: !!cfg.readonly
958
+ };
959
+ }
960
+ }
961
+ exports.DbToolApi = DbToolApi;
962
+ function toCsv(rows) {
963
+ if (!rows || rows.length === 0)
964
+ return '';
965
+ const headers = Object.keys(rows[0]);
966
+ const esc = (v) => {
967
+ const s = v === null || v === undefined ? '' : String(v);
968
+ return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
969
+ };
970
+ const lines = [headers.join(',')];
971
+ for (const row of rows) {
972
+ lines.push(headers.map((h) => esc(row[h])).join(','));
973
+ }
974
+ return lines.join('\n');
975
+ }
976
+ function parseCsv(content) {
977
+ const lines = content.split(/\r?\n/).filter((l) => l.trim() !== '');
978
+ if (lines.length === 0)
979
+ return [];
980
+ const parseLine = (line) => {
981
+ const out = [];
982
+ let cur = '';
983
+ let inQ = false;
984
+ for (let i = 0; i < line.length; i++) {
985
+ const ch = line[i];
986
+ if (inQ) {
987
+ if (ch === '"') {
988
+ if (line[i + 1] === '"') {
989
+ cur += '"';
990
+ i++;
991
+ }
992
+ else
993
+ inQ = false;
994
+ }
995
+ else
996
+ cur += ch;
997
+ }
998
+ else if (ch === '"') {
999
+ inQ = true;
1000
+ }
1001
+ else if (ch === ',') {
1002
+ out.push(cur);
1003
+ cur = '';
1004
+ }
1005
+ else {
1006
+ cur += ch;
1007
+ }
1008
+ }
1009
+ out.push(cur);
1010
+ return out.map((s) => s.trim());
1011
+ };
1012
+ const headers = parseLine(lines[0]);
1013
+ const result = [];
1014
+ for (const l of lines.slice(1)) {
1015
+ const cells = parseLine(l);
1016
+ const row = {};
1017
+ headers.forEach((h, i) => {
1018
+ row[h] = cells[i] !== undefined ? cells[i] : '';
1019
+ });
1020
+ result.push(row);
1021
+ }
1022
+ return result;
1023
+ }
1024
+ function coerce(v, type) {
1025
+ if (v === '' || v === null || v === undefined)
1026
+ return v === '' ? null : v;
1027
+ const t = (type || '').toLowerCase();
1028
+ if (t.includes('int')) {
1029
+ const n = Number(v);
1030
+ return isNaN(n) ? v : n;
1031
+ }
1032
+ if (t.includes('decimal') || t.includes('float') || t.includes('double') || t.includes('numeric') || t.includes('real')) {
1033
+ const n = Number(v);
1034
+ return isNaN(n) ? v : n;
1035
+ }
1036
+ if (t.includes('bool')) {
1037
+ if (v === true || v === 'true' || v === '1' || v === 1)
1038
+ return true;
1039
+ if (v === false || v === 'false' || v === '0' || v === 0)
1040
+ return false;
1041
+ }
1042
+ if (v === 'true')
1043
+ return true;
1044
+ if (v === 'false')
1045
+ return false;
1046
+ const n = Number(v);
1047
+ if (v !== '' && !isNaN(n) && /^-?\d+(\.\d+)?$/.test(String(v)))
1048
+ return n;
1049
+ return v;
1050
+ }
1051
+ function coerceConfigValue(v) {
1052
+ if (v === 'true')
1053
+ return true;
1054
+ if (v === 'false')
1055
+ return false;
1056
+ const n = Number(v);
1057
+ if (!isNaN(n) && String(v).trim() !== '')
1058
+ return n;
1059
+ return v;
1060
+ }
1061
+ function formatDdlDefault(v) {
1062
+ if (v === null)
1063
+ return 'NULL';
1064
+ const up = String(v).toUpperCase();
1065
+ if (up === 'CURRENT_TIMESTAMP' || up === 'NOW()' || up === 'CURRENT_DATE')
1066
+ return up;
1067
+ if (typeof v === 'number' || typeof v === 'boolean')
1068
+ return String(v);
1069
+ return `'${String(v).replace(/'/g, "''")}'`;
1070
+ }
1071
+ /** 列类型输出(用户 DSL 已含长度时原样大写;没有长度时按基础类型) */
1072
+ function typeFor(kind, c) {
1073
+ return c.type.toUpperCase();
1074
+ }
1075
+ function getDataLogPath() {
1076
+ return path.join((0, config_1.dataDir)(), 'audit.log');
1077
+ }
1078
+ //# sourceMappingURL=db-api.js.map