pg2dm-converter/dm-executor.js
dengqichen cd7788de95 init
2025-11-15 17:29:39 +08:00

391 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 达梦数据库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;