391 lines
11 KiB
JavaScript
391 lines
11 KiB
JavaScript
/**
|
||
* 达梦数据库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 <sql-files>
|
||
|
||
示例:
|
||
# 执行单个文件
|
||
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;
|