828 lines
24 KiB
Markdown
828 lines
24 KiB
Markdown
|
|
# 跨数据库SQL兼容性检测方案
|
|||
|
|
|
|||
|
|
## 一、检测目标
|
|||
|
|
|
|||
|
|
检测系统中所有SQL语句在目标数据库(达梦/MySQL/Oracle/DB2)上的兼容性,识别并修复不兼容的SQL语法,确保系统平滑迁移到新数据库环境。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、检测范围
|
|||
|
|
|
|||
|
|
### 检测对象
|
|||
|
|
1. **Mapper XML文件**:所有MyBatis Mapper XML中的SQL语句
|
|||
|
|
2. **Java代码**:使用注解(@Select/@Insert/@Update/@Delete)的SQL
|
|||
|
|
3. **动态SQL**:Java代码中拼接的SQL字符串
|
|||
|
|
4. **数据库脚本**:DDL/DML脚本文件
|
|||
|
|
|
|||
|
|
### 检测项
|
|||
|
|
1. **SQL语法兼容性**
|
|||
|
|
- MySQL特有语法 → 达梦/Oracle语法
|
|||
|
|
- 分页、函数、操作符等
|
|||
|
|
|
|||
|
|
2. **数据类型兼容性**
|
|||
|
|
- MySQL特有类型 → 标准SQL类型
|
|||
|
|
- 类型映射关系
|
|||
|
|
|
|||
|
|
3. **字符串和日期处理**
|
|||
|
|
- 字符串拼接、日期函数等
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、MySQL到达梦兼容性对照表
|
|||
|
|
|
|||
|
|
| SQL类型 | MySQL语法 | 达梦语法 | 兼容性说明 | 检测方法 |
|
|||
|
|
|--------|-----------|---------|-----------|---------|
|
|||
|
|
| **分页** | `LIMIT 10, 20` | `LIMIT 20 OFFSET 10` 或 `ROWNUM` | 达梦8+支持LIMIT | Grep搜索`LIMIT` |
|
|||
|
|
| **自增主键** | `AUTO_INCREMENT` | `IDENTITY` 或 `SEQUENCE` | 需修改为IDENTITY | Grep搜索`AUTO_INCREMENT` |
|
|||
|
|
| **日期函数** | `NOW()` | `SYSDATE` | 需替换为SYSDATE | Grep搜索`NOW()` |
|
|||
|
|
| **字符串拼接** | `CONCAT(a,b)` 或 `CONCAT_WS()` | `a \|\| b` 或 `CONCAT()` | 达梦支持两种方式 | Grep搜索`CONCAT` |
|
|||
|
|
| **反引号** | `` `table` `` | `"table"` | 需替换为双引号 | Grep搜索反引号 |
|
|||
|
|
| **布尔值** | `TRUE/FALSE` | `1/0` | 需替换为1/0 | Grep搜索`TRUE\|FALSE` |
|
|||
|
|
| **注释符号** | `#注释` | `-- 注释` | `#`不支持,需改为`--` | Grep搜索`^[[:space:]]*#` |
|
|||
|
|
| **当前时间** | `CURRENT_TIMESTAMP` | `SYSDATE` | 建议统一用SYSDATE | Grep搜索`CURRENT_TIMESTAMP` |
|
|||
|
|
| **空字符串** | `''` | `''` 或 `NULL` | 大部分兼容 | 人工检查 |
|
|||
|
|
| **数据类型** | `TINYINT/MEDIUMINT` | `TINYINT/INT` | TINYINT需调整为INT | Grep搜索`TINYINT\|MEDIUMINT` |
|
|||
|
|
| **索引提示** | `USE INDEX` | 不支持 | 需删除索引提示 | Grep搜索`USE INDEX` |
|
|||
|
|
| **GROUP BY** | 允许SELECT非聚合字段 | 要求SELECT字段都在GROUP BY中 | 需修改SQL | 人工检查 |
|
|||
|
|
| **IF函数** | `IF(condition, a, b)` | `CASE WHEN` | 需改写为CASE WHEN | Grep搜索`\bIF\(` |
|
|||
|
|
| **日期格式化** | `DATE_FORMAT()` | `TO_CHAR()` | 需改写为TO_CHAR | Grep搜索`DATE_FORMAT` |
|
|||
|
|
| **字符串转数字** | `CAST('123' AS SIGNED)` | `CAST('123' AS INT)` | SIGNED改为INT | Grep搜索`AS SIGNED` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、检测方法
|
|||
|
|
|
|||
|
|
### 方案A:命令行快速扫描
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
#!/bin/bash
|
|||
|
|
# MySQL到达梦SQL兼容性扫描脚本
|
|||
|
|
|
|||
|
|
echo "开始扫描MySQL特有SQL语法..."
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 1. 搜索分页语句(LIMIT)
|
|||
|
|
echo "=== 1. 分页语句 (LIMIT) ==="
|
|||
|
|
grep -rn "LIMIT\s" src/main/resources/mybatis/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 2. 搜索自增主键(AUTO_INCREMENT)
|
|||
|
|
echo "=== 2. 自增主键 (AUTO_INCREMENT) ==="
|
|||
|
|
grep -rn "AUTO_INCREMENT" src/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 3. 搜索日期函数(NOW)
|
|||
|
|
echo "=== 3. 日期函数 (NOW) ==="
|
|||
|
|
grep -rn "NOW()" src/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 4. 搜索反引号(MySQL特有)
|
|||
|
|
echo "=== 4. 反引号 (MySQL特有) ==="
|
|||
|
|
grep -rn '`' src/main/resources/mybatis/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 5. 搜索布尔值(TRUE/FALSE)
|
|||
|
|
echo "=== 5. 布尔值 (TRUE/FALSE) ==="
|
|||
|
|
grep -rn "\bTRUE\b\|\bFALSE\b" src/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 6. 搜索注释符号(#)
|
|||
|
|
echo "=== 6. 注释符号 (#) ==="
|
|||
|
|
grep -rn "^[[:space:]]*#" src/main/resources/mybatis/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 7. 搜索特定数据类型
|
|||
|
|
echo "=== 7. MySQL特有数据类型 (TINYINT/MEDIUMINT) ==="
|
|||
|
|
grep -rn "TINYINT\|MEDIUMINT" src/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 8. 搜索IF函数
|
|||
|
|
echo "=== 8. IF函数 (需改写为CASE WHEN) ==="
|
|||
|
|
grep -rn "\bIF(" src/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 9. 搜索DATE_FORMAT
|
|||
|
|
echo "=== 9. DATE_FORMAT (需改写为TO_CHAR) ==="
|
|||
|
|
grep -rn "DATE_FORMAT" src/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 10. 搜索CAST AS SIGNED
|
|||
|
|
echo "=== 10. CAST AS SIGNED (需改为AS INT) ==="
|
|||
|
|
grep -rn "AS SIGNED" src/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 11. 搜索USE INDEX
|
|||
|
|
echo "=== 11. USE INDEX (达梦不支持) ==="
|
|||
|
|
grep -rn "USE INDEX\|FORCE INDEX\|IGNORE INDEX" src/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# 12. 搜索GROUP_CONCAT
|
|||
|
|
echo "=== 12. GROUP_CONCAT (需改写为LISTAGG) ==="
|
|||
|
|
grep -rn "GROUP_CONCAT" src/
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
echo "扫描完成!"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Windows版本(PowerShell)**:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
# MySQL到达梦SQL兼容性扫描脚本
|
|||
|
|
|
|||
|
|
Write-Host "开始扫描MySQL特有SQL语法..." -ForegroundColor Green
|
|||
|
|
Write-Host ""
|
|||
|
|
|
|||
|
|
# 定义要扫描的模式
|
|||
|
|
$patterns = @{
|
|||
|
|
"分页语句 (LIMIT)" = "LIMIT\s"
|
|||
|
|
"自增主键 (AUTO_INCREMENT)" = "AUTO_INCREMENT"
|
|||
|
|
"日期函数 (NOW)" = "NOW\(\)"
|
|||
|
|
"反引号" = "`""
|
|||
|
|
"布尔值 (TRUE/FALSE)" = "\bTRUE\b|\bFALSE\b"
|
|||
|
|
"注释符号 (#)" = "^\s*#"
|
|||
|
|
"TINYINT/MEDIUMINT" = "TINYINT|MEDIUMINT"
|
|||
|
|
"IF函数" = "\bIF\("
|
|||
|
|
"DATE_FORMAT" = "DATE_FORMAT"
|
|||
|
|
"CAST AS SIGNED" = "AS SIGNED"
|
|||
|
|
"USE INDEX" = "USE INDEX|FORCE INDEX|IGNORE INDEX"
|
|||
|
|
"GROUP_CONCAT" = "GROUP_CONCAT"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 执行扫描
|
|||
|
|
foreach ($item in $patterns.GetEnumerator()) {
|
|||
|
|
Write-Host "=== $($item.Key) ===" -ForegroundColor Yellow
|
|||
|
|
Select-String -Path "src\**\*.xml", "src\**\*.java" -Pattern $item.Value -Recurse
|
|||
|
|
Write-Host ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Write-Host "扫描完成!" -ForegroundColor Green
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 方案B:Python自动化检测工具
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
MySQL到达梦SQL兼容性检测工具
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import re
|
|||
|
|
import json
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Dict, List, Tuple
|
|||
|
|
from dataclasses import dataclass, asdict
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class CompatibilityIssue:
|
|||
|
|
"""兼容性问题"""
|
|||
|
|
issue_type: str
|
|||
|
|
severity: str # HIGH, MEDIUM, LOW
|
|||
|
|
file_path: str
|
|||
|
|
line_number: int
|
|||
|
|
mysql_pattern: str
|
|||
|
|
dm_pattern: str
|
|||
|
|
description: str
|
|||
|
|
suggestion: str
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MySQLToDMCompatibilityChecker:
|
|||
|
|
"""MySQL到达梦兼容性检测器"""
|
|||
|
|
|
|||
|
|
# 检测规则配置
|
|||
|
|
RULES = {
|
|||
|
|
'LIMIT': {
|
|||
|
|
'pattern': r'\bLIMIT\s+(\d+)\s*,\s*(\d+)',
|
|||
|
|
'severity': 'HIGH',
|
|||
|
|
'mysql_pattern': 'LIMIT offset, count',
|
|||
|
|
'dm_pattern': 'LIMIT count OFFSET offset',
|
|||
|
|
'description': 'MySQL分页语法不兼容达梦',
|
|||
|
|
'suggestion': '将 LIMIT offset, count 改为 LIMIT count OFFSET offset'
|
|||
|
|
},
|
|||
|
|
'AUTO_INCREMENT': {
|
|||
|
|
'pattern': r'\bAUTO_INCREMENT\b',
|
|||
|
|
'severity': 'HIGH',
|
|||
|
|
'mysql_pattern': 'AUTO_INCREMENT',
|
|||
|
|
'dm_pattern': 'IDENTITY',
|
|||
|
|
'description': '达梦不支持AUTO_INCREMENT',
|
|||
|
|
'suggestion': '将 AUTO_INCREMENT 改为 IDENTITY 或使用序列SEQUENCE'
|
|||
|
|
},
|
|||
|
|
'NOW': {
|
|||
|
|
'pattern': r'\bNOW\(\)',
|
|||
|
|
'severity': 'HIGH',
|
|||
|
|
'mysql_pattern': 'NOW()',
|
|||
|
|
'dm_pattern': 'SYSDATE',
|
|||
|
|
'description': '达梦不支持NOW()函数',
|
|||
|
|
'suggestion': '将 NOW() 改为 SYSDATE'
|
|||
|
|
},
|
|||
|
|
'BACKTICK': {
|
|||
|
|
'pattern': r'`',
|
|||
|
|
'severity': 'HIGH',
|
|||
|
|
'mysql_pattern': '`table_name`',
|
|||
|
|
'dm_pattern': '"table_name"',
|
|||
|
|
'description': 'MySQL反引号语法',
|
|||
|
|
'suggestion': '将反引号改为双引号 "table_name"'
|
|||
|
|
},
|
|||
|
|
'BOOLEAN': {
|
|||
|
|
'pattern': r'\b(TRUE|FALSE)\b',
|
|||
|
|
'severity': 'MEDIUM',
|
|||
|
|
'mysql_pattern': 'TRUE/FALSE',
|
|||
|
|
'dm_pattern': '1/0',
|
|||
|
|
'description': '达梦不支持布尔值关键字',
|
|||
|
|
'suggestion': '将 TRUE 改为 1,FALSE 改为 0'
|
|||
|
|
},
|
|||
|
|
'COMMENT_HASH': {
|
|||
|
|
'pattern': r'^\s*#',
|
|||
|
|
'severity': 'LOW',
|
|||
|
|
'mysql_pattern': '# 注释',
|
|||
|
|
'dm_pattern': '-- 注释',
|
|||
|
|
'description': '达梦不支持#注释',
|
|||
|
|
'suggestion': '将 # 注释 改为 -- 注释'
|
|||
|
|
},
|
|||
|
|
'TINYINT': {
|
|||
|
|
'pattern': r'\bTINYINT\b',
|
|||
|
|
'severity': 'MEDIUM',
|
|||
|
|
'mysql_pattern': 'TINYINT',
|
|||
|
|
'dm_pattern': 'INT或SMALLINT',
|
|||
|
|
'description': '达梦不完全支持TINYINT',
|
|||
|
|
'suggestion': '将 TINYINT 改为 SMALLINT 或 INT'
|
|||
|
|
},
|
|||
|
|
'IF_FUNCTION': {
|
|||
|
|
'pattern': r'\bIF\s*\(',
|
|||
|
|
'severity': 'HIGH',
|
|||
|
|
'mysql_pattern': 'IF(condition, a, b)',
|
|||
|
|
'dm_pattern': 'CASE WHEN condition THEN a ELSE b END',
|
|||
|
|
'description': '达梦不支持IF()函数',
|
|||
|
|
'suggestion': '将 IF(condition, a, b) 改为 CASE WHEN condition THEN a ELSE b END'
|
|||
|
|
},
|
|||
|
|
'DATE_FORMAT': {
|
|||
|
|
'pattern': r'\bDATE_FORMAT\s*\(',
|
|||
|
|
'severity': 'HIGH',
|
|||
|
|
'mysql_pattern': 'DATE_FORMAT(date, format)',
|
|||
|
|
'dm_pattern': 'TO_CHAR(date, format)',
|
|||
|
|
'description': '达梦不支持DATE_FORMAT函数',
|
|||
|
|
'suggestion': '将 DATE_FORMAT(date, format) 改为 TO_CHAR(date, format)'
|
|||
|
|
},
|
|||
|
|
'CAST_SIGNED': {
|
|||
|
|
'pattern': r'\bCAST\s*\([^)]+\s+AS\s+SIGNED\b',
|
|||
|
|
'severity': 'MEDIUM',
|
|||
|
|
'mysql_pattern': 'CAST(expr AS SIGNED)',
|
|||
|
|
'dm_pattern': 'CAST(expr AS INT)',
|
|||
|
|
'description': '达梦不支持SIGNED类型',
|
|||
|
|
'suggestion': '将 CAST(expr AS SIGNED) 改为 CAST(expr AS INT)'
|
|||
|
|
},
|
|||
|
|
'USE_INDEX': {
|
|||
|
|
'pattern': r'\b(USE|FORCE|IGNORE)\s+INDEX\b',
|
|||
|
|
'severity': 'MEDIUM',
|
|||
|
|
'mysql_pattern': 'USE INDEX (index_name)',
|
|||
|
|
'dm_pattern': '(删除索引提示)',
|
|||
|
|
'description': '达梦不支持索引提示',
|
|||
|
|
'suggestion': '删除 USE INDEX/FORCE INDEX/IGNORE INDEX'
|
|||
|
|
},
|
|||
|
|
'GROUP_CONCAT': {
|
|||
|
|
'pattern': r'\bGROUP_CONCAT\s*\(',
|
|||
|
|
'severity': 'HIGH',
|
|||
|
|
'mysql_pattern': 'GROUP_CONCAT(expr)',
|
|||
|
|
'dm_pattern': 'LISTAGG(expr, delimiter)',
|
|||
|
|
'description': '达梦不支持GROUP_CONCAT函数',
|
|||
|
|
'suggestion': '将 GROUP_CONCAT(expr) 改为 LISTAGG(expr, ",")'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def __init__(self, source_dir: str):
|
|||
|
|
"""
|
|||
|
|
初始化检测器
|
|||
|
|
|
|||
|
|
:param source_dir: 源代码目录
|
|||
|
|
"""
|
|||
|
|
self.source_dir = Path(source_dir)
|
|||
|
|
self.issues: List[CompatibilityIssue] = []
|
|||
|
|
|
|||
|
|
def check_file(self, file_path: Path) -> List[CompatibilityIssue]:
|
|||
|
|
"""检测单个文件"""
|
|||
|
|
issues = []
|
|||
|
|
content = file_path.read_text(encoding='utf-8')
|
|||
|
|
lines = content.split('\n')
|
|||
|
|
|
|||
|
|
for line_no, line in enumerate(lines, 1):
|
|||
|
|
for rule_name, rule in self.RULES.items():
|
|||
|
|
matches = re.finditer(rule['pattern'], line, re.IGNORECASE)
|
|||
|
|
for match in matches:
|
|||
|
|
issues.append(CompatibilityIssue(
|
|||
|
|
issue_type=rule_name,
|
|||
|
|
severity=rule['severity'],
|
|||
|
|
file_path=str(file_path.relative_to(self.source_dir)),
|
|||
|
|
line_number=line_no,
|
|||
|
|
mysql_pattern=rule['mysql_pattern'],
|
|||
|
|
dm_pattern=rule['dm_pattern'],
|
|||
|
|
description=rule['description'],
|
|||
|
|
suggestion=rule['suggestion']
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
return issues
|
|||
|
|
|
|||
|
|
def check_all(self) -> List[CompatibilityIssue]:
|
|||
|
|
"""检测所有文件"""
|
|||
|
|
# 检测XML文件
|
|||
|
|
xml_files = list(self.source_dir.rglob('**/*.xml'))
|
|||
|
|
# 检测Java文件
|
|||
|
|
java_files = list(self.source_dir.rglob('**/*.java'))
|
|||
|
|
|
|||
|
|
all_files = xml_files + java_files
|
|||
|
|
|
|||
|
|
print(f'开始检测 {len(all_files)} 个文件...\n')
|
|||
|
|
|
|||
|
|
for idx, file_path in enumerate(all_files, 1):
|
|||
|
|
print(f'[{idx}/{len(all_files)}] 检测: {file_path.name}')
|
|||
|
|
issues = self.check_file(file_path)
|
|||
|
|
self.issues.extend(issues)
|
|||
|
|
|
|||
|
|
return self.issues
|
|||
|
|
|
|||
|
|
def generate_report(self, output_file: str = 'compatibility_report.json'):
|
|||
|
|
"""生成检测报告"""
|
|||
|
|
report = {
|
|||
|
|
'scan_time': datetime.now().isoformat(),
|
|||
|
|
'summary': {
|
|||
|
|
'total_issues': len(self.issues),
|
|||
|
|
'by_severity': self._count_by_severity(),
|
|||
|
|
'by_type': self._count_by_type(),
|
|||
|
|
'by_file': self._count_by_file()
|
|||
|
|
},
|
|||
|
|
'issues': [asdict(issue) for issue in self.issues]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 保存JSON报告
|
|||
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|||
|
|
json.dump(report, f, ensure_ascii=False, indent=2)
|
|||
|
|
|
|||
|
|
# 生成Markdown报告
|
|||
|
|
md_file = output_file.replace('.json', '.md')
|
|||
|
|
self._generate_markdown_report(report, md_file)
|
|||
|
|
|
|||
|
|
print(f'\n检测完成!')
|
|||
|
|
print(f'JSON报告: {output_file}')
|
|||
|
|
print(f'Markdown报告: {md_file}')
|
|||
|
|
print(f'\n总计发现 {report["summary"]["total_issues"]} 个兼容性问题:')
|
|||
|
|
for severity, count in report['summary']['by_severity'].items():
|
|||
|
|
print(f' - {severity}: {count}')
|
|||
|
|
|
|||
|
|
def _count_by_severity(self) -> Dict[str, int]:
|
|||
|
|
"""按严重程度统计"""
|
|||
|
|
count = {'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
|
|||
|
|
for issue in self.issues:
|
|||
|
|
count[issue.severity] += 1
|
|||
|
|
return count
|
|||
|
|
|
|||
|
|
def _count_by_type(self) -> Dict[str, int]:
|
|||
|
|
"""按问题类型统计"""
|
|||
|
|
count = {}
|
|||
|
|
for issue in self.issues:
|
|||
|
|
if issue.issue_type not in count:
|
|||
|
|
count[issue.issue_type] = 0
|
|||
|
|
count[issue.issue_type] += 1
|
|||
|
|
return dict(sorted(count.items(), key=lambda x: x[1], reverse=True))
|
|||
|
|
|
|||
|
|
def _count_by_file(self) -> Dict[str, int]:
|
|||
|
|
"""按文件统计"""
|
|||
|
|
count = {}
|
|||
|
|
for issue in self.issues:
|
|||
|
|
if issue.file_path not in count:
|
|||
|
|
count[issue.file_path] = 0
|
|||
|
|
count[issue.file_path] += 1
|
|||
|
|
return dict(sorted(count.items(), key=lambda x: x[1], reverse=True)[:10])
|
|||
|
|
|
|||
|
|
def _generate_markdown_report(self, report: dict, output_file: str):
|
|||
|
|
"""生成Markdown格式报告"""
|
|||
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write('# MySQL到达梦SQL兼容性检测报告\n\n')
|
|||
|
|
f.write(f'**扫描时间**: {report["scan_time"]}\n\n')
|
|||
|
|
|
|||
|
|
# 摘要
|
|||
|
|
f.write('## 检测摘要\n\n')
|
|||
|
|
f.write(f'- **总问题数**: {report["summary"]["total_issues"]}\n')
|
|||
|
|
f.write('\n### 按严重程度\n\n')
|
|||
|
|
f.write('| 严重程度 | 数量 |\n')
|
|||
|
|
f.write('|---------|------|\n')
|
|||
|
|
for severity, count in report['summary']['by_severity'].items():
|
|||
|
|
f.write(f'| {severity} | {count} |\n')
|
|||
|
|
|
|||
|
|
f.write('\n### 按问题类型\n\n')
|
|||
|
|
f.write('| 问题类型 | 数量 |\n')
|
|||
|
|
f.write('|---------|------|\n')
|
|||
|
|
for issue_type, count in list(report['summary']['by_type'].items())[:10]:
|
|||
|
|
f.write(f'| {issue_type} | {count} |\n')
|
|||
|
|
|
|||
|
|
f.write('\n### 问题最多的文件 (Top 10)\n\n')
|
|||
|
|
f.write('| 文件 | 问题数 |\n')
|
|||
|
|
f.write('|------|--------|\n')
|
|||
|
|
for file_path, count in report['summary']['by_file'].items():
|
|||
|
|
f.write(f'| {file_path} | {count} |\n')
|
|||
|
|
|
|||
|
|
# 详细问题列表
|
|||
|
|
f.write('\n## 详细问题清单\n\n')
|
|||
|
|
for issue in report['issues']:
|
|||
|
|
f.write(f'### {issue["issue_type"]} - {issue["severity"]}\n\n')
|
|||
|
|
f.write(f'**文件**: `{issue["file_path"]}:{issue["line_number"]}`\n\n')
|
|||
|
|
f.write(f'**问题描述**: {issue["description"]}\n\n')
|
|||
|
|
f.write(f'- **MySQL语法**: `{issue["mysql_pattern"]}`\n')
|
|||
|
|
f.write(f'- **达梦语法**: `{issue["dm_pattern"]}`\n')
|
|||
|
|
f.write(f'- **修复建议**: {issue["suggestion"]}\n\n')
|
|||
|
|
f.write('---\n\n')
|
|||
|
|
|
|||
|
|
def generate_fix_script(self, output_file: str = 'fix_compatibility.sh'):
|
|||
|
|
"""生成自动修复脚本(仅适用于简单替换)"""
|
|||
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write('#!/bin/bash\n')
|
|||
|
|
f.write('# MySQL到达梦SQL兼容性自动修复脚本\n')
|
|||
|
|
f.write('# 注意:此脚本仅处理简单替换,复杂情况需人工确认\n\n')
|
|||
|
|
|
|||
|
|
# 可自动修复的规则
|
|||
|
|
fixable_rules = {
|
|||
|
|
'NOW': ('NOW()', 'SYSDATE'),
|
|||
|
|
'AUTO_INCREMENT': ('AUTO_INCREMENT', 'IDENTITY'),
|
|||
|
|
'BOOLEAN_TRUE': ('\\bTRUE\\b', '1'),
|
|||
|
|
'BOOLEAN_FALSE': ('\\bFALSE\\b', '0'),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for rule_name, (old, new) in fixable_rules.items():
|
|||
|
|
f.write(f'# 修复 {rule_name}\n')
|
|||
|
|
f.write(f'find src -type f -name "*.xml" -o -name "*.java" | \\\n')
|
|||
|
|
f.write(f' xargs sed -i "s/{old}/{new}/g"\n\n')
|
|||
|
|
|
|||
|
|
f.write('echo "自动修复完成!请仔细检查修改内容。"\n')
|
|||
|
|
|
|||
|
|
print(f'自动修复脚本已生成: {output_file}')
|
|||
|
|
print('⚠️ 注意:使用前请先提交代码,修复脚本可能产生副作用')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
"""主函数"""
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
# 配置
|
|||
|
|
SOURCE_DIR = 'src'
|
|||
|
|
JSON_REPORT = 'check/reports/compatibility_report.json'
|
|||
|
|
FIX_SCRIPT = 'check/scripts/fix_compatibility.sh'
|
|||
|
|
|
|||
|
|
# 创建检测器
|
|||
|
|
checker = MySQLToDMCompatibilityChecker(SOURCE_DIR)
|
|||
|
|
|
|||
|
|
# 执行检测
|
|||
|
|
checker.check_all()
|
|||
|
|
|
|||
|
|
# 生成报告
|
|||
|
|
checker.generate_report(JSON_REPORT)
|
|||
|
|
|
|||
|
|
# 生成修复脚本
|
|||
|
|
checker.generate_fix_script(FIX_SCRIPT)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
main()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、常见SQL兼容性问题及修复示例
|
|||
|
|
|
|||
|
|
### 1. 分页查询
|
|||
|
|
|
|||
|
|
**MySQL写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectPage" resultType="...">
|
|||
|
|
SELECT * FROM abnormal_list
|
|||
|
|
LIMIT #{offset}, #{pageSize}
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**达梦写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectPage" resultType="...">
|
|||
|
|
SELECT * FROM abnormal_list
|
|||
|
|
LIMIT #{pageSize} OFFSET #{offset}
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**或者使用ROWNUM**(达梦7及以下):
|
|||
|
|
```xml
|
|||
|
|
<select id="selectPage" resultType="...">
|
|||
|
|
SELECT * FROM (
|
|||
|
|
SELECT t.*, ROWNUM AS rn FROM abnormal_list t WHERE ROWNUM <= #{offset} + #{pageSize}
|
|||
|
|
) WHERE rn > #{offset}
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. 日期函数
|
|||
|
|
|
|||
|
|
**MySQL写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectByCreateTime" resultType="...">
|
|||
|
|
SELECT * FROM abnormal_list
|
|||
|
|
WHERE create_time <= NOW()
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**达梦写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectByCreateTime" resultType="...">
|
|||
|
|
SELECT * FROM abnormal_list
|
|||
|
|
WHERE create_time <= SYSDATE
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. IF函数改为CASE WHEN
|
|||
|
|
|
|||
|
|
**MySQL写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectWithStatus" resultType="...">
|
|||
|
|
SELECT
|
|||
|
|
entname,
|
|||
|
|
IF(abnormal_date IS NULL, '正常', '异常') AS status
|
|||
|
|
FROM abnormal_list
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**达梦写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectWithStatus" resultType="...">
|
|||
|
|
SELECT
|
|||
|
|
entname,
|
|||
|
|
CASE WHEN abnormal_date IS NULL THEN '正常' ELSE '异常' END AS status
|
|||
|
|
FROM abnormal_list
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. GROUP_CONCAT改为LISTAGG
|
|||
|
|
|
|||
|
|
**MySQL写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectGroupConcat" resultType="...">
|
|||
|
|
SELECT
|
|||
|
|
dept_id,
|
|||
|
|
GROUP_CONCAT(entname SEPARATOR ',') AS ent_names
|
|||
|
|
FROM abnormal_list
|
|||
|
|
GROUP BY dept_id
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**达梦写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectGroupConcat" resultType="...">
|
|||
|
|
SELECT
|
|||
|
|
dept_id,
|
|||
|
|
LISTAGG(entname, ',') WITHIN GROUP (ORDER BY entname) AS ent_names
|
|||
|
|
FROM abnormal_list
|
|||
|
|
GROUP BY dept_id
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5. 字符串拼接
|
|||
|
|
|
|||
|
|
**MySQL写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectFullName" resultType="...">
|
|||
|
|
SELECT
|
|||
|
|
CONCAT(last_name, first_name) AS full_name
|
|||
|
|
FROM users
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**达梦写法**(两种都支持):
|
|||
|
|
```xml
|
|||
|
|
<select id="selectFullName" resultType="...">
|
|||
|
|
SELECT
|
|||
|
|
last_name || first_name AS full_name
|
|||
|
|
FROM users
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
或
|
|||
|
|
```xml
|
|||
|
|
<select id="selectFullName" resultType="...">
|
|||
|
|
SELECT
|
|||
|
|
CONCAT(last_name, first_name) AS full_name
|
|||
|
|
FROM users
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 6. CAST类型转换
|
|||
|
|
|
|||
|
|
**MySQL写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectCast" resultType="...">
|
|||
|
|
SELECT CAST(amount AS SIGNED) AS int_amount
|
|||
|
|
FROM payments
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**达梦写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectCast" resultType="...">
|
|||
|
|
SELECT CAST(amount AS INT) AS int_amount
|
|||
|
|
FROM payments
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 7. DATE_FORMAT改为TO_CHAR
|
|||
|
|
|
|||
|
|
**MySQL写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectFormattedDate" resultType="...">
|
|||
|
|
SELECT
|
|||
|
|
DATE_FORMAT(create_time, '%Y-%m-%d') AS formatted_date
|
|||
|
|
FROM abnormal_list
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**达梦写法**:
|
|||
|
|
```xml
|
|||
|
|
<select id="selectFormattedDate" resultType="...">
|
|||
|
|
SELECT
|
|||
|
|
TO_CHAR(create_time, 'YYYY-MM-DD') AS formatted_date
|
|||
|
|
FROM abnormal_list
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、检测步骤
|
|||
|
|
|
|||
|
|
### 1. 准备阶段
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 创建报告目录
|
|||
|
|
mkdir -p check/reports
|
|||
|
|
mkdir -p check/scripts
|
|||
|
|
|
|||
|
|
# 统计需要检测的文件数量
|
|||
|
|
find src -name "*.xml" | wc -l
|
|||
|
|
find src -name "*.java" | wc -l
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 执行检测
|
|||
|
|
|
|||
|
|
**方式1:使用命令行快速扫描**
|
|||
|
|
```bash
|
|||
|
|
# Linux/Mac
|
|||
|
|
bash check/scripts/scan_mysql_syntax.sh > scan_results.txt
|
|||
|
|
|
|||
|
|
# Windows PowerShell
|
|||
|
|
powershell -ExecutionPolicy Bypass -File check/scripts/scan_mysql_syntax.ps1 > scan_results.txt
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**方式2:使用Python自动化检测**
|
|||
|
|
```bash
|
|||
|
|
# 运行检测脚本
|
|||
|
|
python check/scripts/mysql_to_dm_checker.py
|
|||
|
|
|
|||
|
|
# 查看报告
|
|||
|
|
cat check/reports/compatibility_report.json
|
|||
|
|
cat check/reports/compatibility_report.md
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 分析报告
|
|||
|
|
|
|||
|
|
根据报告中的问题优先级处理:
|
|||
|
|
|
|||
|
|
1. **HIGH(高危)**
|
|||
|
|
- 影响SQL执行的关键语法
|
|||
|
|
- 必须修复才能正常运行
|
|||
|
|
|
|||
|
|
2. **MEDIUM(中危)**
|
|||
|
|
- 可能有兼容性问题
|
|||
|
|
- 建议修复
|
|||
|
|
|
|||
|
|
3. **LOW(低危)**
|
|||
|
|
- 次要兼容性问题
|
|||
|
|
- 可选修复
|
|||
|
|
|
|||
|
|
### 4. 修复问题
|
|||
|
|
|
|||
|
|
**自动修复**(谨慎使用):
|
|||
|
|
```bash
|
|||
|
|
# 先提交代码
|
|||
|
|
git add .
|
|||
|
|
git commit -m "备份:修复前的代码"
|
|||
|
|
|
|||
|
|
# 执行自动修复脚本
|
|||
|
|
bash check/scripts/fix_compatibility.sh
|
|||
|
|
|
|||
|
|
# 检查修复结果
|
|||
|
|
git diff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**手动修复**:
|
|||
|
|
根据报告中的文件路径和行号,逐一修复兼容性问题。
|
|||
|
|
|
|||
|
|
### 5. 验证修复
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 重新扫描确认问题已修复
|
|||
|
|
python check/scripts/mysql_to_dm_checker.py
|
|||
|
|
|
|||
|
|
# 对比修复前后的报告
|
|||
|
|
diff check/reports/before_fix.json check/reports/after_fix.json
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、输出报告
|
|||
|
|
|
|||
|
|
### JSON格式报告(兼容性检测机器报告)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"scan_time": "2025-01-05T14:30:00",
|
|||
|
|
"summary": {
|
|||
|
|
"total_issues": 45,
|
|||
|
|
"by_severity": {
|
|||
|
|
"HIGH": 23,
|
|||
|
|
"MEDIUM": 15,
|
|||
|
|
"LOW": 7
|
|||
|
|
},
|
|||
|
|
"by_type": {
|
|||
|
|
"NOW": 18,
|
|||
|
|
"LIMIT": 12,
|
|||
|
|
"IF_FUNCTION": 8,
|
|||
|
|
"AUTO_INCREMENT": 5,
|
|||
|
|
"BOOLEAN": 2
|
|||
|
|
},
|
|||
|
|
"by_file": {
|
|||
|
|
"src/main/resources/mybatis/mapper/aiccs/abnormal/AbnormalListMapper.xml": 8,
|
|||
|
|
"src/main/java/com/chinaweal/aiccs/aiccs/seriousillegal/mapper/SeriousIllegalMapper.java": 5
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"issues": [
|
|||
|
|
{
|
|||
|
|
"issue_type": "NOW",
|
|||
|
|
"severity": "HIGH",
|
|||
|
|
"file_path": "src/main/resources/mybatis/mapper/aiccs/abnormal/AbnormalListMapper.xml",
|
|||
|
|
"line_number": 45,
|
|||
|
|
"mysql_pattern": "NOW()",
|
|||
|
|
"dm_pattern": "SYSDATE",
|
|||
|
|
"description": "达梦不支持NOW()函数",
|
|||
|
|
"suggestion": "将 NOW() 改为 SYSDATE"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Markdown格式报告(兼容性检测人工报告)
|
|||
|
|
|
|||
|
|
参见工具生成的Markdown格式报告。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、注意事项
|
|||
|
|
|
|||
|
|
1. **数据库版本差异**
|
|||
|
|
- 达梦8与达梦7语法有差异
|
|||
|
|
- 确认目标数据库版本
|
|||
|
|
|
|||
|
|
2. **索引提示**
|
|||
|
|
- 达梦不支持MySQL的索引提示语法
|
|||
|
|
- 删除后可能影响查询性能
|
|||
|
|
|
|||
|
|
3. **GROUP BY严格模式**
|
|||
|
|
- 达梦要求SELECT中的非聚合字段必须在GROUP BY中
|
|||
|
|
- MySQL的宽松模式在达梦中会报错
|
|||
|
|
|
|||
|
|
4. **字符串拼接**
|
|||
|
|
- 达梦支持 `||` 和 `CONCAT()` 两种方式
|
|||
|
|
- 建议统一使用 `CONCAT()` 以提高代码可读性
|
|||
|
|
|
|||
|
|
5. **NULL处理**
|
|||
|
|
- 达梦的NULL处理与MySQL可能有细微差异
|
|||
|
|
- 注意空字符串 `''` 和 `NULL` 的区别
|
|||
|
|
|
|||
|
|
6. **自动生成的SQL**
|
|||
|
|
- MyBatis-Plus等框架生成的SQL也需要检测
|
|||
|
|
- 可能需要配置框架的数据库方言
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九、相关文件
|
|||
|
|
|
|||
|
|
- Mapper XML路径: `src/main/resources/mybatis/mapper/{数据源}/**/*.xml`
|
|||
|
|
- 扫描脚本: `check/scripts/scan_mysql_syntax.sh` / `scan_mysql_syntax.ps1`
|
|||
|
|
- 检测工具: `check/scripts/mysql_to_dm_checker.py`
|
|||
|
|
- 检测报告: `check/reports/compatibility_report.json` / `compatibility_report.md`
|
|||
|
|
- 修复脚本: `check/scripts/fix_compatibility.sh`
|