Skip to content

Latest commit

 

History

History
445 lines (323 loc) · 16.6 KB

操作手册.md

File metadata and controls

445 lines (323 loc) · 16.6 KB

PL/SQL → PostgreSQL/OceanBase 代码变更比对 操作手册

1. 概述

本工具用于比较 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 空白归一化:仅去除注释和空白,保留所有语法差异

2. 环境要求

项目 要求
操作系统 Windows 10/11
Python 3.8+(推荐 3.11)
Git 可选,参与项目开发时建议安装
Shell/PowerShell 用于执行命令

3. 目录结构

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

4. 配置路径

当前版本通过命令行参数传入路径,不需要修改脚本源码:

python3 sql_diff.py --old ./examples/old --new ./examples/new --output ./output

参数说明:

  • --old:Oracle 原始代码目录
  • --new:转换后代码目录,支持 PostgreSQL/OceanBase 等目标代码
  • --output:输出目录,默认 output

5. 使用方法

5.1 Word级比对(推荐)

5.1.1 比对整个文件夹

python3 sql_diff.py --old ./oracle --new ./converted --output ./output

脚本会自动遍历 --new 指定目录下所有文件,在 --old 指定目录中按文件名(去掉扩展名、大写)查找配对,逐文件执行:

  1. 提取 Oracle 文件的 PACKAGE BODY 部分
  2. 提取新代码文件的 PACKAGE BODY 部分(PG/OB文件也可能含spec+body)
  3. 按函数/过程拆分(支持无参子程序和嵌套子程序)
  4. 分别用方案1和方案2归一化
  5. Word级对齐比对
  6. 输出统计和归一化文件,同时保存提取的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 }
}

5.1.2 比对单个文件

在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}')

5.1.3 只提取body和归一化(不比对)

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")

5.2 运行测试

python3 -m unittest discover -s tests

测试会验证语义归一化、PACKAGE BODY 提取、无参/嵌套子程序拆分和主流程 CSV 输出。


6. 输出说明

6.1 汇总CSV(diff_summary.csv)

两种方案的汇总数据合并在同一个文件中,由"方案"列区分。

列名 说明
文件名 比对的新代码文件名
原始代码Word数 Oracle body归一化后的Word总数
新代码Word数 新代码body归一化后的Word总数
变更Word数 两侧不同的Word数
Word变更比例 变更Word数 / 原始代码Word数 × 100%
原始函数/过程数 Oracle body中拆分出的函数/过程总数
变更函数/过程数 有Word差异的函数/过程数
变更函数/过程比例 变更函数数 / 原始函数数 × 100%
方案 方案1(语义归一化) 或 方案2(空白归一化)

6.2 明细CSV(diff_detail.csv)

两种方案的明细数据合并在同一个文件中,由"方案"列区分。

列名 说明
文件名 比对的新代码文件名
函数/过程名 函数或过程名称
类型 FUNCTION 或 PROCEDURE
原始Word数 该函数/过程在Oracle侧的Word数
新代码Word数 该函数/过程在新代码侧的Word数
变更Word数 不同的Word数
变更比例 变更Word数 / 原始Word数 × 100%
匹配状态 matched(按名称、层级、父单元配对)/ old_only(仅Oracle有)/ new_only(仅新代码有)
方案 方案1(语义归一化) 或 方案2(空白归一化)

6.3 归一化文件(output/normalized/)

每个比对文件生成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归一化

可用于人工对比审查。

6.4 Body提取文件(output/old_body/、output/new_body/)

每个比对文件生成2个body提取文件:

  • XXX_body.sql(old_body下):Oracle文件提取的PACKAGE BODY部分
  • XXX_body.sql(new_body下):新代码文件提取的PACKAGE BODY部分

7. 处理流程详解

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明细 + 归一化文本

7.1 extract_body — 提取包体

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部分进行比较。

匹配规则:

  1. 优先匹配 CREATE OR REPLACE PACKAGE BODY PKG_NAME IS|AS(同一行)
  2. 同样支持 CREATE OR REPLACE EDITIONABLE PACKAGE BODY PKG_NAME IS|AS
  3. 其次匹配 CREATE OR REPLACE [EDITIONABLE] PACKAGE BODY PKG_NAME 后续行有独立的 IS 或 AS
  4. 找不到body时回退到全文(适用于非PACKAGE类型的文件)

