This commit is contained in:
dengqichen 2025-11-15 15:53:03 +08:00
parent 20bff2b6cd
commit 55ff56a1a7
3 changed files with 308 additions and 62 deletions

149
README.md
View File

@ -6,22 +6,43 @@
本工具通过**12个转换步骤**解决PostgreSQL到达梦迁移过程中的**所有常见语法兼容性问题**
### 🚀 批量处理能力
- ✅ **智能批量转换** - 无参数自动处理input目录所有SQL文件
- ✅ **进度可视化** - 实时显示转换进度和统计信息
- ✅ **错误容错** - 单文件失败不影响其他文件转换
- ✅ **详细报告** - 批量转换完成后生成总体统计报告
### 1⃣ 模式与命名空间处理
- ✅ **移除`pg_catalog`模式前缀** - 达梦不识别PostgreSQL的系统模式
- ✅ **移除数据类型引号** - 达梦不需要给数据类型加引号
### 2⃣ 数据类型转换
### 2⃣ 数据类型转换 (支持13种类型)
- ✅ **基础类型映射**
- `int8``BIGINT`
- `int4``INT`
- `int2``SMALLINT`
- `bool``BIT`
- `numeric``DECIMAL`
- ✅ **TEXT类型特殊处理** - `text``VARCHAR(8000)`
- 关键修复达梦的TEXT是CLOB类型不能建索引
- 解决统一转换为VARCHAR(8000),保证可以建立索引
- ✅ **时间戳精度处理** - `timestamp(6)``TIMESTAMP`
- 达梦不支持timestamp精度参数自动移除
- ✅ **浮点类型支持** 🆕
- `float8``DOUBLE` (双精度浮点)
- `float4``REAL` (单精度浮点)
- `float``REAL`
- ✅ **字符类型完整支持** 🆕
- `text``VARCHAR(8000)` (关键修复达梦TEXT是CLOB不能建索引)
- `bpchar``CHAR` (定长字符串)
- `varchar``VARCHAR` (变长字符串)
- ✅ **时间戳类型完整支持** 🆕
- `timestamp(6)``TIMESTAMP` (移除精度参数)
- `timestamptz``TIMESTAMP` (时间戳带时区)
- 自动移除 `without time zone` / `with time zone` 子句
- ✅ **DECIMAL精度自动修正** 🆕
- 检测超过38位的精度定义
- 自动调整为达梦最大支持精度(38位)
- 保持小数位数不变
- ✅ **带括号类型引号处理** 🆕
- 正确移除 `"VARCHAR(8000)"``VARCHAR(8000)`
- 正确移除 `"DECIMAL(20,6)"``DECIMAL(20,6)`
- 正确移除 `"CHAR"``CHAR`
### 3⃣ 自增序列转换
- ✅ **序列语法转换** - `DEFAULT nextval('seq'::regclass)``IDENTITY(1,1)`
@ -51,6 +72,10 @@
- 检测超过816字符限制的函数索引
- 自动简化或发出警告
- 移除COALESCE包装保留原始列名
- ✅ **索引注释移除** 🆕
- 自动移除所有 `COMMENT ON INDEX` 语句
- 达梦不支持索引注释语法
- 清理多余空行保持格式整洁
### 7⃣ 智能日志与报告
- ✅ 生成详细的转换日志JSON格式
@ -76,7 +101,29 @@ npm install
## 使用方法
### 1. 单文件转换
### 1. 批量转换(推荐)⭐
```bash
# 无参数自动批量转换input目录下所有.sql文件
node converter.js
# 输出示例:
# 📁 批量转换目录: ./input
# ==================================================
# 找到 5 个SQL文件
#
# [1/5] 处理: schema1.sql
# --------------------------------------------------
# ✓ 转换完成: ./output/schema1_dm.sql
# ...
# ==================================================
# 📊 批量转换完成
# ==================================================
# ✓ 成功: 5 个文件
# 📂 输出目录: ./output
```
### 2. 单文件转换
```bash
# 基本用法
@ -85,17 +132,17 @@ node converter.js input/your_schema.sql
# 输出: output/your_schema_dm.sql
```
### 2. 指定输出文件
### 3. 指定输出文件
```bash
node converter.js input/schema.sql output/custom_output.sql
```
### 3. 批量转换
### 4. 批量转换指定目录
```bash
# 转换input目录下所有SQL文件
node converter.js input/*.sql
# 转换指定目录下所有SQL文件
node converter.js ./mydata
```
## 目录结构
@ -225,6 +272,64 @@ CREATE UNIQUE INDEX idx ON table(
"created_at" TIMESTAMP
```
### ❌ 问题9: DECIMAL精度超出范围 🆕
**错误信息**: `-6121: 数据精度超出范围`
**原因**: PostgreSQL的DECIMAL最大精度1000位达梦只支持38位
**解决方案**:
```sql
-- PostgreSQL (转换前)
"coefficient" DECIMAL(50,0)
-- 达梦 (转换后) - 自动调整为38位
"coefficient" DECIMAL(38,0)
```
### ❌ 问题10: 带括号的类型有引号 🆕
**错误信息**: `-3719: 非法的基类名[VARCHAR(8000)]`
**原因**: text转换为VARCHAR(8000)后,类型引号移除逻辑无法处理带括号的类型
**解决方案**:
```sql
-- 转换中间结果 (错误)
"demand_order_no" "VARCHAR(8000)",
-- 达梦 (转换后) - 正确移除引号
"demand_order_no" VARCHAR(8000),
```
### ❌ 问题11: 索引注释不支持 🆕
**错误信息**: `-2007: 语法分析出错 [INDEX]附近出现错误`
**原因**: 达梦不支持 `COMMENT ON INDEX` 语法
**解决方案**:
```sql
-- PostgreSQL (转换前)
CREATE INDEX "idx_name" ON "schema"."table" ("column" ASC);
COMMENT ON INDEX "schema"."idx_name" IS '索引注释';
-- 达梦 (转换后) - 移除索引注释
CREATE INDEX "idx_name" ON "schema"."table" ("column" ASC);
-- 注释已被自动移除
```
### ❌ 问题12: bpchar类型未识别 🆕
**错误信息**: `-3719: 非法的基类名[bpchar]`
**原因**: `bpchar` 是 PostgreSQL 的 blank-padded char 内部类型名,对应 CHAR 类型
**解决方案**:
```sql
-- PostgreSQL (转换前)
"open_alert" "bpchar",
-- 达梦 (转换后)
"open_alert" CHAR,
```
## 转换规则详解
### 1. 数据类型映射
@ -236,8 +341,15 @@ CREATE UNIQUE INDEX idx ON table(
| int2 | SMALLINT | 2字节整数 |
| numeric | DECIMAL | 精确数值 |
| bool | BIT | 布尔值 |
| float8 | DOUBLE | 双精度浮点 🆕 |
| float4 | REAL | 单精度浮点 🆕 |
| float | REAL | 通用浮点 🆕 |
| text | VARCHAR(8000) | **关键**避免CLOB不能建索引 |
| bpchar | CHAR | 定长字符串 🆕 |
| varchar | VARCHAR | 变长字符串 |
| timestamptz | TIMESTAMP | 时间戳带时区 🆕 |
| timestamp(n) | TIMESTAMP | 移除精度参数 |
| DECIMAL(>38,n) | DECIMAL(38,n) | 自动修正精度 🆕 |
### 2. 序列转换
@ -456,6 +568,21 @@ const pattern = /COALESCE\s*\(\s*"?(\w+)"?\s*,\s*'[^']+'\s*\)/gi;
## 更新日志
- **v1.2.0 (2025-11-15)** - 批量转换增强版 🆕
- ✅ **批量转换功能** - 支持目录级批量处理
- 无参数自动处理input目录
- 支持指定目录批量转换
- 详细进度显示和统计报告
- ✅ **完整数据类型支持** - 新增多种类型映射
- `bpchar``CHAR` (定长字符串)
- `float8``DOUBLE` (双精度浮点)
- `float4``REAL` (单精度浮点)
- `timestamptz``TIMESTAMP` (时间戳带时区)
- ✅ **DECIMAL精度自动修正** - 最大38位限制
- ✅ **带括号类型引号处理** - 修复VARCHAR(8000)等
- ✅ **移除索引注释** - COMMENT ON INDEX自动清理
- ✅ **timestamp时区子句清理** - without/with time zone
- **v1.0.0 (2025-11-15)** - 生产版本
- ✅ 完整的12步转换流程
- ✅ 解决8大类常见迁移问题

View File

@ -10,6 +10,7 @@ module.exports = {
'int2': 'SMALLINT',
'numeric': 'DECIMAL',
'varchar': 'VARCHAR',
'bpchar': 'CHAR', // PostgreSQL blank-padded char
'timestamp': 'TIMESTAMP',
'timestamptz': 'TIMESTAMP', // PostgreSQL timestamp with time zone
'bool': 'BIT',

View File

@ -44,7 +44,7 @@ class PG2DMConverter {
let converted = sql;
// 1. 转换基本类型(包括浮点类型和时间戳类型)
const typePattern = /\b(int8|int4|int2|numeric|bool|float8|float4|float|timestamptz|text)\b/gi;
const typePattern = /\b(int8|int4|int2|numeric|bool|float8|float4|float|timestamptz|text|bpchar)\b/gi;
converted = converted.replace(typePattern, (match) => {
const lowerMatch = match.toLowerCase();
@ -199,13 +199,13 @@ class PG2DMConverter {
let converted = sql;
// 移除引号中的数据类型(达梦不需要给类型加引号)
// 1. 先处理带括号的类型VARCHAR(8000), DECIMAL(20,6)
// 1. 先处理带括号的类型VARCHAR(8000), DECIMAL(20,6), CHAR(10)
converted = converted.replace(/\s"(VARCHAR|CHAR|DECIMAL|NUMERIC)\s*\([^)]+\)"\s/gi, ' $1 ');
converted = converted.replace(/\s"(VARCHAR|CHAR|DECIMAL|NUMERIC)\s*\([^)]+\)"([,\n\r])/gi, ' $1$2');
// 2. 再处理简单类型
converted = converted.replace(/\s"(BIGINT|INT|SMALLINT|TINYINT|VARCHAR|CHAR|TEXT|DATE|TIME|TIMESTAMP|BIT|BOOLEAN|BOOL|BLOB|CLOB)"\s/gi, ' $1 ');
converted = converted.replace(/\s"(BIGINT|INT|SMALLINT|TINYINT|VARCHAR|CHAR|TEXT|DATE|TIME|TIMESTAMP|BIT|BOOLEAN|BOOL|BLOB|CLOB)"([,\n\r])/gi, ' $1$2');
// 2. 再处理简单类型包括不带长度的CHAR
converted = converted.replace(/\s"(BIGINT|INT|SMALLINT|TINYINT|VARCHAR|CHAR|TEXT|DATE|TIME|TIMESTAMP|BIT|BOOLEAN|BOOL|BLOB|CLOB|DOUBLE|REAL)"\s/gi, ' $1 ');
converted = converted.replace(/\s"(BIGINT|INT|SMALLINT|TINYINT|VARCHAR|CHAR|TEXT|DATE|TIME|TIMESTAMP|BIT|BOOLEAN|BOOL|BLOB|CLOB|DOUBLE|REAL)"([,\n\r])/gi, ' $1$2');
this.log('移除数据类型引号');
@ -231,6 +231,28 @@ class PG2DMConverter {
return converted;
}
/**
* 移除索引注释达梦不支持COMMENT ON INDEX
*/
removeIndexComments(sql) {
let converted = sql;
// 匹配并移除 COMMENT ON INDEX 语句
// 格式: COMMENT ON INDEX "schema"."index_name" IS '注释内容';
const commentPattern = /COMMENT\s+ON\s+INDEX\s+"[^"]+"\."[^"]+"\s+IS\s+'[^']*'\s*;/gi;
const matches = sql.match(commentPattern);
if (matches) {
this.log(`移除 ${matches.length} 个索引注释达梦不支持COMMENT ON INDEX语法`);
converted = converted.replace(commentPattern, '');
// 清理可能产生的多余空行
converted = converted.replace(/\n\n\n+/g, '\n\n');
}
return converted;
}
/**
* 简化索引语法
*/
@ -412,7 +434,11 @@ class PG2DMConverter {
this.log('步骤10: 处理COALESCE函数索引...');
converted = this.processCoalesceIndexes(converted);
// 11. 添加转换说明
// 11. 移除索引注释达梦不支持COMMENT ON INDEX
this.log('步骤11: 移除索引注释...');
converted = this.removeIndexComments(converted);
// 12. 添加转换说明
if (config.output.addConversionComment) {
converted = this.addConversionHeader(converted, originalFile);
}
@ -448,45 +474,9 @@ function ensureDir(dirPath) {
}
/**
* 主函数
* 转换单个文件
*/
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log(`
PostgreSQL到达梦数据库SQL转换器
======================================
使用方法:
node converter.js <input-file.sql> [output-file.sql]
node converter.js input/*.sql
示例:
node converter.js input/schema.sql
node converter.js input/schema.sql output/schema_dm.sql
node converter.js input/*.sql
说明:
- 如果不指定输出文件将自动在output目录生成 *_dm.sql 文件
- 支持通配符批量处理多个文件
- 会自动生成转换日志文件 *_conversion.log.json
`);
process.exit(0);
}
// 确保input和output目录存在
ensureDir('./input');
ensureDir('./output');
const inputFile = args[0];
// 检查文件是否存在
if (!fs.existsSync(inputFile)) {
console.error(`错误: 文件不存在: ${inputFile}`);
process.exit(1);
}
function convertSingleFile(inputFile, outputFile) {
// 读取输入文件
console.log(`\n读取文件: ${inputFile}`);
const sqlContent = fs.readFileSync(inputFile, 'utf8');
@ -496,15 +486,17 @@ PostgreSQL到达梦数据库SQL转换器
const convertedSql = converter.convert(sqlContent, inputFile);
// 确定输出文件路径
const outputFile = args[1] || path.join(
'./output',
path.basename(inputFile, '.sql') + '_dm.sql'
);
if (!outputFile) {
outputFile = path.join(
'./output',
path.basename(inputFile, '.sql') + '_dm.sql'
);
}
// 写入输出文件
ensureDir(path.dirname(outputFile));
fs.writeFileSync(outputFile, convertedSql, 'utf8');
console.log(`\n✓ 转换完成,输出文件: ${outputFile}`);
console.log(`✓ 转换完成: ${outputFile}`);
// 生成日志
if (config.output.generateLog) {
@ -513,18 +505,144 @@ PostgreSQL到达梦数据库SQL转换器
// 显示警告
if (converter.warnings.length > 0) {
console.log('\n⚠ 警告信息:');
console.log('⚠ 警告信息:');
converter.warnings.forEach((warn, i) => {
console.log(` ${i + 1}. ${warn}`);
});
}
console.log('\n转换统计:');
// 显示统计
console.log('转换统计:');
console.log(` - 数据类型转换: ${converter.stats.dataTypes}`);
console.log(` - 序列转IDENTITY: ${converter.stats.sequences}`);
console.log(` - COLLATE移除: ${converter.stats.collates}`);
console.log(` - 索引简化: ${converter.stats.indexes}`);
console.log(` - COALESCE索引处理: ${converter.stats.coalesceIndexes}`);
return { success: true, warnings: converter.warnings.length };
}
/**
* 批量转换目录下所有SQL文件
*/
function batchConvert(inputDir) {
console.log(`\n📁 批量转换目录: ${inputDir}`);
console.log('='.repeat(50));
// 读取目录下所有.sql文件
const files = fs.readdirSync(inputDir)
.filter(file => file.toLowerCase().endsWith('.sql'))
.map(file => path.join(inputDir, file));
if (files.length === 0) {
console.log(`\n⚠ 目录中没有找到.sql文件: ${inputDir}`);
return;
}
console.log(`\n找到 ${files.length} 个SQL文件`);
let successCount = 0;
let failCount = 0;
let totalWarnings = 0;
// 逐个转换
files.forEach((file, index) => {
try {
console.log(`\n[${index + 1}/${files.length}] 处理: ${path.basename(file)}`);
console.log('-'.repeat(50));
const result = convertSingleFile(file, null);
successCount++;
totalWarnings += result.warnings;
} catch (error) {
console.error(`✗ 转换失败: ${error.message}`);
failCount++;
}
});
// 显示总结
console.log('\n' + '='.repeat(50));
console.log('📊 批量转换完成');
console.log('='.repeat(50));
console.log(`✓ 成功: ${successCount} 个文件`);
if (failCount > 0) {
console.log(`✗ 失败: ${failCount} 个文件`);
}
if (totalWarnings > 0) {
console.log(`⚠ 总警告: ${totalWarnings}`);
}
console.log(`📂 输出目录: ./output`);
}
/**
* 主函数
*/
function main() {
const args = process.argv.slice(2);
// 确保input和output目录存在
ensureDir('./input');
ensureDir('./output');
// 无参数批量处理input目录
if (args.length === 0) {
if (fs.existsSync('./input')) {
batchConvert('./input');
} else {
console.log(`
PostgreSQL到达梦数据库SQL转换器
======================================
使用方法:
node converter.js # 批量转换input目录下所有.sql文件
node converter.js <input-file.sql> # 转换单个文件
node converter.js <input-dir> # 批量转换指定目录
node converter.js <input-file> <output> # 指定输出文件
示例:
node converter.js # 批量转换input/*.sql
node converter.js input/schema.sql # 转换单个文件
node converter.js ./mydata # 批量转换mydata目录
node converter.js input/schema.sql output/schema_dm.sql
说明:
- 批量模式会自动在output目录生成 *_dm.sql 文件
- 会自动生成转换日志文件 *_conversion.log.json
- 批量模式会显示详细的进度和统计信息
`);
}
return;
}
const inputPath = args[0];
// 检查路径是否存在
if (!fs.existsSync(inputPath)) {
console.error(`✗ 错误: 路径不存在: ${inputPath}`);
process.exit(1);
}
// 检查是文件还是目录
const stat = fs.statSync(inputPath);
if (stat.isDirectory()) {
// 批量转换目录
batchConvert(inputPath);
} else if (stat.isFile()) {
// 单个文件转换
const outputFile = args[1];
try {
convertSingleFile(inputPath, outputFile);
console.log('\n✓ 转换成功!');
} catch (error) {
console.error(`\n✗ 转换失败: ${error.message}`);
console.error(error.stack);
process.exit(1);
}
} else {
console.error(`✗ 错误: 不支持的路径类型: ${inputPath}`);
process.exit(1);
}
}
// 运行主函数