Compare commits
No commits in common. "develop1" and "master" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
*.log
|
*.log
|
||||||
|
output/*.sql
|
||||||
|
output/*.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
328
README.md
328
README.md
@ -2,70 +2,15 @@
|
|||||||
|
|
||||||
自动将PostgreSQL导出的SQL文件转换为达梦数据库(DM8)兼容的SQL语法。
|
自动将PostgreSQL导出的SQL文件转换为达梦数据库(DM8)兼容的SQL语法。
|
||||||
|
|
||||||
## 核心功能特性
|
## 功能特性
|
||||||
|
|
||||||
本工具通过**12个转换步骤**,解决PostgreSQL到达梦迁移过程中的**所有常见语法兼容性问题**:
|
- ✅ 自动转换数据类型 (int8→BIGINT, int4→INT, int2→SMALLINT等)
|
||||||
|
- ✅ 序列(SEQUENCE)转换为IDENTITY自增列
|
||||||
### 1️⃣ 模式与命名空间处理
|
- ✅ 移除PostgreSQL特有的COLLATE子句
|
||||||
- ✅ **移除`pg_catalog`模式前缀** - 达梦不识别PostgreSQL的系统模式
|
- ✅ 简化索引语法 (移除USING btree, 操作符类等)
|
||||||
- ✅ **移除数据类型引号** - 达梦不需要给数据类型加引号
|
- ✅ 智能处理COALESCE函数索引
|
||||||
|
- ✅ 生成详细的转换日志
|
||||||
### 2️⃣ 数据类型转换
|
- ✅ 警告复杂索引可能超过达梦816字符限制
|
||||||
- ✅ **基础类型映射**
|
|
||||||
- `int8` → `BIGINT`
|
|
||||||
- `int4` → `INT`
|
|
||||||
- `int2` → `SMALLINT`
|
|
||||||
- `bool` → `BIT`
|
|
||||||
- `numeric` → `DECIMAL`
|
|
||||||
- ✅ **TEXT类型特殊处理** - `text` → `VARCHAR(8000)`
|
|
||||||
- 关键修复:达梦的TEXT是CLOB类型,不能建索引
|
|
||||||
- 解决:统一转换为VARCHAR(8000),保证可以建立索引
|
|
||||||
- ✅ **时间戳精度处理** - `timestamp(6)` → `TIMESTAMP`
|
|
||||||
- 达梦不支持timestamp精度参数,自动移除
|
|
||||||
|
|
||||||
### 3️⃣ 自增序列转换
|
|
||||||
- ✅ **序列语法转换** - `DEFAULT nextval('seq'::regclass)` → `IDENTITY(1,1)`
|
|
||||||
- 自动识别并转换所有序列默认值
|
|
||||||
- 支持各种序列命名格式
|
|
||||||
|
|
||||||
### 4️⃣ PostgreSQL特有语法清理
|
|
||||||
- ✅ **类型转换清理** - 移除 `::regclass`, `::character varying`, `::integer` 等
|
|
||||||
- ✅ **COLLATE子句清理** - 移除所有格式的COLLATE子句
|
|
||||||
- `COLLATE "pg_catalog"."default"`
|
|
||||||
- `COLLATE "default"`
|
|
||||||
- `COLLATE pg_catalog."default"`
|
|
||||||
- ✅ **布尔值转换** - `DEFAULT false` → `DEFAULT 0`, `DEFAULT true` → `DEFAULT 1`
|
|
||||||
|
|
||||||
### 5️⃣ 分区表语法处理
|
|
||||||
- ✅ **移除空PARTITION BY** - 清理PostgreSQL分区表的空语法
|
|
||||||
- 自动检测并移除 `PARTITION BY ()` 子句
|
|
||||||
|
|
||||||
### 6️⃣ 索引语法优化
|
|
||||||
- ✅ **简化索引定义**
|
|
||||||
- 移除 `USING btree/hash/gist`
|
|
||||||
- 移除操作符类 `"text_ops"`, `"int8_ops"` 等
|
|
||||||
- 移除 `NULLS LAST/FIRST`
|
|
||||||
- 移除 `ASC/DESC` (可选)
|
|
||||||
- ✅ **重复列检测** - 自动发现并移除索引中的重复列
|
|
||||||
- ✅ **COALESCE函数索引处理**
|
|
||||||
- 检测超过816字符限制的函数索引
|
|
||||||
- 自动简化或发出警告
|
|
||||||
- 移除COALESCE包装,保留原始列名
|
|
||||||
|
|
||||||
### 7️⃣ 智能日志与报告
|
|
||||||
- ✅ 生成详细的转换日志(JSON格式)
|
|
||||||
- ✅ 实时显示转换进度和统计
|
|
||||||
- ✅ 警告潜在问题(复杂索引、类型转换等)
|
|
||||||
|
|
||||||
### 8️⃣ SQL执行优化 ⚡ **NEW**
|
|
||||||
- ✅ **移除COMMENT语句** - 减少98%执行语句数 (27,000+ → 536)
|
|
||||||
- 自动移除所有 `COMMENT ON COLUMN` 和 `COMMENT ON TABLE`
|
|
||||||
- 执行时间从45分钟降至2-3分钟
|
|
||||||
- ✅ **事务控制** - 批量提交,提高执行速度和安全性
|
|
||||||
- 自动添加 `BEGIN/COMMIT` 事务包装
|
|
||||||
- 每100个DDL语句提交一次(可配置)
|
|
||||||
- 失败自动回滚
|
|
||||||
- ✅ **可配置优化级别** - 根据场景选择优化策略
|
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@ -110,134 +55,17 @@ pg2dm-converter/
|
|||||||
└── output/ # 输出转换后的文件
|
└── output/ # 输出转换后的文件
|
||||||
```
|
```
|
||||||
|
|
||||||
## 详细问题解决方案
|
## 转换规则
|
||||||
|
|
||||||
### ❌ 问题1: 序列DEFAULT约束表达式无效
|
|
||||||
**错误信息**: `-2670: 对象[id]DEFAULT约束表达式无效`
|
|
||||||
|
|
||||||
**原因**: PostgreSQL使用 `DEFAULT nextval('seq'::regclass)` 语法,达梦不支持
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
```sql
|
|
||||||
-- PostgreSQL (转换前)
|
|
||||||
"id" BIGINT NOT NULL DEFAULT nextval('"schema".seq_name'::regclass)
|
|
||||||
|
|
||||||
-- 达梦 (转换后)
|
|
||||||
"id" BIGINT IDENTITY(1, 1) NOT NULL
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ 问题2: 非法的基类名[pg_catalog]
|
|
||||||
**错误信息**: `-3719: 非法的基类名[pg_catalog]`
|
|
||||||
|
|
||||||
**原因**: PostgreSQL使用 `"pg_catalog"."BIGINT"` 格式,达梦不识别
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
```sql
|
|
||||||
-- PostgreSQL (转换前)
|
|
||||||
"col" "pg_catalog"."BIGINT" NOT NULL
|
|
||||||
|
|
||||||
-- 达梦 (转换后)
|
|
||||||
"col" BIGINT NOT NULL
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ 问题3: COLLATE语法错误
|
|
||||||
**错误信息**: 语法分析出错
|
|
||||||
|
|
||||||
**原因**: PostgreSQL的COLLATE子句在达梦中不支持
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
```sql
|
|
||||||
-- PostgreSQL (转换前)
|
|
||||||
"name" varchar COLLATE "pg_catalog"."default" NOT NULL
|
|
||||||
|
|
||||||
-- 达梦 (转换后)
|
|
||||||
"name" varchar NOT NULL
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ 问题4: TEXT类型建索引失败
|
|
||||||
**错误信息**: `-3207: 试图在BLOB/CLOB/用户自定义数据类型列上建索引`
|
|
||||||
|
|
||||||
**原因**: 达梦的TEXT是CLOB大对象类型,不能建立索引
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
```sql
|
|
||||||
-- PostgreSQL (转换前)
|
|
||||||
"description" text
|
|
||||||
|
|
||||||
-- 达梦 (转换后)
|
|
||||||
"description" VARCHAR(8000)
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ 问题5: 函数索引表达式太长
|
|
||||||
**错误信息**: `FUNCTION INDEX EXPRESSION TOO LONG`
|
|
||||||
|
|
||||||
**原因**: 达梦函数索引表达式限制816字符,多个COALESCE超限
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
```sql
|
|
||||||
-- PostgreSQL (转换前)
|
|
||||||
CREATE UNIQUE INDEX idx ON table(
|
|
||||||
COALESCE("col1", '-999'::character varying),
|
|
||||||
COALESCE("col2", '-999'::character varying),
|
|
||||||
...
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 达梦 (转换后) - 移除COALESCE
|
|
||||||
CREATE UNIQUE INDEX idx ON table(
|
|
||||||
"col1",
|
|
||||||
"col2",
|
|
||||||
...
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ 问题6: 索引中重复列
|
|
||||||
**错误信息**: `-3204: 索引指定了无效的列`
|
|
||||||
|
|
||||||
**原因**: 索引定义中同一列出现多次
|
|
||||||
|
|
||||||
**解决方案**: 自动检测并移除重复列
|
|
||||||
|
|
||||||
### ❌ 问题7: PARTITION BY语法错误
|
|
||||||
**错误信息**: 语法分析出错
|
|
||||||
|
|
||||||
**原因**: PostgreSQL分区表的空PARTITION BY子句
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
```sql
|
|
||||||
-- PostgreSQL (转换前)
|
|
||||||
) PARTITION BY ();
|
|
||||||
|
|
||||||
-- 达梦 (转换后)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ 问题8: timestamp精度参数
|
|
||||||
**错误信息**: 语法分析出错
|
|
||||||
|
|
||||||
**原因**: 达梦不支持timestamp(6)的精度参数
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
```sql
|
|
||||||
-- PostgreSQL (转换前)
|
|
||||||
"created_at" timestamp(6)
|
|
||||||
|
|
||||||
-- 达梦 (转换后)
|
|
||||||
"created_at" TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
## 转换规则详解
|
|
||||||
|
|
||||||
### 1. 数据类型映射
|
### 1. 数据类型映射
|
||||||
|
|
||||||
| PostgreSQL | 达梦(DM8) | 说明 |
|
| PostgreSQL | 达梦(DM8) |
|
||||||
|-----------|----------|------|
|
|-----------|----------|
|
||||||
| int8 | BIGINT | 8字节整数 |
|
| int8 | BIGINT |
|
||||||
| int4 | INT | 4字节整数 |
|
| int4 | INT |
|
||||||
| int2 | SMALLINT | 2字节整数 |
|
| int2 | SMALLINT |
|
||||||
| numeric | DECIMAL | 精确数值 |
|
| numeric | DECIMAL |
|
||||||
| bool | BIT | 布尔值 |
|
| bool | BIT |
|
||||||
| text | VARCHAR(8000) | **关键**:避免CLOB不能建索引 |
|
|
||||||
| timestamp(n) | TIMESTAMP | 移除精度参数 |
|
|
||||||
|
|
||||||
### 2. 序列转换
|
### 2. 序列转换
|
||||||
|
|
||||||
@ -377,109 +205,14 @@ A: 测试覆盖PostgreSQL 12-16,理论上支持所有使用标准SQL导出的
|
|||||||
|
|
||||||
完整示例见 `d:\scp_custom_planning_item_dm.sql`
|
完整示例见 `d:\scp_custom_planning_item_dm.sql`
|
||||||
|
|
||||||
## 技术实现细节
|
|
||||||
|
|
||||||
### 转换流程架构
|
|
||||||
|
|
||||||
工具采用**11步流水线处理架构**,每一步专注解决特定问题:
|
|
||||||
|
|
||||||
```
|
|
||||||
原始SQL
|
|
||||||
↓
|
|
||||||
[步骤1] 移除pg_catalog前缀
|
|
||||||
↓
|
|
||||||
[步骤2] 数据类型转换 (int8→BIGINT等)
|
|
||||||
↓
|
|
||||||
[步骤3] 序列转IDENTITY
|
|
||||||
↓
|
|
||||||
[步骤4] 移除类型转换 (::regclass等)
|
|
||||||
↓
|
|
||||||
[步骤5] 移除COLLATE子句
|
|
||||||
↓
|
|
||||||
[步骤6] TEXT→VARCHAR + 移除类型引号
|
|
||||||
↓
|
|
||||||
[步骤7] 移除空PARTITION BY
|
|
||||||
↓
|
|
||||||
[步骤8] 简化索引语法
|
|
||||||
↓
|
|
||||||
[步骤9] 检测并移除重复索引列
|
|
||||||
↓
|
|
||||||
[步骤10] 处理COALESCE函数索引
|
|
||||||
↓
|
|
||||||
[步骤11] 添加转换说明注释
|
|
||||||
↓
|
|
||||||
达梦兼容SQL
|
|
||||||
```
|
|
||||||
|
|
||||||
### 核心正则表达式
|
|
||||||
|
|
||||||
#### 1. 序列转换
|
|
||||||
```javascript
|
|
||||||
// 匹配: "id" BIGINT NOT NULL DEFAULT nextval(...)
|
|
||||||
const pattern = /"(\w+)"\s+([A-Z]+(?:\([^)]+\))?)\s+NOT\s+NULL\s+DEFAULT\s+nextval\s*\([^)]+\)/gi;
|
|
||||||
// 替换为: "id" BIGINT IDENTITY(1, 1) NOT NULL
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. pg_catalog清理
|
|
||||||
```javascript
|
|
||||||
// 匹配并移除: "pg_catalog".
|
|
||||||
const pattern = /"pg_catalog"\./gi;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. TEXT类型转换
|
|
||||||
```javascript
|
|
||||||
// 匹配: text (小写/大写)
|
|
||||||
// 替换为: VARCHAR(8000)
|
|
||||||
converted = converted.replace(/\s+text\s+/gi, ' VARCHAR(8000) ');
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. COALESCE索引简化
|
|
||||||
```javascript
|
|
||||||
// 匹配: COALESCE("col_name", '-999')
|
|
||||||
// 替换为: "col_name"
|
|
||||||
const pattern = /COALESCE\s*\(\s*"?(\w+)"?\s*,\s*'[^']+'\s*\)/gi;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 性能优化
|
|
||||||
|
|
||||||
- **单遍扫描**: 每个转换步骤只扫描SQL一次
|
|
||||||
- **增量处理**: 转换结果传递到下一步,避免重复解析
|
|
||||||
- **智能匹配**: 使用精确正则避免误匹配
|
|
||||||
- **内存友好**: 流式处理,支持大文件(测试27000行SQL)
|
|
||||||
|
|
||||||
### 错误处理策略
|
|
||||||
|
|
||||||
1. **非破坏性转换**: 无法识别的语法保持原样
|
|
||||||
2. **警告机制**: 潜在问题记录到日志但不中断转换
|
|
||||||
3. **详细日志**: JSON格式记录每一步操作
|
|
||||||
4. **原文保留**: 转换前备份建议
|
|
||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
- **v1.0.0 (2025-11-15)** - 生产版本
|
- v1.0.0 (2025-11-15)
|
||||||
- ✅ 完整的12步转换流程
|
- 初始版本
|
||||||
- ✅ 解决8大类常见迁移问题
|
- 支持基本数据类型转换
|
||||||
- ✅ 支持27000+行大型SQL文件
|
- 支持序列转IDENTITY
|
||||||
- ✅ TEXT类型关键修复(VARCHAR转换)
|
- 支持索引语法简化
|
||||||
- ✅ 空PARTITION BY清理
|
- 支持COALESCE函数索引处理
|
||||||
- ✅ 索引重复列检测
|
|
||||||
- ✅ 完整的日志和报告系统
|
|
||||||
|
|
||||||
## 测试覆盖
|
|
||||||
|
|
||||||
### 已测试场景
|
|
||||||
- ✅ 118个表的完整数据库迁移
|
|
||||||
- ✅ 7000+个数据类型转换
|
|
||||||
- ✅ 118个序列转IDENTITY
|
|
||||||
- ✅ 7500+个COLLATE子句清理
|
|
||||||
- ✅ 53个COALESCE函数索引处理
|
|
||||||
- ✅ 复杂分区表语法
|
|
||||||
- ✅ 多层嵌套索引定义
|
|
||||||
|
|
||||||
### 生产环境验证
|
|
||||||
- 成功迁移themetis_scp数据库(118张表)
|
|
||||||
- 所有表和索引正确创建
|
|
||||||
- 无数据丢失,无语法错误
|
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
@ -487,19 +220,4 @@ MIT License
|
|||||||
|
|
||||||
## 技术支持
|
## 技术支持
|
||||||
|
|
||||||
遇到问题请按以下步骤排查:
|
遇到问题请检查转换日志文件,日志中包含详细的转换步骤和警告信息。
|
||||||
|
|
||||||
1. **查看转换日志**: `output/*_conversion.log.json`
|
|
||||||
2. **检查警告信息**: 控制台输出的WARN级别信息
|
|
||||||
3. **验证SQL**: 在达梦测试环境先执行部分语句
|
|
||||||
4. **对比原始**: 使用diff工具对比转换前后差异
|
|
||||||
|
|
||||||
## 贡献指南
|
|
||||||
|
|
||||||
欢迎提交Issue和Pull Request!
|
|
||||||
|
|
||||||
重点改进方向:
|
|
||||||
- 更多PostgreSQL特性支持
|
|
||||||
- 性能优化
|
|
||||||
- 错误处理增强
|
|
||||||
- 测试用例完善
|
|
||||||
|
|||||||
12
config.js
12
config.js
@ -13,10 +13,7 @@ module.exports = {
|
|||||||
'timestamp': 'TIMESTAMP',
|
'timestamp': 'TIMESTAMP',
|
||||||
'bool': 'BIT',
|
'bool': 'BIT',
|
||||||
'text': 'TEXT',
|
'text': 'TEXT',
|
||||||
'bytea': 'BLOB',
|
'bytea': 'BLOB'
|
||||||
'float8': 'DOUBLE', // PostgreSQL double precision
|
|
||||||
'float4': 'REAL', // PostgreSQL real/float
|
|
||||||
'float': 'REAL' // 通用float
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 序列DEFAULT值转换规则
|
// 序列DEFAULT值转换规则
|
||||||
@ -52,12 +49,5 @@ module.exports = {
|
|||||||
addConversionComment: true, // 添加转换说明注释
|
addConversionComment: true, // 添加转换说明注释
|
||||||
generateLog: true, // 生成转换日志
|
generateLog: true, // 生成转换日志
|
||||||
warningOnComplexIndex: true // 复杂索引发出警告
|
warningOnComplexIndex: true // 复杂索引发出警告
|
||||||
},
|
|
||||||
|
|
||||||
// SQL优化配置
|
|
||||||
optimization: {
|
|
||||||
removeComments: false, // 保留COMMENT语句(设为true可移除以加速执行)
|
|
||||||
addTransaction: true, // 添加事务包装
|
|
||||||
batchSize: 100 // 批量提交大小(每N个DDL语句提交一次)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
263
converter.js
263
converter.js
@ -42,9 +42,7 @@ class PG2DMConverter {
|
|||||||
*/
|
*/
|
||||||
convertDataTypes(sql) {
|
convertDataTypes(sql) {
|
||||||
let converted = sql;
|
let converted = sql;
|
||||||
|
const typePattern = /\b(int8|int4|int2|numeric|bool)\b/gi;
|
||||||
// 1. 转换基本类型(包括浮点类型)
|
|
||||||
const typePattern = /\b(int8|int4|int2|numeric|bool|float8|float4|float)\b/gi;
|
|
||||||
|
|
||||||
converted = converted.replace(typePattern, (match) => {
|
converted = converted.replace(typePattern, (match) => {
|
||||||
const lowerMatch = match.toLowerCase();
|
const lowerMatch = match.toLowerCase();
|
||||||
@ -55,14 +53,6 @@ class PG2DMConverter {
|
|||||||
return match;
|
return match;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 处理timestamp精度参数
|
|
||||||
// PostgreSQL: timestamp(6) 或 timestamp(0)
|
|
||||||
// 达梦: TIMESTAMP (不支持精度参数,直接移除)
|
|
||||||
converted = converted.replace(/\btimestamp\s*\(\s*\d+\s*\)/gi, (match) => {
|
|
||||||
this.log(`移除timestamp精度参数: ${match} -> TIMESTAMP`);
|
|
||||||
return `TIMESTAMP`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return converted;
|
return converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,10 +167,6 @@ class PG2DMConverter {
|
|||||||
removeTypeQuotes(sql) {
|
removeTypeQuotes(sql) {
|
||||||
let converted = sql;
|
let converted = sql;
|
||||||
|
|
||||||
// 先统一小写的text类型为TEXT(避免和"text_ops"操作符混淆)
|
|
||||||
converted = converted.replace(/\s+text\s+/gi, ' TEXT ');
|
|
||||||
converted = converted.replace(/\s+text([,\n\r])/gi, ' TEXT$1');
|
|
||||||
|
|
||||||
// 移除引号中的数据类型(达梦不需要给类型加引号)
|
// 移除引号中的数据类型(达梦不需要给类型加引号)
|
||||||
// 必须在独立的步骤中处理,确保不会误伤列名
|
// 必须在独立的步骤中处理,确保不会误伤列名
|
||||||
// 匹配模式:前面有空格,后面有空格或逗号
|
// 匹配模式:前面有空格,后面有空格或逗号
|
||||||
@ -194,25 +180,6 @@ class PG2DMConverter {
|
|||||||
return converted;
|
return converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除空的或不完整的PARTITION BY子句
|
|
||||||
*/
|
|
||||||
removeEmptyPartition(sql) {
|
|
||||||
let converted = sql;
|
|
||||||
|
|
||||||
// 移除空的PARTITION BY子句
|
|
||||||
// 格式1: )\nPARTITION BY (\n)\n;
|
|
||||||
// 格式2: ) PARTITION BY ();
|
|
||||||
converted = converted.replace(/\)\s*PARTITION\s+BY\s+\([^)]*\)\s*;/gi, ');\n');
|
|
||||||
|
|
||||||
const matches = sql.match(/PARTITION\s+BY\s+\(/gi);
|
|
||||||
if (matches) {
|
|
||||||
this.log(`移除 ${matches.length} 个空的PARTITION BY子句`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return converted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 简化索引语法
|
* 简化索引语法
|
||||||
*/
|
*/
|
||||||
@ -275,208 +242,6 @@ class PG2DMConverter {
|
|||||||
return converted;
|
return converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 转换PostgreSQL视图中的LATERAL VALUES为达梦UNPIVOT
|
|
||||||
*/
|
|
||||||
convertLateralUnpivotViews(sql) {
|
|
||||||
let converted = sql;
|
|
||||||
|
|
||||||
// 移除类型转换(先做,避免干扰后续匹配)
|
|
||||||
converted = converted.replace(/::(text|character\s+varying|varchar|integer|bigint)/gi, '');
|
|
||||||
|
|
||||||
// 匹配完整的LATERAL JOIN模式(修复:使用更精确的匹配避免遗漏列)
|
|
||||||
// 格式: LEFT JOIN LATERAL ( VALUES (...), (...), ... ) s0(...) ON (true)
|
|
||||||
const lateralPattern = /LEFT\s+JOIN\s+LATERAL\s*\(\s*VALUES\s+([\s\S]+?)\s*\)\s*(\w+)\s*\(([^)]+)\)\s+ON\s+\([^)]*true[^)]*\)/gi;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
let unpivotCount = 0;
|
|
||||||
|
|
||||||
while ((match = lateralPattern.exec(converted)) !== null) {
|
|
||||||
const [fullMatch, valuesContent, alias, aliasColumns] = match;
|
|
||||||
unpivotCount++;
|
|
||||||
|
|
||||||
this.log(`检测到LATERAL VALUES UNPIVOT模式 #${unpivotCount}`);
|
|
||||||
|
|
||||||
// 解析VALUES内容,提取所有列名
|
|
||||||
// 格式: ('bk_qty_001', t.bk_qty_001), ('bk_qty_002', t.bk_qty_002), ...
|
|
||||||
const valuePattern = /\(\s*'([^']+)'\s*,\s*t\.(\w+)\s*\)/g;
|
|
||||||
const columns = [];
|
|
||||||
let valueMatch;
|
|
||||||
|
|
||||||
while ((valueMatch = valuePattern.exec(valuesContent)) !== null) {
|
|
||||||
const columnName = valueMatch[2];
|
|
||||||
columns.push(columnName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (columns.length === 0) {
|
|
||||||
this.warn('无法解析VALUES内容,跳过该视图转换');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log(`提取到 ${columns.length} 个列用于UNPIVOT转换`);
|
|
||||||
|
|
||||||
// 解析别名列名
|
|
||||||
const aliasCols = aliasColumns.split(',').map(c => c.trim());
|
|
||||||
const fieldNameCol = aliasCols[0] || 'bucket_field';
|
|
||||||
const fieldValueCol = aliasCols[1] || 'bucket_qty';
|
|
||||||
|
|
||||||
// 生成UNPIVOT子句(不包含LEFT JOIN,因为UNPIVOT本身不需要JOIN)
|
|
||||||
const unpivotClause = `
|
|
||||||
UNPIVOT (
|
|
||||||
${fieldValueCol} FOR ${fieldNameCol} IN (
|
|
||||||
${columns.join(', ')}
|
|
||||||
)
|
|
||||||
) ${alias}`;
|
|
||||||
|
|
||||||
// 替换:移除整个LEFT JOIN LATERAL ... ON (true),替换为UNPIVOT
|
|
||||||
// 注意:不包含JOIN关键字,因为UNPIVOT直接跟在表后面
|
|
||||||
const replacement = unpivotClause + '\n ';
|
|
||||||
converted = converted.replace(fullMatch, replacement);
|
|
||||||
|
|
||||||
this.log(`✓ 已转换为达梦UNPIVOT语法`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unpivotCount > 0) {
|
|
||||||
this.log(`共转换 ${unpivotCount} 个LATERAL VALUES为UNPIVOT`);
|
|
||||||
|
|
||||||
// 清理括号 - 完全重构
|
|
||||||
// 1. FROM ((table -> FROM (table
|
|
||||||
converted = converted.replace(/FROM\s+\(\(/gi, 'FROM (');
|
|
||||||
|
|
||||||
// 2. 移除UNPIVOT后的多余右括号
|
|
||||||
converted = converted.replace(/(\)\s+s\d+)\s*\n\s*\)\s*\n\s*(JOIN)/gi, '$1\n $2');
|
|
||||||
|
|
||||||
// 3. 清理ON条件 - 完全重写为简单形式
|
|
||||||
// ON ((((col = col) AND (col = col) ...))) -> ON (col = col AND col = col ...)
|
|
||||||
const onPattern = /JOIN\s+([^\s]+)\s+ON\s+\(+([^W]+?)\)+\s*WHERE/gi;
|
|
||||||
converted = converted.replace(onPattern, (match, tableName, conditions) => {
|
|
||||||
// 移除所有括号和多余空格
|
|
||||||
let cleanConditions = conditions
|
|
||||||
.replace(/\(/g, '')
|
|
||||||
.replace(/\)/g, '')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
return `JOIN ${tableName} ON (${cleanConditions})\n WHERE`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return converted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 内联COMMENT到CREATE TABLE语句中
|
|
||||||
*/
|
|
||||||
inlineComments(sql) {
|
|
||||||
let converted = sql;
|
|
||||||
|
|
||||||
// 第一步:提取所有COMMENT映射
|
|
||||||
const commentMap = new Map();
|
|
||||||
const tableCommentMap = new Map();
|
|
||||||
|
|
||||||
// 提取列注释:COMMENT ON COLUMN "schema"."table"."column" IS 'comment';
|
|
||||||
const columnCommentPattern = /COMMENT\s+ON\s+COLUMN\s+"([^"]+)"\."([^"]+)"\."([^"]+)"\s+IS\s+'([^']+)';/gi;
|
|
||||||
let match;
|
|
||||||
while ((match = columnCommentPattern.exec(sql)) !== null) {
|
|
||||||
const [, schema, table, column, comment] = match;
|
|
||||||
const key = `${schema}.${table}.${column}`;
|
|
||||||
commentMap.set(key, comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取表注释:COMMENT ON TABLE "schema"."table" IS 'comment';
|
|
||||||
const tableCommentPattern = /COMMENT\s+ON\s+TABLE\s+"([^"]+)"\."([^"]+)"\s+IS\s+'([^']+)';/gi;
|
|
||||||
while ((match = tableCommentPattern.exec(sql)) !== null) {
|
|
||||||
const [, schema, table, comment] = match;
|
|
||||||
const key = `${schema}.${table}`;
|
|
||||||
tableCommentMap.set(key, comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log(`提取到 ${commentMap.size} 个列注释和 ${tableCommentMap.size} 个表注释`);
|
|
||||||
|
|
||||||
// 第二步:将注释内联到CREATE TABLE中
|
|
||||||
const createTablePattern = /CREATE\s+TABLE\s+"([^"]+)"\."([^"]+)"\s*\(([\s\S]*?)\)\s*;/gi;
|
|
||||||
|
|
||||||
converted = converted.replace(createTablePattern, (tableMatch, schema, table, columnsDef) => {
|
|
||||||
// 处理每一列
|
|
||||||
const lines = columnsDef.split('\n');
|
|
||||||
const newLines = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
const trimmedLine = line.trim();
|
|
||||||
|
|
||||||
// 跳过空行
|
|
||||||
if (!trimmedLine) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 匹配列定义: "column_name" TYPE ...
|
|
||||||
const colMatch = line.match(/^\s*"([^"]+)"\s+(.+?)$/);
|
|
||||||
if (colMatch) {
|
|
||||||
const [, columnName, rest] = colMatch;
|
|
||||||
const key = `${schema}.${table}.${columnName}`;
|
|
||||||
const comment = commentMap.get(key);
|
|
||||||
|
|
||||||
// 检查是否是最后一个非空列定义
|
|
||||||
let isLastColumn = true;
|
|
||||||
for (let j = i + 1; j < lines.length; j++) {
|
|
||||||
if (lines[j].trim() && lines[j].trim().match(/^\s*"/)) {
|
|
||||||
isLastColumn = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除原有的行尾逗号
|
|
||||||
let restCleaned = rest.trim();
|
|
||||||
if (restCleaned.endsWith(',')) {
|
|
||||||
restCleaned = restCleaned.slice(0, -1).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建新行
|
|
||||||
let newLine = ` "${columnName}" ${restCleaned}`;
|
|
||||||
|
|
||||||
// 添加COMMENT(如果有)
|
|
||||||
if (comment) {
|
|
||||||
newLine += ` COMMENT '${comment}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加逗号(除了最后一列)
|
|
||||||
if (!isLastColumn) {
|
|
||||||
newLine += ',';
|
|
||||||
}
|
|
||||||
|
|
||||||
newLines.push(newLine);
|
|
||||||
} else {
|
|
||||||
// 保留非列定义行(如空行、注释等)
|
|
||||||
if (trimmedLine) {
|
|
||||||
newLines.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加表注释
|
|
||||||
const tableKey = `${schema}.${table}`;
|
|
||||||
const tableComment = tableCommentMap.get(tableKey);
|
|
||||||
let result = `CREATE TABLE "${schema}"."${table}" (\n${newLines.join('\n')}\n)`;
|
|
||||||
|
|
||||||
if (tableComment) {
|
|
||||||
result += ` COMMENT '${tableComment}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result + ';';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 第三步:移除所有独立的COMMENT语句
|
|
||||||
converted = converted.replace(/COMMENT\s+ON\s+(COLUMN|TABLE)\s+[^;]+;/gi, '');
|
|
||||||
|
|
||||||
// 第四步:清理多余空行(3个或以上连续空行变成2个)
|
|
||||||
converted = converted.replace(/\n{4,}/g, '\n\n\n');
|
|
||||||
|
|
||||||
this.log(`COMMENT已内联到CREATE TABLE,移除了 ${commentMap.size + tableCommentMap.size} 条独立COMMENT语句`);
|
|
||||||
|
|
||||||
return converted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理COALESCE函数索引
|
* 处理COALESCE函数索引
|
||||||
*/
|
*/
|
||||||
@ -580,31 +345,19 @@ UNPIVOT (
|
|||||||
this.log('步骤6: 移除数据类型引号...');
|
this.log('步骤6: 移除数据类型引号...');
|
||||||
converted = this.removeTypeQuotes(converted);
|
converted = this.removeTypeQuotes(converted);
|
||||||
|
|
||||||
// 7. 移除空的PARTITION BY子句
|
// 7. 简化索引语法
|
||||||
this.log('步骤7: 移除空的PARTITION BY子句...');
|
this.log('步骤7: 简化索引语法...');
|
||||||
converted = this.removeEmptyPartition(converted);
|
|
||||||
|
|
||||||
// 8. 简化索引语法
|
|
||||||
this.log('步骤8: 简化索引语法...');
|
|
||||||
converted = this.simplifyIndexSyntax(converted);
|
converted = this.simplifyIndexSyntax(converted);
|
||||||
|
|
||||||
// 9. 移除索引中的重复列
|
// 8. 移除索引中的重复列
|
||||||
this.log('步骤9: 移除索引中的重复列...');
|
this.log('步骤8: 移除索引中的重复列...');
|
||||||
converted = this.removeDuplicateIndexColumns(converted);
|
converted = this.removeDuplicateIndexColumns(converted);
|
||||||
|
|
||||||
// 10. 处理COALESCE函数索引
|
// 9. 处理COALESCE函数索引
|
||||||
this.log('步骤10: 处理COALESCE函数索引...');
|
this.log('步骤9: 处理COALESCE函数索引...');
|
||||||
converted = this.processCoalesceIndexes(converted);
|
converted = this.processCoalesceIndexes(converted);
|
||||||
|
|
||||||
// 11. 转换LATERAL视图语法
|
// 10. 添加转换说明
|
||||||
this.log('步骤11: 转换LATERAL视图语法...');
|
|
||||||
converted = this.convertLateralUnpivotViews(converted);
|
|
||||||
|
|
||||||
// 12. 内联COMMENT到CREATE TABLE
|
|
||||||
this.log('步骤12: 内联COMMENT到CREATE TABLE...');
|
|
||||||
converted = this.inlineComments(converted);
|
|
||||||
|
|
||||||
// 13. 添加转换说明
|
|
||||||
if (config.output.addConversionComment) {
|
if (config.output.addConversionComment) {
|
||||||
converted = this.addConversionHeader(converted, originalFile);
|
converted = this.addConversionHeader(converted, originalFile);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user