结束标记:从文件末尾向前搜索 END PKG_NAME;,搜索范围限制在 body起始行之后,避免误匹配 spec 的 END。

Schema前缀:支持PG/OB文件中带schema前缀的包名引用,如 "TAIPINGLIFEMAIN"."PKG_NAME"。

7.2 split_units — 拆分函数/过程

支持以下函数/过程签名形式:

  • 带参数子程序: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 会递归提取为子单元。父单元统计时使用自己的代码行,不重复统计嵌套子单元;子单元会单独计入函数/过程数。

7.3 match_units — 函数配对

按 函数/过程名 + 嵌套层级 + 父单元名 将新旧函数/过程配对。同一键多次出现时按出现顺序一一对应。无法配对的标记为 old_only 或 new_only。

这样可以区分顶层函数和嵌套函数,也可以避免不同父单元下的同名嵌套子程序误配对。

7.4 归一化

预处理(两种方案共用):

  1. 去除块注释 /* ... */
  2. 去除行注释 -- ...
  3. 去除空行
  4. 转大写

方案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 空白归一化:不做上述等价替换,只做预处理。

7.5 Word级比对

使用 difflib.SequenceMatcher 对归一化后的Word序列做最长公共子序列对齐。

Word分词:re.findall(r'\w+|[^\w\s]', line),即标识符/关键字 + 操作符/标点分别作为一个Word。

变更计算:

  • replace:取 max(旧侧Word数, 新侧Word数)
  • delete:旧侧Word数
  • insert:新侧Word数

8. 自定义比对

8.1 比对不同目录的文件

通过命令行参数指定目录:

python3 sql_diff.py --old ./oracle --new ./converted --output ./output

8.2 只比对指定文件

import 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}%')

8.3 添加新的语义等价替换规则

在 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)
        # ... 其余规则

8.4 处理非PACKAGE文件

对于独立函数(非PACKAGE),extract_body 找不到 PACKAGE BODY 时会回退返回全文,因此也能正常比对。


9. 常见问题

Q1: 文件名不匹配怎么办?

配对逻辑:取新代码文件名去掉扩展名后大写,在Oracle目录中搜索同名的文件。如果Oracle文件名和新代码文件名不同,需要手动指定路径并用 process_file() 比对。

OB文件可能带 -body/_body 后缀,需先批量重命名去掉后缀(见5.1.1节说明)。

Q2: 输出显示 NOT_FOUND?

Oracle目录中没有找到与新代码文件同名的文件。检查文件名是否一致(忽略扩展名和大小写),或是否需要去掉文件名中的 -body/_body 后缀。

Q3: 变更函数/过程比例超过100%?

新代码文件中的函数/过程数多于Oracle文件(新增了函数),新增的每个函数都算作"变更",所以比例可能超过100%。

Q4: 方案1和方案2差异很大说明什么?

说明该文件的大量差异来自 Oracle→PG/OB 的语法等价替换(如 NVL→COALESCE、DECLARE、END函数名等),而非真正的业务逻辑变更。方案1更能反映真实变更程度。

Q5: 如何查看某个函数具体改了什么?

  1. 打开 output/normalized/ 下对应的 XXX_old_s1.txt 和 XXX_new_s1.txt
  2. 搜索函数名,手动对比两段归一化代码
  3. 或使用文本比对工具(如 Beyond Compare)对比两个归一化文件

10. 完整运行步骤(从零开始)

Step 1: 准备目录路径

准备 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 }
}

Step 2: 运行比对

# 使用完整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

Step 3: 查看结果

# 汇总结果
Start-Process output\diff_summary.csv

# 函数级明细
Start-Process output\diff_detail.csv

# 归一化代码
dir output\normalized\

# 提取的body文件
dir output\old_body\
dir output\new_body\

Step 4: 人工审查

重点关注:

  • 方案1 Word变更比例 > 10% 的文件 → 可能存在较多业务逻辑变更
  • 方案1 变更函数比例 < 20% 但方案2 = 100% 的文件 → 大部分是语法转换差异,非业务变更
  • new_only 状态的函数 → 新代码新增函数,需确认是否合理
  • old_only 状态的函数 → Oracle有但新代码缺失,需确认是否遗漏