/** * 达梦数据库SQL自动执行器 * 基于disql命令行工具,零额外依赖 */ const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); const iconv = require('iconv-lite'); class DMExecutor { constructor(configFile = './db-mapping.json') { this.config = JSON.parse(fs.readFileSync(configFile, 'utf8')); this.disqlPath = this.findDisql(); this.stats = []; } /** * 查找disql工具路径 */ findDisql() { const possiblePaths = [ 'D:\\sortware\\dm_manager\\bin\\disql.exe', 'D:\\dmdbms\\bin\\disql.exe', 'C:\\dmdbms\\bin\\disql.exe', 'disql' // PATH中 ]; for (const p of possiblePaths) { if (p === 'disql' || fs.existsSync(p)) { return p; } } throw new Error('未找到disql工具,请确认达梦数据库已安装'); } /** * 检测SQL文件中的schema */ detectSchema(sqlContent) { const match = sqlContent.match(/"([^"]+)"\./); return match ? match[1] : null; } /** * 获取schema对应的端口 */ getPort(schema) { if (!schema) return this.config.defaultPort; const mapping = this.config.schemaMappings[schema]; return mapping ? mapping.port : this.config.defaultPort; } /** * 使用disql执行SQL文件 */ async executeSQL(sqlFile) { const sqlContent = fs.readFileSync(sqlFile, 'utf8'); const schema = this.detectSchema(sqlContent); const port = this.getPort(schema); const { host, user, password } = this.config.defaultConnection; console.log(`\n${'='.repeat(70)}`); console.log(`📂 执行: ${path.basename(sqlFile)}`); console.log(`📋 Schema: ${schema || '(未检测到)'}`); console.log(`🎯 端口: ${port}`); console.log('='.repeat(70)); const startTime = Date.now(); return new Promise((resolve) => { const absoluteSqlFile = path.resolve(sqlFile); console.log(`🔗 连接信息: ${user}@${host}:${port}`); console.log(`📄 SQL文件: ${absoluteSqlFile}`); console.log(`⏳ 执行中...`); // 使用交互式方式,通过stdin传递密码,避免命令行参数中@符号的解析问题 const connectionString = `${user}@${host}:${port}`; console.log(`📝 连接参数: ${connectionString}`); // 启动disql,通过stdin传递密码和SQL命令 const disql = spawn(this.disqlPath, [connectionString], { shell: true, stdio: ['pipe', 'pipe', 'pipe'] // 启用stdin以传递密码 }); // 直接读取SQL文件内容并通过stdin执行 // 避免@file.sql路径解析问题 const sqlCommands = sqlContent; // 构建完整的命令序列 const commands = `${password}\nSET TIMING ON;\nSET FEEDBACK ON;\n${sqlCommands}\nEXIT\n`; console.log(`📤 发送SQL内容: ${sqlCommands.split('\n').length} 行`); console.log(`📋 SQL大小: ${(commands.length / 1024).toFixed(2)} KB`); // 写入命令到stdin disql.stdin.write(commands, 'utf8', (err) => { if (err) { console.error('❌ 写入stdin失败:', err); } }); disql.stdin.end(); let stdout = ''; let stderr = ''; let lastOutput = Date.now(); // 心跳检测 - 每秒显示一个点 const heartbeat = setInterval(() => { process.stdout.write('.'); }, 1000); // 超时检测 - 5分钟无输出则认为超时 const timeout = setTimeout(() => { clearInterval(heartbeat); disql.kill(); console.log('\n❌ 超时:5分钟无响应'); }, 300000); disql.stdout.on('data', (data) => { // 将GBK编码转换为UTF-8 const text = iconv.decode(data, 'gbk'); stdout += text; lastOutput = Date.now(); // 实时显示关键信息 const keywords = ['执行成功', '执行失败', '行受影响', '影响行数', 'CREATE TABLE', 'CREATE INDEX', 'ALTER TABLE', '已用时间']; if (keywords.some(keyword => text.includes(keyword))) { // 显示包含关键字的行 const lines = text.split('\n').filter(line => keywords.some(keyword => line.includes(keyword)) ); lines.forEach(line => { if (line.trim()) { process.stdout.write(`\n ${line.trim().substring(0, 120)}`); } }); } }); disql.stderr.on('data', (data) => { // 将GBK编码转换为UTF-8 const text = iconv.decode(data, 'gbk'); stderr += text; // 显示错误(过滤掉正常的密码提示) if (text.trim() && !text.includes('密码:')) { process.stdout.write(`\n⚠ ${text.trim().substring(0, 150)}`); } }); disql.on('close', (code) => { clearInterval(heartbeat); clearTimeout(timeout); const duration = ((Date.now() - startTime) / 1000).toFixed(2); console.log('\n'); const result = { file: path.basename(sqlFile), schema: schema, port: port, duration: duration, success: code === 0, exitCode: code, output: stdout, error: stderr }; this.stats.push(result); this.printResult(result); resolve(result); }); disql.on('error', (error) => { clearInterval(heartbeat); clearTimeout(timeout); const result = { file: path.basename(sqlFile), schema: schema, port: port, duration: 0, success: false, error: error.message }; this.stats.push(result); resolve(result); }); }); } /** * 批量执行SQL文件 */ async executeBatch(sqlFiles) { console.log('\n' + '='.repeat(70)); console.log('🚀 达梦数据库批量执行器'); console.log('='.repeat(70)); console.log(`📂 文件数: ${sqlFiles.length}`); console.log(`🌐 服务器: ${this.config.defaultConnection.host}`); console.log(`🔧 工具: ${this.disqlPath}`); console.log('='.repeat(70)); const overallStart = Date.now(); for (let i = 0; i < sqlFiles.length; i++) { console.log(`\n[${i + 1}/${sqlFiles.length}]`); await this.executeSQL(sqlFiles[i]); } const overallDuration = ((Date.now() - overallStart) / 1000).toFixed(2); this.printSummary(overallDuration); } /** * 打印单个文件执行结果 */ printResult(result) { console.log('-'.repeat(70)); if (result.success) { console.log(`✅ ${result.file} 执行成功`); } else { console.log(`❌ ${result.file} 执行失败`); if (result.error) { console.log(`错误: ${result.error.substring(0, 200)}`); } } console.log(`端口: ${result.port} | 耗时: ${result.duration}秒`); console.log('-'.repeat(70)); } /** * 打印总体统计 */ printSummary(duration) { console.log('\n' + '='.repeat(70)); console.log('📊 执行统计'); console.log('='.repeat(70)); const total = this.stats.length; const success = this.stats.filter(s => s.success).length; const failed = total - success; console.log(`总文件数: ${total}`); console.log(`✅ 成功: ${success}`); console.log(`❌ 失败: ${failed}`); console.log(`⏱ 总耗时: ${duration}秒`); // 按端口分组 const portStats = {}; this.stats.forEach(s => { if (!portStats[s.port]) { portStats[s.port] = { total: 0, success: 0, failed: 0 }; } portStats[s.port].total++; if (s.success) portStats[s.port].success++; else portStats[s.port].failed++; }); console.log('\n按端口统计:'); Object.keys(portStats).sort().forEach(port => { const stat = portStats[port]; console.log(` 端口 ${port}: ${stat.total}个文件 (✅${stat.success} ❌${stat.failed})`); }); // 显示失败的文件 const failedFiles = this.stats.filter(s => !s.success); if (failedFiles.length > 0) { console.log('\n失败的文件:'); failedFiles.forEach(f => { console.log(` ❌ ${f.file} - ${f.error || '执行失败'}`); }); } // 保存报告 this.saveReport(); console.log('='.repeat(70)); } /** * 保存执行报告 */ saveReport() { const reportFile = path.join('./output', `execution_report_${Date.now()}.json`); const report = { timestamp: new Date().toISOString(), tool: 'disql', server: this.config.defaultConnection.host, summary: { total: this.stats.length, success: this.stats.filter(s => s.success).length, failed: this.stats.filter(s => !s.success).length }, details: this.stats }; fs.writeFileSync(reportFile, JSON.stringify(report, null, 2), 'utf8'); console.log(`\n📄 详细报告: ${reportFile}`); } } /** * 主函数 */ async function main() { const args = process.argv.slice(2); if (args.length === 0) { console.log(` 达梦数据库SQL执行器 ====================================== 基于disql命令行工具,零额外依赖 使用方法: node dm-executor.js 示例: # 执行单个文件 node dm-executor.js output/schema_dm.sql # 批量执行 node dm-executor.js output/*_dm.sql # 执行所有文件 node dm-executor.js output/*.sql 前提条件: - 达梦数据库已安装 - disql工具可用 - db-mapping.json已配置 配置文件: db-mapping.json - 数据库连接和schema映射配置 `); process.exit(0); } // 解析SQL文件列表 const sqlFiles = []; args.forEach(arg => { if (arg.includes('*')) { // 通配符展开 const dir = path.dirname(arg); const pattern = path.basename(arg).replace(/\*/g, '.*'); const regex = new RegExp(`^${pattern}$`); if (fs.existsSync(dir)) { const files = fs.readdirSync(dir) .filter(f => regex.test(f) && f.endsWith('.sql')) .map(f => path.join(dir, f)); sqlFiles.push(...files); } } else if (fs.existsSync(arg)) { sqlFiles.push(arg); } else { console.error(`❌ 文件不存在: ${arg}`); } }); if (sqlFiles.length === 0) { console.error('❌ 未找到SQL文件'); process.exit(1); } // 执行 try { const executor = new DMExecutor(); await executor.executeBatch(sqlFiles); const failed = executor.stats.filter(s => !s.success).length; process.exit(failed > 0 ? 1 : 0); } catch (error) { console.error('\n❌ 执行失败:', error.message); console.error(error.stack); process.exit(1); } } if (require.main === module) { main(); } module.exports = DMExecutor;