本工具用于比较 Oracle PL/SQL 原始代码与转换后代码(PostgreSQL 或 OceanBase)之间的差异,输出 Word 级别的变更统计和归一化代码文件。
提供两种比对方式:
- Word级比对(
sql_diff.py):基于函数/过程拆分 + Word对齐的差异统计,更精确
Word级比对提供两种归一化方案:
- 方案1 语义归一化:忽略 Oracle→PG/OB 语法等价替换(DECLARE/END函数名/NUMBER→NUMERIC/DEFAULT→:=/NVL→COALESCE/SYSDATE→CURRENT_DATE/ROWNUM→ROW_NUMBER/AS→IS),只统计真正的语义变化
- 方案2 空白归一化:仅去除注释和空白,保留所有语法差异
| 项目 | 要求 |
|---|---|
| 操作系统 | Windows 10/11 |
| Python | 3.8+(推荐 3.11) |
| Git | 可选,参与项目开发时建议安装 |
| Shell/PowerShell | 用于执行命令 |
diff_code/
├── sql_diff.py # Word级比对主脚本(推荐使用)
├── README.md # 开源说明和快速开始
├── LICENSE # 开源许可证
├── examples/ # 示例SQL文件
├── tests/ # 单元测试
├── 操作手册.md # 本文件
└── output/ # 输出目录(自动创建)
├── diff_summary.csv # 汇总CSV(两种方案交错,由"方案"列区分)
├── diff_detail.csv # 函数/过程级明细CSV(两种方案交错,由"方案"列区分)
├── normalized/ # 归一化代码文件
│ ├── XXX_old_s1.txt # 原始代码方案1归一化
│ ├── XXX_new_s1.txt # 新代码方案1归一化
│ ├── XXX_old_s2.txt # 原始代码方案2归一化
│ └── XXX_new_s2.txt # 新代码方案2归一化
├── old_body/ # 提取的Oracle body文件
│ └── XXX_body.sql
└── new_body/ # 提取的新代码body文件
└── XXX_body.sql
当前版本通过命令行参数传入路径,不需要修改脚本源码:
python3 sql_diff.py --old ./examples/old --new ./examples/new --output ./output参数说明:
--old:Oracle 原始代码目录--new:转换后代码目录,支持 PostgreSQL/OceanBase 等目标代码--output:输出目录,默认output
python3 sql_diff.py --old ./oracle --new ./converted --output ./output脚本会自动遍历 --new 指定目录下所有文件,在 --old 指定目录中按文件名(去掉扩展名、大写)查找配对,逐文件执行:
- 提取 Oracle 文件的 PACKAGE BODY 部分
- 提取新代码文件的 PACKAGE BODY 部分(PG/OB文件也可能含spec+body)
- 按函数/过程拆分(支持无参子程序和嵌套子程序)
- 分别用方案1和方案2归一化
- Word级对齐比对
- 输出统计和归一化文件,同时保存提取的body文件
注意:OB文件命名可能带
-body/_body/-BODY后缀(如PKG_LIFE_PUBLIC-body.sql),运行前需先去掉后缀使其与Oracle文件名匹配。批量重命名命令:Get-ChildItem "OB目录\*" | ForEach-Object { $newName = $_.Name -replace '[-_]body(?=\.sql$)', '' -replace '[-_]BODY(?=\.SQL$)', '' if ($newName -ne $_.Name) { Rename-Item $_.FullName -NewName $newName } }
在Python交互环境或脚本中:
import sys
sys.path.insert(0, r'.')
from sql_diff import read_file, extract_body, compute_scheme_diff, normalize_semantic, normalize_whitespace_only
# 1. 读取文件
old_content = read_file(r'D:\项目\...\plsql\PKG_LIFE_PUBLIC.sql')
new_content = read_file(r'D:\项目\...\OB变更后\body\PKG_LIFE_PUBLIC.sql')
# 2. 提取body(两侧均需提取,Oracle和新代码文件都可能含spec+body)
pkg_name = 'PKG_LIFE_PUBLIC'
old_body = extract_body(old_content, pkg_name)
new_body = extract_body(new_content, pkg_name)
# 3. 方案1比对(语义归一化)
ow1, nw1, cw1, units1, fc1, cc1, norm_old1, norm_new1 = compute_scheme_diff(
old_body, new_body, normalize_semantic)
ratio1 = round(cw1 / ow1 * 100, 2) if ow1 > 0 else 0
print(f'方案1: 变更Word比 {ratio1}%,变更函数 {cc1}/{fc1}')
# 4. 方案2比对(空白归一化)
ow2, nw2, cw2, units2, fc2, cc2, norm_old2, norm_new2 = compute_scheme_diff(
old_body, new_body, normalize_whitespace_only)
ratio2 = round(cw2 / ow2 * 100, 2) if ow2 > 0 else 0
print(f'方案2: 变更Word比 {ratio2}%,变更函数 {cc2}/{fc2}')from sql_diff import read_file, extract_body, split_units, normalize_semantic, normalize_whitespace_only
content = read_file(r'D:\项目\...\PKG_LIFE_PUBLIC.sql')
body = extract_body(content, 'PKG_LIFE_PUBLIC')
# 保存body
with open('PKG_LIFE_PUBLIC_body.txt', 'w', encoding='utf-8') as f:
f.write(body)
# 归一化
units = split_units(body)
for u in units:
unit_lines = u.get('own_lines', u['lines'])
tokens_s1, norm_s1 = normalize_semantic(unit_lines)
tokens_s2, norm_s2 = normalize_whitespace_only(unit_lines)
parent = f", parent={u['parent']}" if u.get('parent') else ""
print(f"{u['type']} {u['name']} depth={u.get('depth', 0)}{parent}: S1={len(tokens_s1)} tokens, S2={len(tokens_s2)} tokens")python3 -m unittest discover -s tests测试会验证语义归一化、PACKAGE BODY 提取、无参/嵌套子程序拆分和主流程 CSV 输出。
两种方案的汇总数据合并在同一个文件中,由"方案"列区分。
| 列名 | 说明 |
|---|---|
| 文件名 | 比对的新代码文件名 |
| 原始代码Word数 | Oracle body归一化后的Word总数 |
| 新代码Word数 | 新代码body归一化后的Word总数 |
| 变更Word数 | 两侧不同的Word数 |
| Word变更比例 | 变更Word数 / 原始代码Word数 × 100% |
| 原始函数/过程数 | Oracle body中拆分出的函数/过程总数 |
| 变更函数/过程数 | 有Word差异的函数/过程数 |
| 变更函数/过程比例 | 变更函数数 / 原始函数数 × 100% |
| 方案 | 方案1(语义归一化) 或 方案2(空白归一化) |
两种方案的明细数据合并在同一个文件中,由"方案"列区分。
| 列名 | 说明 |
|---|---|
| 文件名 | 比对的新代码文件名 |
| 函数/过程名 | 函数或过程名称 |
| 类型 | FUNCTION 或 PROCEDURE |
| 原始Word数 | 该函数/过程在Oracle侧的Word数 |
| 新代码Word数 | 该函数/过程在新代码侧的Word数 |
| 变更Word数 | 不同的Word数 |
| 变更比例 | 变更Word数 / 原始Word数 × 100% |
| 匹配状态 | matched(按名称、层级、父单元配对)/ old_only(仅Oracle有)/ new_only(仅新代码有) |
| 方案 | 方案1(语义归一化) 或 方案2(空白归一化) |
每个比对文件生成4个归一化文本:
XXX_old_s1.txt:Oracle body经方案1归一化,按函数/过程分段XXX_new_s1.txt:新代码body经方案1归一化,按函数/过程分段XXX_old_s2.txt:Oracle body经方案2归一化XXX_new_s2.txt:新代码body经方案2归一化
可用于人工对比审查。
每个比对文件生成2个body提取文件:
XXX_body.sql(old_body下):Oracle文件提取的PACKAGE BODY部分XXX_body.sql(new_body下):新代码文件提取的PACKAGE BODY部分
Oracle文件 (.sql) 新代码文件 (.sql / PG或OB)
│ │
▼ ▼
extract_body() extract_body()
提取PACKAGE BODY部分 提取PACKAGE BODY部分
(跳过spec声明) (跳过spec声明)
│ │
▼ ▼
split_units() split_units()
按FUNCTION/PROCEDURE拆分 按FUNCTION/PROCEDURE拆分
│ │
▼ ▼
match_units() ———— 按名称+层级+父单元配对 ─┘
│
├──── 方案1: normalize_semantic() ──→ compute_word_diff_tokens()
│ (去除等价语法差异) (difflib.SequenceMatcher对齐)
│
└──── 方案2: normalize_whitespace_only() → compute_word_diff_tokens()
(仅去注释空白) (difflib.SequenceMatcher对齐)
│
▼
输出: CSV汇总 + CSV明细 + 归一化文本
Oracle的PACKAGE文件通常包含声明(spec)和包体(body)两部分:
CREATE OR REPLACE PACKAGE PKG_NAME AS -- spec开始
...常量、类型声明...
END PKG_NAME; -- spec结束
CREATE OR REPLACE PACKAGE BODY PKG_NAME AS -- body开始
...函数/过程实现...
END PKG_NAME; -- body结束PG/OB转换后的文件也可能包含spec+body,因此新旧两侧均调用 extract_body() 提取body部分进行比较。
匹配规则:
- 优先匹配
CREATE OR REPLACE PACKAGE BODY PKG_NAME IS|AS(同一行) - 同样支持
CREATE OR REPLACE EDITIONABLE PACKAGE BODY PKG_NAME IS|AS - 其次匹配
CREATE OR REPLACE [EDITIONABLE] PACKAGE BODY PKG_NAME后续行有独立的IS或AS - 找不到body时回退到全文(适用于非PACKAGE类型的文件)
结束标记:从文件末尾向前搜索 END PKG_NAME;,搜索范围限制在 body起始行之后,避免误匹配 spec 的 END。
Schema前缀:支持PG/OB文件中带schema前缀的包名引用,如 "TAIPINGLIFEMAIN"."PKG_NAME"。
支持以下函数/过程签名形式:
- 带参数子程序:
FUNCTION F(...)、PROCEDURE P(...) - 多行参数子程序:函数/过程名独占一行,后续数行出现参数括号
- 无参子程序:
FUNCTION F RETURN NUMBER IS、PROCEDURE P IS
签名解析:跟踪括号深度,遇到 ) 且深度归零时确定签名结束。
Body开始:签名后查找 IS/AS 或 RETURN type IS/AS 行。
前向声明过滤:只有签名、没有 IS/AS 实现体且以分号结束的声明会被跳过。
Body结束:跟踪 BEGIN/END 嵌套深度,深度归零时确定函数结束。END IF;、END LOOP;、END CASE; 按控制块处理,不作为函数/过程结束。
嵌套子程序:在父单元 BEGIN 之前出现的 FUNCTION/PROCEDURE 会递归提取为子单元。父单元统计时使用自己的代码行,不重复统计嵌套子单元;子单元会单独计入函数/过程数。
按 函数/过程名 + 嵌套层级 + 父单元名 将新旧函数/过程配对。同一键多次出现时按出现顺序一一对应。无法配对的标记为 old_only 或 new_only。
这样可以区分顶层函数和嵌套函数,也可以避免不同父单元下的同名嵌套子程序误配对。
预处理(两种方案共用):
- 去除块注释
/* ... */ - 去除行注释
-- ... - 去除空行
- 转大写
方案1 语义归一化(额外处理):
| 原始 | 归一化为 | 说明 |
|---|---|---|
DECLARE |
删除 | PG在body内加DECLARE,Oracle不加 |
END func_name; |
END; |
Oracle带函数名,PG不带 |
NUMBER |
NUMERIC |
Oracle→PG类型映射 |
DEFAULT |
:= |
Oracle→PG默认值语法 |
NVL |
COALESCE |
Oracle→PG函数映射 |
SYSDATE |
CURRENT_DATE |
Oracle→PG日期函数映射 |
ROWNUM |
ROW_NUMBER |
Oracle→PG伪列映射 |
AS(独立行) |
IS |
Oracle用AS/IS,PG统一用IS |
方案2 空白归一化:不做上述等价替换,只做预处理。
使用 difflib.SequenceMatcher 对归一化后的Word序列做最长公共子序列对齐。
Word分词:re.findall(r'\w+|[^\w\s]', line),即标识符/关键字 + 操作符/标点分别作为一个Word。
变更计算:
replace:取 max(旧侧Word数, 新侧Word数)delete:旧侧Word数insert:新侧Word数
通过命令行参数指定目录:
python3 sql_diff.py --old ./oracle --new ./converted --output ./outputimport sys
sys.path.insert(0, r'.')
from sql_diff import *
old_path = r'D:\your\oracle\dir\YOUR_PKG.sql'
new_path = r'D:\your\pg\dir\YOUR_PKG.sql'
pkg_name = 'YOUR_PKG'
result = process_file(old_path, new_path, pkg_name)
ow, nw, cw, units = result['scheme1']
ratio = round(cw / ow * 100, 2) if ow > 0 else 0
print(f'变更比例: {ratio}%')在 normalize_semantic() 函数中添加 re.sub 行:
def normalize_semantic(lines):
preprocessed = preprocess_lines(lines)
normalized = []
for line in preprocessed:
s = line
# 在此处添加新的替换规则,例如:
# s = re.sub(r'\bORA_FUNC\b', 'PG_FUNC', s, flags=re.IGNORECASE)
s = re.sub(r'\bDECLARE\b', '', s, flags=re.IGNORECASE)
# ... 其余规则对于独立函数(非PACKAGE),extract_body 找不到 PACKAGE BODY 时会回退返回全文,因此也能正常比对。
配对逻辑:取新代码文件名去掉扩展名后大写,在Oracle目录中搜索同名的文件。如果Oracle文件名和新代码文件名不同,需要手动指定路径并用 process_file() 比对。
OB文件可能带
-body/_body后缀,需先批量重命名去掉后缀(见5.1.1节说明)。
Oracle目录中没有找到与新代码文件同名的文件。检查文件名是否一致(忽略扩展名和大小写),或是否需要去掉文件名中的 -body/_body 后缀。
新代码文件中的函数/过程数多于Oracle文件(新增了函数),新增的每个函数都算作"变更",所以比例可能超过100%。
说明该文件的大量差异来自 Oracle→PG/OB 的语法等价替换(如 NVL→COALESCE、DECLARE、END函数名等),而非真正的业务逻辑变更。方案1更能反映真实变更程度。
- 打开
output/normalized/下对应的XXX_old_s1.txt和XXX_new_s1.txt - 搜索函数名,手动对比两段归一化代码
- 或使用文本比对工具(如 Beyond Compare)对比两个归一化文件
准备 Oracle 原始代码目录、转换后代码目录和输出目录。路径通过命令行参数传入,不需要修改脚本源码。
OB文件预处理:如果新代码文件名带
-body/_body/-BODY后缀,需先批量重命名:Get-ChildItem "你的新代码目录\*" | ForEach-Object { $newName = $_.Name -replace '[-_]body(?=\.sql$)', '' -replace '[-_]BODY(?=\.SQL$)', '' if ($newName -ne $_.Name) { Rename-Item $_.FullName -NewName $newName } }
# 使用完整Python路径(如果系统Python不是3.8+),注意替换为你的实际路径
C:\Path\To\Python311\python.exe sql_diff.py --old .\oracle --new .\converted --output .\output
# 或直接
python sql_diff.py --old .\oracle --new .\converted --output .\output# 汇总结果
Start-Process output\diff_summary.csv
# 函数级明细
Start-Process output\diff_detail.csv
# 归一化代码
dir output\normalized\
# 提取的body文件
dir output\old_body\
dir output\new_body\重点关注:
- 方案1 Word变更比例 > 10% 的文件 → 可能存在较多业务逻辑变更
- 方案1 变更函数比例 < 20% 但方案2 = 100% 的文件 → 大部分是语法转换差异,非业务变更
new_only状态的函数 → 新代码新增函数,需确认是否合理old_only状态的函数 → Oracle有但新代码缺失,需确认是否遗漏