diff --git a/README.md b/README.md index 706ddb8..1fa1f0e 100644 --- a/README.md +++ b/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大类常见迁移问题 diff --git a/config.js b/config.js index b8f60b8..87e488d 100644 --- a/config.js +++ b/config.js @@ -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', diff --git a/converter.js b/converter.js index 75d09f0..b58185c 100644 --- a/converter.js +++ b/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 [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 # 转换单个文件 + node converter.js # 批量转换指定目录 + node converter.js # 指定输出文件 + +示例: + 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); + } } // 运行主函数