@xiaokexiang/db-cli 0.2.1

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 (4) hide show
  1. package/README.md +111 -0
  2. package/bin.cjs +11 -0
  3. package/db-cli.js +997 -0
  4. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # DB-CLI - 多数据库命令行工具
2
+
3
+ 一个基于 Node.js 的多数据库 CLI 工具,支持 MySQL 和达梦数据库的 SQL 导入、导出和执行。
4
+
5
+ ## 安装
6
+ ### 全局安装(推荐)
7
+
8
+ ```bash
9
+ npm install -g @xiaokexiang/db-cli
10
+ ```
11
+
12
+ 安装后可使用 `db-cli` 命令。
13
+
14
+ ## 快速开始
15
+
16
+ ### 1. 连接数据库
17
+
18
+ ```bash
19
+ db-cli -c '<连接字符串>' <命令> [选项]
20
+ ```
21
+
22
+ **连接字符串格式**:
23
+
24
+ | 数据库 | 格式 | 示例 |
25
+ |--------|------|------|
26
+ | 达梦数据库 | `dm://用户名:密码@主机:端口` | `dm://SYSDBA:SYSDBA001@localhost:5236` |
27
+ | MySQL | `mysql://用户名:密码@主机:端口` | `mysql://root:password@localhost:3306` |
28
+
29
+ > **注意**:连接字符串不支持数据库名,所有 schema 请通过 SQL 语句处理。
30
+
31
+ **示例**:
32
+ ```bash
33
+ # 达梦数据库
34
+ db-cli -c 'dm://SYSDBA:SYSDBA001@localhost:5236' exec -q 'SELECT 1'
35
+
36
+ # MySQL
37
+ db-cli -c 'mysql://root:password@localhost:3306' exec -q 'SELECT 1'
38
+ ```
39
+
40
+ ### 2. 执行 SQL 查询
41
+
42
+ ```bash
43
+ # 表格输出(默认)
44
+ db-cli -c 'mysql://root:password@localhost:3306' exec -q 'SELECT * FROM users'
45
+
46
+ # JSON 输出
47
+ db-cli -c 'mysql://root:password@localhost:3306' exec -q 'SELECT * FROM users' --format json
48
+
49
+ # 多条语句执行
50
+ db-cli -c 'mysql://root:password@localhost:3306' exec -q 'SELECT 1; SELECT 2;'
51
+
52
+ # 遇到错误继续执行
53
+ db-cli -c 'mysql://root:password@localhost:3306' exec -q 'SELECT 1; INVALID_SQL; SELECT 3;' --continue-on-error
54
+ ```
55
+
56
+ ### 3. 导入 SQL 文件
57
+
58
+ ```bash
59
+ # 导入到 MySQL(推荐:在 SQL 文件中使用 USE 语句)
60
+ db-cli -c 'mysql://root:password@localhost:3306' import -f data.sql
61
+
62
+ # 导入到 MySQL(使用 -s 参数指定数据库)
63
+ db-cli -c 'mysql://root:password@localhost:3306' import -s database_name -f data.sql
64
+
65
+ # 导入到达梦数据库
66
+ db-cli -c 'dm://SYSDBA:SYSDBA001@localhost:5236' import -f data.sql
67
+
68
+ # 遇到错误继续执行
69
+ db-cli -c 'mysql://root:password@localhost:3306' import -f data.sql --continue-on-error
70
+ ```
71
+
72
+ **SQL 文件格式说明**:
73
+ - MySQL: 使用 `;` 作为语句分隔符,支持 `#` 和 `--` 注释,可以在文件开头使用 `USE database_name;` 选择数据库
74
+ - 达梦数据库:支持 `/` 和 `;` 作为语句分隔符,支持 `--` 注释
75
+
76
+ ### 4. 导出数据
77
+
78
+ ```bash
79
+ # 导出整个数据库(表结构 + 数据)
80
+ db-cli -c 'mysql://root:password@localhost:3306' export -s bocloud_upms
81
+
82
+ # 仅导出表结构
83
+ db-cli -c 'mysql://root:password@localhost:3306' export -s bocloud_upms --type=schema
84
+
85
+ # 仅导出数据
86
+ db-cli -c 'mysql://root:password@localhost:3306' export -s bocloud_upms --type=data
87
+
88
+ # 导出单个表
89
+ db-cli -c 'mysql://root:password@localhost:3306' export -s bocloud_upms -t upms_core_account
90
+
91
+ # 导出多个表
92
+ db-cli -c 'mysql://root:password@localhost:3306' export -s bocloud_upms -T users,roles,permissions
93
+
94
+ # 导出到文件
95
+ db-cli -c 'mysql://root:password@localhost:3306' export -s bocloud_upms -o backup.sql
96
+
97
+ # 自定义查询导出
98
+ db-cli -c 'mysql://root:password@localhost:3306' export -q 'SELECT * FROM users WHERE id > 100'
99
+ ```
100
+
101
+ ## 命令帮助
102
+
103
+ ```bash
104
+ # 查看主帮助
105
+ db-cli --help
106
+
107
+ # 查看具体命令帮助
108
+ db-cli exec --help
109
+ db-cli import --help
110
+ db-cli export --help
111
+ ```
package/bin.cjs ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const { spawnSync } = require('child_process');
4
+ const path = require('path');
5
+
6
+ const result = spawnSync(process.execPath, ['--openssl-legacy-provider', path.join(__dirname, 'db-cli.js'), ...process.argv.slice(2)], {
7
+ stdio: 'inherit',
8
+ shell: false
9
+ });
10
+
11
+ process.exitCode = result.status;
package/db-cli.js ADDED
@@ -0,0 +1,997 @@
1
+ #!/usr/bin/env node
2
+ import mysql from "mysql2/promise";
3
+ import dm from "dmdb";
4
+ import fs from "fs";
5
+ import cac from "cac";
6
+
7
+ // ============================================
8
+ // DB-CLI - 多数据库 CLI 工具
9
+ // ============================================
10
+ // 用法:
11
+ // node db-cli.js -h 显示帮助
12
+ // node db-cli.js -c 'dm://user:pass@host:port' import -f xx 导入 SQL
13
+ // node db-cli.js -c 'dm://user:pass@host:port' export -s xx 导出数据
14
+ // node db-cli.js -c 'dm://user:pass@host:port' exec -q 'xx' 执行 SQL
15
+ // node db-cli.js -c 'mysql://user:pass@host:port' ... MySQL 支持
16
+ // ============================================
17
+
18
+ // 解析连接字符串
19
+ function parseConnectionString(connStr) {
20
+ // mysql://user:pass@host:port - 不支持 database
21
+ const mysqlMatch = connStr.match(
22
+ /^mysql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)$/,
23
+ );
24
+
25
+ // dm://user:pass@host:port
26
+ const dmMatch = connStr.match(/^dm:\/\/([^:]+):([^@]+)@([^:]+):(\d+)$/);
27
+
28
+ if (mysqlMatch) {
29
+ return {
30
+ type: "mysql",
31
+ username: mysqlMatch[1],
32
+ password: mysqlMatch[2],
33
+ host: mysqlMatch[3],
34
+ port: parseInt(mysqlMatch[4], 10),
35
+ database: null, // MySQL 连接字符串不再支持数据库名
36
+ };
37
+ }
38
+
39
+ if (dmMatch) {
40
+ return {
41
+ type: "dm",
42
+ username: dmMatch[1],
43
+ password: dmMatch[2],
44
+ host: dmMatch[3],
45
+ port: parseInt(dmMatch[4], 10),
46
+ };
47
+ }
48
+
49
+ throw new Error(`连接字符串格式错误
50
+ 支持格式:
51
+ - MySQL: mysql://user:pass@host:port
52
+ - 达梦数据库:dm://user:pass@host:port
53
+ 注意:连接字符串不再支持数据库名,所有 schema 请通过 SQL 语句处理`);
54
+ }
55
+
56
+ // 创建数据库连接的工厂函数
57
+ async function createConnection(connInfo) {
58
+ if (connInfo.type === "mysql") {
59
+ const conn = await mysql.createConnection({
60
+ host: connInfo.host,
61
+ port: connInfo.port,
62
+ user: connInfo.username,
63
+ password: connInfo.password,
64
+ });
65
+
66
+ // 包装 MySQL 连接以匹配统一接口
67
+ return {
68
+ type: "mysql",
69
+ raw: conn,
70
+ execute: async (sql) => {
71
+ const [rows] = await conn.execute(sql);
72
+ // 转换为统一格式
73
+ return {
74
+ rows: Array.isArray(rows) ? rows : [],
75
+ metaData:
76
+ rows.length > 0
77
+ ? Object.keys(rows[0]).map((k) => ({ name: k }))
78
+ : [],
79
+ updateCount:
80
+ rows.affectedRows !== undefined ? rows.affectedRows : rows.length,
81
+ };
82
+ },
83
+ commit: async () => {
84
+ await conn.commit();
85
+ },
86
+ rollback: async () => {
87
+ await conn.rollback();
88
+ },
89
+ close: async () => {
90
+ await conn.end();
91
+ },
92
+ };
93
+ } else if (connInfo.type === "dm") {
94
+ const conn = await dm.getConnection(
95
+ `dm://${connInfo.username}:${connInfo.password}@${connInfo.host}:${connInfo.port}`,
96
+ );
97
+ return {
98
+ type: "dm",
99
+ raw: conn,
100
+ execute: (sql) => conn.execute(sql),
101
+ commit: () => conn.commit(),
102
+ rollback: () => conn.rollback(),
103
+ close: () => conn.close(),
104
+ };
105
+ }
106
+
107
+ throw new Error("不支持的数据库类型:" + connInfo.type);
108
+ }
109
+
110
+ // ============================================
111
+ // 导入功能实现
112
+ // ============================================
113
+
114
+ function updateProgress(current, total) {
115
+ const percentage = ((current / total) * 100).toFixed(1);
116
+ const barWidth = 30;
117
+ const filled = Math.round((barWidth * current) / total);
118
+ const empty = barWidth - filled;
119
+ const bar = "█".repeat(filled) + "░".repeat(empty);
120
+ process.stdout.write(`\r进度:[${bar}] ${current}/${total} (${percentage}%)`);
121
+ if (current === total) {
122
+ process.stdout.write("\n");
123
+ }
124
+ }
125
+
126
+ // SQL 解析器 - 导出供测试使用
127
+ // 支持 MySQL DELIMITER 语法和达梦数据库 / 分隔符
128
+ export function readSqlFile(filePath, dbType = "dm") {
129
+ const content = fs.readFileSync(filePath, "utf-8");
130
+
131
+ const statements = [];
132
+ const lines = content.split("\n");
133
+ let currentStatement = "";
134
+
135
+ // MySQL DELIMITER 处理状态
136
+ // insideDelimiterBlock = true 表示当前在使用自定义分隔符(如 $$)
137
+ let insideDelimiterBlock = false;
138
+ let currentDelimiter = ";"; // 当前有效的语句分隔符
139
+
140
+ // 达梦数据库存储过程状态
141
+ let inDamengProcedure = false;
142
+
143
+ for (let line of lines) {
144
+ const originalLine = line;
145
+ line = line.trim();
146
+
147
+ // 跳过空行
148
+ if (!line) continue;
149
+
150
+ // 注释处理
151
+ // -- 注释:两种数据库都支持
152
+ if (line.startsWith("--")) continue;
153
+ // # 注释:仅 MySQL 支持
154
+ if (dbType === "mysql" && line.startsWith("#")) continue;
155
+
156
+ // MySQL DELIMITER 处理
157
+ if (dbType === "mysql") {
158
+ // 检查是否是 DELIMITER 语句
159
+ const delimiterMatch = line.match(/^DELIMITER\s+(\S+)\s*$/i);
160
+ if (delimiterMatch) {
161
+ const newDelimiter = delimiterMatch[1];
162
+ // 切换分隔符模式
163
+ if (newDelimiter === ";") {
164
+ // 恢复标准分隔符,结束当前语句
165
+ insideDelimiterBlock = false;
166
+ currentDelimiter = ";";
167
+ if (currentStatement.trim()) {
168
+ statements.push(currentStatement.trim());
169
+ currentStatement = "";
170
+ }
171
+ } else {
172
+ // 使用自定义分隔符(如 $$)
173
+ insideDelimiterBlock = true;
174
+ currentDelimiter = newDelimiter;
175
+ }
176
+ continue;
177
+ }
178
+
179
+ // 在 DELIMITER 块内,检查是否遇到自定义分隔符(如 $$ 在行尾)
180
+ if (insideDelimiterBlock) {
181
+ // 检查行尾是否是自定义分隔符(如 "END $$")
182
+ if (line.endsWith(currentDelimiter)) {
183
+ // 移除行尾的分隔符
184
+ const lineWithoutDelimiter = line.substring(0, line.length - currentDelimiter.length).trim();
185
+ if (lineWithoutDelimiter) {
186
+ currentStatement += lineWithoutDelimiter + "\n";
187
+ }
188
+ if (currentStatement.trim()) {
189
+ statements.push(currentStatement.trim());
190
+ }
191
+ currentStatement = "";
192
+ insideDelimiterBlock = false;
193
+ currentDelimiter = ";";
194
+ } else {
195
+ currentStatement += originalLine + "\n";
196
+ }
197
+ continue;
198
+ }
199
+ }
200
+
201
+ // 达梦数据库存储过程处理
202
+ if (dbType === "dm") {
203
+ // 检查是否是存储过程开始
204
+ if (line.includes("CREATE OR REPLACE PROCEDURE") || line.includes("CREATE PROCEDURE")) {
205
+ inDamengProcedure = true;
206
+ }
207
+
208
+ // 检查是否是 / 分隔符(达梦存储过程结束符)
209
+ if (line === "/") {
210
+ if (inDamengProcedure) {
211
+ // 结束存储过程
212
+ if (currentStatement.trim()) {
213
+ statements.push(currentStatement.trim());
214
+ }
215
+ currentStatement = "";
216
+ inDamengProcedure = false;
217
+ } else {
218
+ // 普通语句的 / 分隔符
219
+ if (currentStatement.trim()) {
220
+ statements.push(currentStatement.trim());
221
+ currentStatement = "";
222
+ }
223
+ }
224
+ continue;
225
+ }
226
+
227
+ // 在存储过程内,累积所有行(包括分号)
228
+ if (inDamengProcedure) {
229
+ currentStatement += originalLine + "\n";
230
+ continue;
231
+ }
232
+
233
+ // 普通达梦语句:分号结束
234
+ currentStatement += originalLine + "\n";
235
+ if (line.endsWith(";")) {
236
+ const trimmed = currentStatement.trim();
237
+ if (trimmed) {
238
+ statements.push(trimmed);
239
+ }
240
+ currentStatement = "";
241
+ }
242
+ continue;
243
+ }
244
+
245
+ // 标准 ; 分隔符处理(其他数据库)
246
+ currentStatement += originalLine + "\n";
247
+ if (line.endsWith(";")) {
248
+ const trimmed = currentStatement.trim();
249
+ if (trimmed) {
250
+ statements.push(trimmed);
251
+ }
252
+ currentStatement = "";
253
+ }
254
+ }
255
+
256
+ // 处理剩余语句
257
+ if (currentStatement.trim()) {
258
+ statements.push(currentStatement.trim());
259
+ }
260
+
261
+ return statements;
262
+ }
263
+
264
+ async function executeSqlStatements(conn, statements, config) {
265
+ const continueOnError = config.continueOnError;
266
+ let success = 0;
267
+ let errors = 0;
268
+ const errorDetails = [];
269
+ const total = statements.length;
270
+ let lastProgressUpdate = 0;
271
+
272
+ console.log("执行中...");
273
+ updateProgress(0, total);
274
+
275
+ for (let i = 0; i < statements.length; i++) {
276
+ const statement = statements[i];
277
+ try {
278
+ // MySQL: DDL 和管理命令不能用 prepared statement 执行,需要使用 raw.query
279
+ if (conn.type === "mysql") {
280
+ // 检查是否是 DDL 或管理命令
281
+ // 包含:USE, SET, SHOW, CREATE, DROP, ALTER, TRUNCATE, REPLACE, RENAME, LOAD, LOCK, UNLOCK
282
+ // GRANT, REVOKE, FLUSH, RESET, CALL, BEGIN, COMMIT, ROLLBACK 等
283
+ const ddlPattern = /^\s*(USE|SET|SHOW|CREATE|DROP|ALTER|TRUNCATE|REPLACE|RENAME|LOAD|LOCK|UNLOCK|GRANT|REVOKE|FLUSH|RESET|CALL|BEGIN|COMMIT|ROLLBACK|START\s+TRANSACTION)\s*/i;
284
+ if (ddlPattern.test(statement)) {
285
+ await conn.raw.query(statement);
286
+ success++;
287
+ const progressStep = Math.max(1, Math.floor(total / 100));
288
+ if (i + 1 - lastProgressUpdate >= progressStep) {
289
+ updateProgress(i + 1, total);
290
+ lastProgressUpdate = i + 1;
291
+ }
292
+ continue;
293
+ }
294
+ }
295
+
296
+ await conn.execute(statement);
297
+ success++;
298
+ const progressStep = Math.max(1, Math.floor(total / 100));
299
+ if (i + 1 - lastProgressUpdate >= progressStep) {
300
+ updateProgress(i + 1, total);
301
+ lastProgressUpdate = i + 1;
302
+ }
303
+ } catch (err) {
304
+ errors++;
305
+ const preview = statement.substring(0, 100).replace(/\n/g, " ");
306
+ errorDetails.push({
307
+ index: i + 1,
308
+ message: err.message,
309
+ preview: preview + (statement.length > 100 ? "..." : ""),
310
+ });
311
+ if (!continueOnError) {
312
+ console.log();
313
+ console.log(`\n第 ${i + 1} 条语句执行失败,正在回滚...`);
314
+ await conn.rollback();
315
+ console.log("事务已回滚");
316
+ throw err;
317
+ }
318
+ }
319
+ }
320
+
321
+ if (lastProgressUpdate < total) {
322
+ updateProgress(total, total);
323
+ }
324
+
325
+ console.log(`\n完成:${success} 成功,${errors} 失败`);
326
+
327
+ if (errorDetails.length > 0) {
328
+ console.log("\n失败语句详情:");
329
+ for (const err of errorDetails) {
330
+ console.log(`\n语句 ${err.index}: ${err.message}`);
331
+ console.log(` SQL: ${err.preview}`);
332
+ }
333
+ }
334
+
335
+ return { success, errors };
336
+ }
337
+
338
+ async function runImport(conn, options) {
339
+ const config = {
340
+ file: options.file,
341
+ continueOnError: options.continueOnError || false,
342
+ dbType: conn.type, // 从连接对象获取数据库类型
343
+ };
344
+
345
+ if (!config.file) {
346
+ console.error("错误:缺少必填参数 -f/--file");
347
+ process.exit(1);
348
+ }
349
+
350
+ if (!fs.existsSync(config.file)) {
351
+ console.error(`错误:文件不存在:${config.file}`);
352
+ process.exit(1);
353
+ }
354
+
355
+ console.log("导入 SQL 文件");
356
+ console.log("============");
357
+ console.log(`文件:${config.file}`);
358
+ console.log("");
359
+
360
+ const statements = readSqlFile(config.file, config.dbType);
361
+ console.log(`读取到 ${statements.length} 条 SQL 语句`);
362
+ console.log("");
363
+
364
+ await executeSqlStatements(conn, statements, config);
365
+ console.log("导入完成");
366
+ }
367
+
368
+ // ============================================
369
+ // 导出功能实现
370
+ // ============================================
371
+
372
+ async function getTableList(conn, schema, filterTables = []) {
373
+ let sql = "SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER = ?";
374
+ const params = [schema.toUpperCase()];
375
+
376
+ if (filterTables.length > 0) {
377
+ sql += " AND TABLE_NAME IN (" + filterTables.map(() => "?").join(",") + ")";
378
+ params.push(...filterTables.map((t) => t.toUpperCase()));
379
+ }
380
+
381
+ const result = await conn.execute(sql, params);
382
+ return result.rows.map((row) => row[0]);
383
+ }
384
+
385
+ async function getTableListMySQL(conn, database, filterTables = []) {
386
+ // 先选择数据库 (使用 query 而非 execute,因为 USE 命令不支持 prepared statement)
387
+ await conn.raw.query(`USE \`${database}\``);
388
+
389
+ const result = await conn.raw.query("SHOW TABLES");
390
+ let tables = result[0].map((row) => {
391
+ // MySQL SHOW TABLES returns single column data
392
+ const tableName = Object.values(row)[0];
393
+ return tableName;
394
+ });
395
+
396
+ if (filterTables.length > 0) {
397
+ tables = tables.filter((t) => filterTables.includes(t));
398
+ }
399
+
400
+ return tables;
401
+ }
402
+
403
+ async function getTableDDL(conn, tableName, schema) {
404
+ const result = await conn.execute(
405
+ "SELECT DBMS_METADATA.GET_DDL(?, ?, ?) AS DDL FROM DUAL",
406
+ ["TABLE", tableName.toUpperCase(), schema.toUpperCase()],
407
+ );
408
+
409
+ if (result.rows.length === 0) return "";
410
+
411
+ const clob = result.rows[0][0];
412
+ const ddl = await clob.getData();
413
+ await clob.close();
414
+
415
+ return ddl + ";";
416
+ }
417
+
418
+ async function getTableDDLMySQL(conn, tableName, database) {
419
+ const result = await conn.raw.query(`SHOW CREATE TABLE \`${tableName}\``);
420
+ if (result[0].length === 0) return "";
421
+
422
+ // SHOW CREATE TABLE returns two columns: table name and CREATE statement
423
+ const createStmt = result[0][0]['Create Table'];
424
+ return createStmt + ";";
425
+ }
426
+
427
+ async function getTableData(conn, tableName, schema) {
428
+ const result = await conn.execute(`SELECT * FROM "${schema}"."${tableName}"`);
429
+ return {
430
+ rows: result.rows,
431
+ metaData: result.metaData,
432
+ };
433
+ }
434
+
435
+ async function getTableDataMySQL(conn, tableName, database) {
436
+ const result = await conn.raw.query(`SELECT * FROM \`${tableName}\``);
437
+ return {
438
+ rows: result[0],
439
+ metaData:
440
+ result[0].length > 0
441
+ ? Object.keys(result[0][0]).map((k) => ({ name: k }))
442
+ : [],
443
+ };
444
+ }
445
+
446
+ function generateInserts(tableName, rows, metaData, dbType = "dm") {
447
+ const statements = [];
448
+ const columns = metaData.map((col) => col.name);
449
+
450
+ // Numeric pattern for recognizing numeric strings (INT, DECIMAL, etc.)
451
+ const numericPattern = /^-?\d+(\.\d+)?$/;
452
+
453
+ for (const row of rows) {
454
+ // MySQL returns objects, DM returns arrays
455
+ const values = Array.isArray(row)
456
+ ? row.map((val, idx) => {
457
+ if (val === null) return "NULL";
458
+ if (typeof val === "number") return val.toString();
459
+ // Check if string is a numeric value (e.g., DECIMAL from MySQL)
460
+ if (typeof val === "string" && numericPattern.test(val)) return val;
461
+ // Handle Date objects - format as ISO string for SQL
462
+ if (val instanceof Date) {
463
+ // Check for invalid date
464
+ if (isNaN(val.getTime())) return "NULL";
465
+ return `'${val.toISOString().replace("T", " ").substring(0, 19)}'`;
466
+ }
467
+ const escaped = val.toString().replace(/'/g, "''");
468
+ return `'${escaped}'`;
469
+ })
470
+ : columns.map((col) => {
471
+ const val = row[col];
472
+ if (val === null) return "NULL";
473
+ if (typeof val === "number") return val.toString();
474
+ // Check if string is a numeric value (e.g., DECIMAL from MySQL)
475
+ if (typeof val === "string" && numericPattern.test(val)) return val;
476
+ // Handle Date objects - format as ISO string for SQL
477
+ if (val instanceof Date) {
478
+ // Check for invalid date
479
+ if (isNaN(val.getTime())) return "NULL";
480
+ return `'${val.toISOString().replace("T", " ").substring(0, 19)}'`;
481
+ }
482
+ const escaped = val.toString().replace(/'/g, "''");
483
+ return `'${escaped}'`;
484
+ });
485
+
486
+ if (dbType === "mysql") {
487
+ statements.push(
488
+ `INSERT INTO \`${tableName}\` (\`${columns.join("`, `")}\`) VALUES (${values.join(", ")});`,
489
+ );
490
+ } else {
491
+ statements.push(
492
+ `INSERT INTO "${tableName}" ("${columns.join('", "')}") VALUES (${values.join(", ")});`,
493
+ );
494
+ }
495
+ }
496
+
497
+ return statements;
498
+ }
499
+
500
+ async function runExport(conn, options) {
501
+ const config = {
502
+ schema: options.schema,
503
+ type: options.type || "all",
504
+ mode: "full",
505
+ tables: options.table
506
+ ? Array.isArray(options.table)
507
+ ? options.table
508
+ : [options.table]
509
+ : [],
510
+ tablesList: options.tablesList,
511
+ query: options.query,
512
+ output: options.output,
513
+ };
514
+
515
+ // 处理 --type 参数
516
+ if (options.type) {
517
+ const typeValue = options.type.toLowerCase();
518
+ if (typeValue === "schema") {
519
+ config.mode = "schema";
520
+ } else if (typeValue === "data") {
521
+ config.mode = "data";
522
+ } else if (typeValue === "all") {
523
+ config.mode = "full";
524
+ } else {
525
+ console.error("错误:--type 必须是 schema、data 或 all");
526
+ showExportHelp();
527
+ process.exit(1);
528
+ }
529
+ }
530
+
531
+ // 处理多表选项
532
+ if (options.tablesList) {
533
+ config.tables = options.tablesList.split(",").map((t) => t.trim());
534
+ }
535
+
536
+ if (!config.schema && !config.query) {
537
+ console.error("错误:缺少必填参数 -s/--schema 或 -q/--query");
538
+ showExportHelp();
539
+ process.exit(1);
540
+ }
541
+
542
+ console.log(`${conn.type === "mysql" ? "MySQL" : "达梦数据库"}导出工具`);
543
+ console.log("================");
544
+ console.log(
545
+ `${conn.type === "mysql" ? "Database" : "Schema"}: ${config.schema || "N/A"}`,
546
+ );
547
+ console.log(`模式:${config.mode}`);
548
+ console.log("");
549
+
550
+ const output = [];
551
+
552
+ if (config.query) {
553
+ console.log(`执行查询...`);
554
+ const result = await conn.execute(config.query);
555
+ console.log(`查询到 ${result.rows.length} 行数据`);
556
+ const inserts = generateInserts(
557
+ "QUERY_RESULT",
558
+ result.rows,
559
+ result.metaData,
560
+ conn.type,
561
+ );
562
+ output.push(`-- 查询结果:${result.rows.length} 行`);
563
+ output.push(`-- 列:${result.metaData.map((col) => col.name).join(", ")}`);
564
+ output.push("");
565
+ output.push(...inserts);
566
+ } else {
567
+ console.log("获取表列表...");
568
+ // Select correct function based on database type
569
+ const getTableListFn =
570
+ conn.type === "mysql" ? getTableListMySQL : getTableList;
571
+ const getTableDDLFn =
572
+ conn.type === "mysql" ? getTableDDLMySQL : getTableDDL;
573
+ const getDataFn = conn.type === "mysql" ? getTableDataMySQL : getTableData;
574
+
575
+ // Get table list (note parameter differences)
576
+ const tables = await getTableListFn(conn, config.schema, config.tables);
577
+ console.log(`找到 ${tables.length} 个表`);
578
+
579
+ for (let i = 0; i < tables.length; i++) {
580
+ const tableName = tables[i];
581
+ console.log(`[${i + 1}/${tables.length}] 处理表:${tableName}`);
582
+
583
+ if (config.mode === "full" || config.mode === "schema") {
584
+ console.log(" - 导出表结构...");
585
+ const ddl = await getTableDDLFn(conn, tableName, config.schema);
586
+ output.push(ddl);
587
+ }
588
+
589
+ if (config.mode === "full" || config.mode === "data") {
590
+ console.log(" - 导出数据...");
591
+ const data = await getDataFn(conn, tableName, config.schema);
592
+ const inserts = generateInserts(
593
+ tableName,
594
+ data.rows,
595
+ data.metaData,
596
+ conn.type,
597
+ );
598
+ output.push(...inserts);
599
+ console.log(` 导出 ${data.rows.length} 行`);
600
+ }
601
+ }
602
+ }
603
+
604
+ const result = output.join("\n");
605
+
606
+ if (config.output) {
607
+ fs.writeFileSync(config.output, result, "utf-8");
608
+ console.log(`\n导出完成!已保存到:${config.output}`);
609
+ console.log(
610
+ `总大小:${(Buffer.byteLength(result, "utf8") / 1024).toFixed(2)} KB`,
611
+ );
612
+ } else {
613
+ console.log("");
614
+ console.log(result);
615
+ }
616
+ }
617
+
618
+ // ============================================
619
+ // 执行功能实现
620
+ // ============================================
621
+
622
+ async function runExec(conn, options) {
623
+ const config = {
624
+ sql: options.query,
625
+ format: options.format || "table",
626
+ continueOnError: options.continueOnError || false,
627
+ };
628
+
629
+ if (!config.sql) {
630
+ console.error("错误:缺少 SQL 语句,请使用 -q/--query 指定");
631
+ showExecHelp();
632
+ process.exit(1);
633
+ }
634
+
635
+ // 验证 format 参数
636
+ if (options.format) {
637
+ const formatValue = options.format.toLowerCase();
638
+ if (formatValue === "json") {
639
+ config.format = "json";
640
+ } else if (formatValue === "table") {
641
+ config.format = "table";
642
+ } else {
643
+ console.error("错误:--format 必须是 json 或 table");
644
+ showExecHelp();
645
+ process.exit(1);
646
+ }
647
+ }
648
+
649
+ const startTime = Date.now();
650
+
651
+ // 分割 SQL 语句(按分号分隔)
652
+ const statements = splitSqlStatements(config.sql);
653
+
654
+ if (statements.length === 0) {
655
+ if (config.format === "json") {
656
+ console.log('[{"rows": 0, "elapsed": "0ms", "data": []}]');
657
+ } else {
658
+ console.table([]);
659
+ console.log("0 rows");
660
+ console.log("Elapsed: 0ms");
661
+ }
662
+ return;
663
+ }
664
+
665
+ const results = [];
666
+
667
+ if (config.continueOnError) {
668
+ // --continue-on-error 模式:每条语句自动提交,跳过错误
669
+ let success = 0;
670
+ let errors = 0;
671
+
672
+ for (let i = 0; i < statements.length; i++) {
673
+ const statementStartTime = Date.now();
674
+ try {
675
+ const result = await conn.execute(statements[i]);
676
+ const elapsed = Date.now() - statementStartTime;
677
+ if (result.rows && result.metaData) {
678
+ results.push({
679
+ statement: i + 1,
680
+ rows: formatRows(result.rows, result.metaData),
681
+ elapsed: `${elapsed}ms`,
682
+ metaData: result.metaData,
683
+ updateCount: result.rows.length,
684
+ });
685
+ } else if (result.updateCount !== undefined) {
686
+ results.push({
687
+ statement: i + 1,
688
+ elapsed: `${elapsed}ms`,
689
+ updateCount: result.updateCount,
690
+ });
691
+ }
692
+ success++;
693
+ } catch (err) {
694
+ errors++;
695
+ console.error(`语句 ${i + 1} 错误:${err.message}`);
696
+ }
697
+ }
698
+
699
+ console.log(`${success} 成功,${errors} 失败`);
700
+ } else {
701
+ // 默认模式:事务执行
702
+ try {
703
+ for (let i = 0; i < statements.length; i++) {
704
+ const statementStartTime = Date.now();
705
+ const result = await conn.execute(statements[i]);
706
+ const elapsed = Date.now() - statementStartTime;
707
+ if (result.rows && result.metaData) {
708
+ results.push({
709
+ statement: i + 1,
710
+ rows: formatRows(result.rows, result.metaData),
711
+ elapsed: `${elapsed}ms`,
712
+ metaData: result.metaData,
713
+ updateCount: result.rows.length,
714
+ });
715
+ } else if (result.updateCount !== undefined) {
716
+ results.push({
717
+ statement: i + 1,
718
+ elapsed: `${elapsed}ms`,
719
+ updateCount: result.updateCount,
720
+ });
721
+ }
722
+ }
723
+ await conn.commit();
724
+ } catch (err) {
725
+ await conn.rollback();
726
+ throw err;
727
+ }
728
+ }
729
+
730
+ const totalElapsed = Date.now() - startTime;
731
+
732
+ // 输出结果
733
+ if (config.format === "json") {
734
+ // JSON 模式:新结构 [{rows, elapsed, data}, ...]
735
+ const resultOutput = results.map((r) => ({
736
+ rows: r.rows ? r.rows.length : 0,
737
+ elapsed: r.elapsed || `${totalElapsed}ms`,
738
+ data: r.rows || [],
739
+ }));
740
+ console.log(JSON.stringify(resultOutput, null, 2));
741
+ } else {
742
+ // 表格模式(默认)
743
+ for (let i = 0; i < results.length; i++) {
744
+ const r = results[i];
745
+ if (results.length > 1) {
746
+ console.log(`--- 语句 ${i + 1} ---`);
747
+ }
748
+ if (r.rows && r.rows.length > 0) {
749
+ console.table(r.rows);
750
+ console.log(`${r.rows.length} rows returned`);
751
+ } else {
752
+ // 空结果:显示空表格
753
+ console.table([]);
754
+ console.log("0 rows");
755
+ }
756
+ console.log(`Elapsed: ${r.elapsed || totalElapsed + "ms"}`);
757
+ }
758
+ }
759
+ }
760
+
761
+ // 分割 SQL 语句的辅助函数
762
+ function splitSqlStatements(sql) {
763
+ // 移除单行注释
764
+ let cleaned = sql.replace(/--.*$/gm, "");
765
+
766
+ // 按分号分割
767
+ const statements = cleaned
768
+ .split(";")
769
+ .map((s) => s.trim())
770
+ .filter((s) => s.length > 0);
771
+
772
+ return statements;
773
+ }
774
+
775
+ // 格式化行数据(处理长字符串截断)
776
+ function formatRows(rows, metaData) {
777
+ return rows.map((row) => {
778
+ const formattedRow = {};
779
+ // 支持数组和对象两种格式
780
+ // 达梦返回数组格式:row[0], row[1], ...
781
+ // MySQL 返回对象格式:row.columnName
782
+ if (Array.isArray(row)) {
783
+ for (let i = 0; i < row.length; i++) {
784
+ const col = metaData[i].name;
785
+ let val = row[i];
786
+ // 字符串截断到 50 字符
787
+ if (typeof val === "string" && val.length > 50) {
788
+ val = val.substring(0, 50) + "...";
789
+ }
790
+ formattedRow[col] = val;
791
+ }
792
+ } else {
793
+ // 对象格式
794
+ for (const colMeta of metaData) {
795
+ const col = colMeta.name;
796
+ let val = row[col];
797
+ if (typeof val === "string" && val.length > 50) {
798
+ val = val.substring(0, 50) + "...";
799
+ }
800
+ formattedRow[col] = val;
801
+ }
802
+ }
803
+ return formattedRow;
804
+ });
805
+ }
806
+
807
+ // ============================================
808
+ // 使用 cac 定义 CLI
809
+ // ============================================
810
+
811
+ const cli = cac("db-cli");
812
+
813
+ // 全局连接选项
814
+ cli.option(
815
+ "-c, --connection <string>",
816
+ "数据库连接字符串\n 达梦数据库:dm://user:pass@host:port\n MySQL: mysql://user:pass@host:port",
817
+ );
818
+
819
+ // 获取全局选项的辅助函数
820
+ function getConnectionStr() {
821
+ return cli.options.connection;
822
+ }
823
+
824
+ // 检查连接字符串的辅助函数
825
+ function checkConnectionStr() {
826
+ const connStr = getConnectionStr();
827
+ // 空字符串视为未提供连接
828
+ if (!connStr || connStr.trim() === "") {
829
+ console.error("错误:缺少必填参数 -c/--connection");
830
+ console.error("用法:db -c '<连接字符串>' <command> [options]");
831
+ console.error("");
832
+ console.error("示例:");
833
+ console.error(" # 达梦数据库");
834
+ console.error(
835
+ " db -c 'dm://SYSDBA:SYSDBA@10.50.8.44:5236' exec -q 'SELECT 1'",
836
+ );
837
+ console.error("");
838
+ console.error(" # MySQL");
839
+ console.error(
840
+ " db -c 'mysql://root:password@localhost:3306' exec -q 'SELECT 1'",
841
+ );
842
+ process.exit(1);
843
+ }
844
+ return connStr;
845
+ }
846
+
847
+ // import 子命令
848
+ cli
849
+ .command("import", "导入 SQL 文件到数据库")
850
+ .option("-f, --file <path>", "SQL 文件路径")
851
+ .option("-s, --schema <name>", "Schema/数据库名称")
852
+ .option("--continue-on-error", "遇到错误继续执行")
853
+ .alias("i")
854
+ .action(async (options) => {
855
+ const connStr = checkConnectionStr();
856
+ try {
857
+ const connInfo = parseConnectionString(connStr);
858
+ console.log(`连接数据库:${connInfo.host}:${connInfo.port}`);
859
+ console.log(`用户:${connInfo.username}`);
860
+ console.log(
861
+ `类型:${connInfo.type === "mysql" ? "MySQL" : "达梦数据库"}`,
862
+ );
863
+ console.log("");
864
+
865
+ const conn = await createConnection(connInfo);
866
+ console.log("数据库连接成功");
867
+
868
+ // MySQL 需要先选择数据库
869
+ if (conn.type === "mysql" && options.schema) {
870
+ await conn.raw.query(`USE \`${options.schema}\``);
871
+ console.log(`已选择数据库:${options.schema}`);
872
+ }
873
+ console.log("");
874
+
875
+ await runImport(conn, options);
876
+
877
+ await conn.close();
878
+ } catch (err) {
879
+ console.error("错误:", err.message);
880
+ process.exit(1);
881
+ }
882
+ });
883
+
884
+ // export 子命令
885
+ cli
886
+ .command("export", "从数据库导出数据/表结构")
887
+ .option("-s, --schema <name>", "Schema 名称")
888
+ .option("-t, --table <name>", "导出单表")
889
+ .option("-T, --tables-list <list>", "导出多表 (逗号分隔)")
890
+ .option("-q, --query <sql>", "自定义查询导出")
891
+ .option("-o, --output <file>", "输出文件路径")
892
+ .option("--type <type>", "导出类型:schema|data|all", { default: "all" })
893
+ .alias("e")
894
+ .action(async (options) => {
895
+ const connStr = checkConnectionStr();
896
+ try {
897
+ const connInfo = parseConnectionString(connStr);
898
+ console.log(`连接数据库:${connInfo.host}:${connInfo.port}`);
899
+ console.log(`用户:${connInfo.username}`);
900
+ console.log(
901
+ `类型:${connInfo.type === "mysql" ? "MySQL" : "达梦数据库"}`,
902
+ );
903
+ console.log("");
904
+
905
+ const conn = await createConnection(connInfo);
906
+ console.log("数据库连接成功");
907
+ console.log("");
908
+
909
+ await runExport(conn, options);
910
+
911
+ await conn.close();
912
+ } catch (err) {
913
+ console.error("错误:", err.message);
914
+ process.exit(1);
915
+ }
916
+ });
917
+
918
+ // exec 子命令
919
+ cli
920
+ .command("exec", "执行 SQL 语句")
921
+ .option("-q, --query <sql>", "SQL 语句")
922
+ .option("--format <format>", "输出格式:json|table", { default: "table" })
923
+ .option("--continue-on-error", "遇到错误继续执行")
924
+ .alias("x")
925
+ .action(async (options) => {
926
+ const connStr = checkConnectionStr();
927
+ try {
928
+ const connInfo = parseConnectionString(connStr);
929
+ console.log(`连接数据库:${connInfo.host}:${connInfo.port}`);
930
+ console.log(`用户:${connInfo.username}`);
931
+ console.log(
932
+ `类型:${connInfo.type === "mysql" ? "MySQL" : "达梦数据库"}`,
933
+ );
934
+ console.log("");
935
+
936
+ const conn = await createConnection(connInfo);
937
+ console.log("数据库连接成功");
938
+ console.log("");
939
+
940
+ await runExec(conn, options);
941
+
942
+ await conn.close();
943
+ } catch (err) {
944
+ console.error("错误:", err.message);
945
+ process.exit(1);
946
+ }
947
+ });
948
+
949
+ // 帮助信息(cac 自动生成)
950
+ cli.help();
951
+
952
+ // Only run CLI when executed directly (not when imported as module)
953
+ const isMainModule = process.argv[1] && process.argv[1].endsWith("db-cli.js");
954
+ if (isMainModule) {
955
+ // 当没有任何参数时,显示主帮助
956
+ const rawArgs = process.argv.slice(2);
957
+ if (rawArgs.length === 0) {
958
+ cli.outputHelp();
959
+ process.exit(0);
960
+ }
961
+
962
+ // 未知命令检测 - 在解析前检查第一个参数是否为已知命令
963
+ const firstArg = rawArgs[0];
964
+ if (!firstArg.startsWith("-")) {
965
+ const knownCommands = [
966
+ "import",
967
+ "i",
968
+ "export",
969
+ "e",
970
+ "exec",
971
+ "x",
972
+ "help",
973
+ "--help",
974
+ "-h",
975
+ ];
976
+ if (!knownCommands.includes(firstArg)) {
977
+ console.error(`错误:不识别的命令 '${firstArg}'`);
978
+ console.error("可用命令:import (i), export (e), exec (x)");
979
+ console.error("使用 <命令> --help 查看具体命令的帮助");
980
+ console.error("示例:db exec --help");
981
+ process.exit(1);
982
+ }
983
+ }
984
+
985
+ // 解析命令行参数 - 捕获未知选项错误
986
+ try {
987
+ cli.parse();
988
+ } catch (err) {
989
+ if (err.message && err.message.includes("Unknown option")) {
990
+ const match = err.message.match(/Unknown option `([^`]+)`/);
991
+ const unknownOpt = match ? match[1] : "unknown";
992
+ console.error(`不识别的选项 '${unknownOpt}'`);
993
+ process.exit(1);
994
+ }
995
+ throw err;
996
+ }
997
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@xiaokexiang/db-cli",
3
+ "version": "0.2.1",
4
+ "type": "module",
5
+ "main": "db-cli.js",
6
+ "scripts": {
7
+ "db-cli": "node --openssl-legacy-provider db-cli.js"
8
+ },
9
+ "bin": {
10
+ "db-cli": "./bin.cjs"
11
+ },
12
+ "keywords": [
13
+ "database",
14
+ "cli",
15
+ "sql",
16
+ "mysql",
17
+ "dameng"
18
+ ],
19
+ "author": "xiaokexiang",
20
+ "license": "ISC",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/xiaokexiang/db-cli.git"
24
+ },
25
+ "description": "多数据库 CLI 工具 - 支持 MySQL 和达梦数据库",
26
+ "dependencies": {
27
+ "cac": "^7.0.0",
28
+ "dmdb": "^1.0.48286",
29
+ "mysql2": "^3.20.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=12"
33
+ },
34
+ "os": [
35
+ "win32",
36
+ "linux",
37
+ "darwin"
38
+ ]
39
+ }