init
This commit is contained in:
parent
20bff2b6cd
commit
55ff56a1a7
149
README.md
149
README.md
@ -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大类常见迁移问题
|
||||
|
||||
@ -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',
|
||||
|
||||
214
converter.js
214
converter.js
@ -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(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行主函数
|
||||
|
||||
Loading…
Reference in New Issue
Block a user