基于语法树的SQL自动改写工具开发系列(2)-使用PYTHON进行简单SQL改写的开发实战

一、前言

前面一篇写了如何搭建环境,本文接着讲怎么使用antlr4进行开发。

二、实战

根据上一篇,基于语法树的SQL自动改写工具开发系列(1)-离线安装语法树解析工具antlr4-DA-技术分享-M版,先在本地部署好开发环境。

DEMO 1

写一段期望改写的原始SQL,使用pygrun进行解析
比如在原生PG中不支持ORACLE中的table()函数,但有替代的unnest改写方式
ORACLE:

SELECT A FROM TABLE(VAR) T

PG:

SELECT A FROM (SELECT * FROM UNNEST(VAR) COLUMN_VALUE) T

要实现的改写规则为,找到table函数的整个表达式,并取出它的入参,将table函数表达式的节点替换成(SELECT * FROM UNNEST(入参) COLUMN_VALUE)
语法树:

C:\antlr>pygrun PlSql sql_script  --tree
SELECT A FROM TABLE(VAR) T;
^Z
(sql_script
   (unit_statement
      (data_manipulation_language_statements
         (select_statement
            (select_only_statement
               (subquery
                  (subquery_basic_elements
                     (query_block SELECT
                        (selected_list
                           (select_list_elements
                              (expression
                                 (logical_expression
                                    (unary_logical_expression
                                       (multiset_expression
                                          (relational_expression
                                             (compound_expression
                                                (concatenation
                                                   (model_expression
                                                      (unary_expression
                                                         (atom
                                                            (general_element
                                                               (general_element_part
                                                                  (id_expression
                                                                     (regular_id A))))))))))))))))
                        (from_clause FROM
                           (table_ref_list
                              (table_ref
                                 (table_ref_aux
                                    (table_ref_aux_internal
                                       (dml_table_expression_clause
                                          (table_collection_expression TABLE (
                                             (expression
                                                (logical_expression
                                                   (unary_logical_expression
                                                      (multiset_expression
                                                         (relational_expression
                                                            (compound_expression
                                                               (concatenation
                                                                  (model_expression
                                                                     (unary_expression
                                                                        (atom
                                                                           (general_element
                                                                              (general_element_part
                                                                                 (id_expression
                                                                                    (regular_id VAR)))))))))))))) ))))
                                 (table_alias
                                    (identifier
                                       (id_expression
                                          (regular_id T))))))))))))))) ; <EOF>)

C:\antlr>

以下为一个完整且可以运行的demo代码,用于说明如何进行语法改写

from antlr4 import FileStream, CommonTokenStream
from PlSqlLexer import PlSqlLexer
from PlSqlParser import PlSqlParser
from PlSqlParserVisitor import PlSqlParserVisitor
from antlr4.TokenStreamRewriter import TokenStreamRewriter

class SQLTransformer(PlSqlParserVisitor):
    def __init__(self, token_stream):
        super().__init__()
        self.rewriter = TokenStreamRewriter(token_stream)

    def visitDml_table_expression_clause(self, ctx: PlSqlParser.Dml_table_expression_clauseContext):
        if ctx.table_collection_expression() and ctx.table_collection_expression().getText().startswith('table'):
            argument = ctx.table_collection_expression().expression().getText()
            new_text = "(select * from unnest({}) column_value)".format(argument)
            self.rewriter.replace(TokenStreamRewriter.DEFAULT_PROGRAM_NAME, ctx.table_collection_expression().start.tokenIndex, ctx.table_collection_expression().stop.tokenIndex, new_text)
        return self.visitChildren(ctx)

def main(input_file, output_file):
    input_stream = FileStream(input_file, encoding='utf-8')
    lexer = PlSqlLexer(input_stream)
    stream = CommonTokenStream(lexer)
    parser = PlSqlParser(stream)
    tree = parser.sql_script()

    transformer = SQLTransformer(stream)
    transformer.visit(tree)
    output_text = transformer.rewriter.getDefaultText()
    with open(output_file, 'w', encoding='utf-8',newline='') as f:
        f.write(output_text)

if __name__ == '__main__':
    input_file = 'input.sql'
    output_file = 'output.sql'
    main(input_file, output_file)

其中def main内的代码基本可以固定,我们直接看class SQLTransformer里的def visitDml_table_expression_clause
visitDml_table_expression_clause其实是PlSqlParserVisitor.py里面的一个def,这个def的名称由visit加上节点名组成,也就是说,语法树中的每一个节点,都有一个对应的visit。而原本PlSqlParserVisitor.py里的每个visit里面都是空的,直接就return出去了:

def  visitDml_table_expression_clause(self, ctx:PlSqlParser.Dml_table_expression_clauseContext):

return  self.visitChildren(ctx)

我们自己写的这个visit就是实现了里面的具体内容。

如果我们需要修改某个语法,可以从语法树中,找到这个语法相关的上下文的最小节点,以本文前面输出的语法树为例,就应该是 (table_collection_expression TABLE (,所以理论上,我们再写个visitTable_collection_expression就好了,但本文的demo代码是从visitDml_table_expression_clause开始,是为了说明如何引用当前节点的下级节点

if ctx.table_collection_expression() and ctx.table_collection_expression().getText().startswith('table')

这句是一个判断,作用是,判断当前节点下,是否存在table_collection_expression这个节点,我们可以去对比语法树,如果没有使用table函数,是不会有这个节点的;第二个条件就是,获取table_collection_expression这个节点的文本,判断它是不是使用 table开始。这里要注意,如果不写第一个条件,在没有table_collection_expression节点时,对它执行getText会报错。

argument = ctx.table_collection_expression().expression().getText()

这里是取出table_collection_expression的下一个叫expression的节点,对照语法树可以看到,虽然里面有很多层,但这个节点实际只包含VAR这个文本,因此这里就可以得到argument="VAR",即前文例子中,table函数的入参。

new_text = "(select * from unnest({}) column_value)".format(argument)

这一句很好理解,就是格式化一个字符串,把argument的值替换{},得到(select * from unnest(VAR) column_value)

self.rewriter.replace(TokenStreamRewriter.DEFAULT_PROGRAM_NAME, ctx.table_collection_expression().start.tokenIndex, ctx.table_collection_expression().stop.tokenIndex, new_text)

这一句就是最关键的,self.rewriter是前面定义的TokenStreamRewriter(token_stream),在TokenStreamRewriter里面,可以支持对节点的替换、删除、增加等操作。
TokenStreamRewriter.replace有4个入参,分别为程序名,开始位置、结束位置、需要替换成的文本。
程序名一般固定使用TokenStreamRewriter.DEFAULT_PROGRAM_NAME就行,如果期望一次解析,就能做多种替换,比如同时生成支持PG和MYSQL的两种语法,就可以在这里设置程序名,针对不同的程序名写不同的规则。
开始位置和结束位置,可以使用对应节点的start.tokenIndexstop.tokenIndex
至此,我们就完成了一个改写规则的开发。

DEMO 2

如果需要在一次语法树解析中就完成多种规则的执行,可以再添加几个def visit,比如我们再针对create type语句来进行改写。

ORACLE:

CREATE OR REPLACE TYPE TY_TEST AS OBJECT(COL1 INT,COL2 VARCHAR(20));

PG:

CREATE TYPE TY_TEST AS (COL1 INT,COL2 VARCHAR(20));

改写规则为,对于create_type的语法节点,将create or replace 改为create,并且删除object

语法树:

C:\antlr>pygrun PlSql sql_script  --tree
CREATE OR REPLACE TYPE TY_TEST AS OBJECT(COL1 INT,COL2 VARCHAR(20));
^Z
(sql_script
   (unit_statement
      (create_type CREATE OR REPLACE TYPE
         (type_definition
            (type_name
               (id_expression
                  (regular_id TY_TEST)))
            (object_type_def
               (object_as_part AS OBJECT) (
               (object_member_spec
                  (identifier
                     (id_expression
                        (regular_id COL1)))
                  (type_spec
                     (datatype
                        (native_datatype_element INT)))) ,
               (object_member_spec
                  (identifier
                     (id_expression
                        (regular_id COL2)))
                  (type_spec
                     (datatype
                        (native_datatype_element VARCHAR)
                        (precision_part (
                           (numeric 20) ))))) ))))) ; <EOF>)

改写代码

    def visitCreate_type(self, ctx:PlSqlParser.Create_typeContext):
        # 检查并修改 'create or replace' 为 'create'
        if ctx.getChild(0).getText() == 'create' and ctx.getChild(1).getText() == 'or' and ctx.getChild(2).getText() == 'replace':
           # print(f"Modifying: {ctx.getChild(0).getText()} {ctx.getChild(1).getText()} {ctx.getChild(2).getText()}")
            self.rewriter.replace(
                TokenStreamRewriter.DEFAULT_PROGRAM_NAME,
                ctx.getChild(0).symbol.tokenIndex,
                ctx.getChild(2).symbol.tokenIndex,
                'create'
            )   
        object_as_part = ctx.type_definition().object_type_def().object_as_part()
        #print(f"ctx.object_as_part.getText(): {object_type_def.object_as_part().getChild(1).getText()}")
        # 删除 object关键字
        if object_as_part.getChild(1).getText()=='OBJECT':
            self.rewriter.delete(
                TokenStreamRewriter.DEFAULT_PROGRAM_NAME,
                object_as_part.getChild(1).symbol.tokenIndex,
                object_as_part.getChild(1).symbol.tokenIndex
            )
        return self.visitChildren(ctx)

这里可以从(create_type CREATE OR REPLACE TYPE看到,CREATE OR REPLACE TYPE这一串都在create_type这个节点上,可以通过getChild(n)来取出中间的每一部分,而这每一部分的位置,则是通过symbol.tokenIndex获取。
然后从(object_as_part AS OBJECT) (中可以看到,我们期望删除的object是在object_as_part这个节点的第二个字符串,因此使用了getChild(1)。这里需要注意,由于object_as_part这个节点是在create_type这个节点的下面很多层,然后后面会要多次使用这个节点,所以可以定义一个object_as_part = ctx.type_definition().object_type_def().object_as_part(),减少冗余代码。
找到OBJECT后,执行self.rewriter.delete,就可以把OBJECT删掉了。

DEMO 3

在ORACLE的sql脚本中,create type应该以/结尾,但是在OG中,则不能有/,如果一个脚本文件里混合了多种语句,就会出现有的/要删,有的不能删,因此我们可以写一个规则,将所有create type语句后面的/删掉。

    def visitSql_script(self, ctx:PlSqlParser.Sql_scriptContext):
        for i in range(ctx.getChildCount() - 1):
            unit = ctx.getChild(i)
            if isinstance(unit, PlSqlParser.Unit_statementContext):
                create_type_stmt = unit.getChild(0)
                # 对于create type语句
                if isinstance(create_type_stmt, PlSqlParser.Create_typeContext):
                    # 遍历兄弟节点,找到 `/` 进行删除
                    for j in range(i + 1, ctx.getChildCount()):
                        sibling = ctx.getChild(j)
                        if sibling.getText() == '/':
                            #print(f"Deleting: {sibling.getText()}")
                            self.rewriter.delete(
                                TokenStreamRewriter.DEFAULT_PROGRAM_NAME,
                                sibling.symbol.tokenIndex,
                                sibling.symbol.tokenIndex
                            )
                            break
        return self.visitChildren(ctx)

这里没有使用visitCreate_type的原因是,在antlr4生成的语法树中,/这个节点并不在Create_type这个节点的内部,所以得对Sql_script这个根节点,找到所有的单条语句Unit_statement,然后判断里面是不是有Create_type,然后再回头删掉/

三、注意事项

antlr4目前的版本在sql语法解析规则中存在一个我认为很严重的BUG,就是SQL中如果包含有pro或者rem,解析就会报错,参考这个issue
# [PlSql] “REM”, “REMARK”, “PRO”, “PROMPT” can not be a identifier #3817
其实规避手段也很简单,删除g4文件中的PROMPT_MESSAGE和REMARK_COMMENT这两个语法定义,然后重新执行antlr4vjava PlSqlParser.g4antlr4vjava PlSqlLexer.g4即可。不过这样就将无法解析SQLPLUS中的promptremark命令了

  • PlSqlLexer.g4
//REMARK_COMMENT:
//    'REM' {this.IsNewlineAtPos(-4)}? 'ARK'? (' ' ~('\r' | '\n')*)? NEWLINE_EOF -> channel(HIDDEN)
//;

// https://docs.oracle.com/cd/E11882_01/server.112/e16604/ch_twelve032.htm#SQPUG052
//PROMPT_MESSAGE: 'PRO' {this.IsNewlineAtPos(-4)}? 'MPT'? (' ' ~('\r' | '\n')*)? NEWLINE_EOF;
  • PlSqlParser.g4
//    | PROMPT_MESSAGE

四、总结

antlr4很强大,我们可以借助其语法树的能力,实现精准的语法改写,当然前提是我们要先想好怎么去改写,然后才能编写对应的规则。这种方式虽然比正则替换要慢很多,但是对于大量复杂的存储过程而言,使用语法树进行改写的方式无疑比正则替换更加可靠。使用高级开发语言还可以连接数据库获取元数据来推断被引用的对象类型和属性,来进行更加个性化的复杂逻辑改写。

  • 本文作者: DarkAthena
  • 本文链接: https://www.darkathena.top/archives/antlr4-part2-dev-with-python
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/918256.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

java八股-jvm入门-程序计数器,堆,元空间,虚拟机栈,本地方法栈,类加载器,双亲委派,类加载执行过程

文章目录 PC Register堆虚拟机栈方法区(Metaspace元空间双亲委派机制类加载器 类装载的执行过程 PC Register 程序计数器&#xff08;Program Counter Register&#xff09;是 Java 虚拟机&#xff08;JVM&#xff09;中的一个组件&#xff0c;它在 JVM 的内存模型中扮演着非常…

11.12机器学习_特征工程

四 特征工程 1 特征工程概念 特征工程:就是对特征进行相关的处理 一般使用pandas来进行数据清洗和数据处理、使用sklearn来进行特征工程 特征工程是将任意数据(如文本或图像)转换为可用于机器学习的数字特征,比如:字典特征提取(特征离散化)、文本特征提取、图像特征提取。 …

STL序列式容器之list

相较于vector的连续性空间&#xff0c;list相对比较复杂&#xff1b;list内部使用了双向环形链表的方式对数据进行存储&#xff1b;list在增加元素时&#xff0c;采用了精准的方式分配一片空间对数据及附加指针等信息进行存储&#xff1b; list节点定义如下 template<clas…

算法日记 26-27day 贪心算法

接下来的题目有些地方比较相似。需要注意多个条件。 题目&#xff1a;分发糖果 135. 分发糖果 - 力扣&#xff08;LeetCode&#xff09; n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求&#xff0c;给这些孩子分发糖果&#xff1a; 每…

Linux下编译MFEM

本文记录在Linux下编译MFEM的过程。 零、环境 操作系统Ubuntu 22.04.4 LTSVS Code1.92.1Git2.34.1GCC11.4.0CMake3.22.1Boost1.74.0oneAPI2024.2.1 一、安装依赖 二、编译代码 附录I: CMakeUserPresets.json {"version": 4,"configurePresets": [{&quo…

Pytest-Bdd-Playwright 系列教程(9):使用 数据表(DataTable 参数) 来传递参数

Pytest-Bdd-Playwright 系列教程&#xff08;9&#xff09;&#xff1a;使用 数据表&#xff08;DataTable 参数&#xff09; 来传递参数 前言一、什么是 datatable 参数&#xff1f;Gherkin 表格示例 二、datatable 参数的基本使用三、完整代码和运行效果完整的测试代码 前言 …

C语言项⽬实践-贪吃蛇

目录 1.项目要点 2.窗口设置 2.1mode命令 2.2title命令 2.3system函数 2.Win32 API 2.1 COORD 2.2 GetStdHandle 2.3 CONSOLE_CURSOR_INFO 2.4 GetConsoleCursorInfo 2.5 SetConsoleCursorInfo 2.5 SetConsoleCursorPosition 2.7 GetAsyncKeyState 3.贪吃蛇游戏设…

使用 Prompt API 与您的对象聊天

tl;dr&#xff1a;GET、PUT、PROMPT。现在&#xff0c;可以使用新的 PromptObject API 仅使用自然语言对存储在 MinIO 上的对象进行总结、交谈和提问。在本文中&#xff0c;我们将探讨这个新 API 的一些用例以及代码示例。 赋予动机&#xff1a; 对象存储和 S3 API 的无处不在…

Oracle OCP认证考试考点详解082系列19

题记&#xff1a; 本系列主要讲解Oracle OCP认证考试考点&#xff08;题目&#xff09;&#xff0c;适用于19C/21C,跟着学OCP考试必过。 91. 第91题&#xff1a; 题目 解析及答案&#xff1a; 关于 Oracle 数据库中的索引及其管理&#xff0c;以下哪三个陈述是正确的&#x…

脑机接口、嵌入式 AI 、工业级 MR、空间视频和下一代 XR 浏览器丨RTE2024 空间计算和新硬件专场回顾

这一轮硬件创新由 AI 引爆&#xff0c;或许最大受益者仍是 AI&#xff0c;因为只有硬件才能为 AI 直接获取最真实世界的数据。 在人工智能与硬件融合的新时代&#xff0c;实时互动技术正迎来前所未有的创新浪潮。从嵌入式系统到混合现实&#xff0c;从空间视频到脑机接口&…

Element UI如何实现按需导入--Vue3篇

前言 在使用 Element UI 时&#xff0c;按需导入&#xff08;即按需引入&#xff09;是一个常见的需求&#xff0c;尤其是在构建大型应用时。按需导入可以显著减少打包后的文件体积&#xff0c;提升应用的加载速度。本文将详细介绍如何在项目中实现 Element UI 的按需导入&…

【设计模式】行为型模式(五):解释器模式、访问者模式、依赖注入

《设计模式之行为型模式》系列&#xff0c;共包含以下文章&#xff1a; 行为型模式&#xff08;一&#xff09;&#xff1a;模板方法模式、观察者模式行为型模式&#xff08;二&#xff09;&#xff1a;策略模式、命令模式行为型模式&#xff08;三&#xff09;&#xff1a;责…

.NET 9.0 中 System.Text.Json 的全面使用指南

以下是一些 System.Text.Json 在 .NET 9.0 中的使用方式&#xff0c;包括序列化、反序列化、配置选项等&#xff0c;并附上输出结果。 基本序列化和反序列化 using System; using System.Text.Json; public class Program {public class Person{public string Name { get; se…

《InsCode AI IDE:编程新时代的引领者》

《InsCode AI IDE&#xff1a;编程新时代的引领者》 一、InsCode AI IDE 的诞生与亮相二、独特功能与优势&#xff08;一&#xff09;智能编程体验&#xff08;二&#xff09;多语言支持与功能迭代 三、实际应用与案例&#xff08;一&#xff09;游戏开发案例&#xff08;二&am…

优选算法 - 5 ( 栈 队列 + 宽搜 优先级队列 9000 字详解 )

一&#xff1a;栈 1.1 删除字符串中的所有相邻重复项 题目链接&#xff1a;删除字符串中的所有相邻重复项 class Solution {public String removeDuplicates(String _s) {// 用 StringBuffer 模拟一下栈结构StringBuffer ret new StringBuffer();// 接着把 _s 转换为字符数组…

【linux012】文件操作命令篇 - more 命令

文章目录 more 命令1、基本用法2、常见选项3、交互式键盘命令4、举例5、注意事项 more 命令 more 是 Linux 中的一个分页查看命令&#xff0c;用于逐屏显示文件内容。它特别适合用于查看较长的文件&#xff0c;与 cat 不同&#xff0c;more 不会一次性输出所有内容&#xff0c…

企业BI工具如何选择?主流5款BI工具多维对比

数据大爆炸时代&#xff0c;企业数据爆发式增长&#xff0c;来自产品、运营、价值链以及外部的数据都成指数级增长趋势。利用大数据分析实现精细化运营&#xff0c;驱动业务增长是企业的理想蓝图。而BI工具能够整合、分析并可视化复杂的数据集&#xff0c;帮助管理层和决策者快…

Qt 5.6.3 手动配置 mingw 环境

- 安装 qt 5.6.3 mingw 版 - 打开 qt creator - 找到选项 工具 - 选项- 构建和运行 - 找到 “编译器” 选项卡 ,点击 "添加" “编译器路径” 设置为 qt 安装目录下&#xff0c; tool 文件夹内的 g.exe 设置完成后&#xff0c;点击 "apply" ,使选项生…

linux使用scp和密钥在不同服务器传输文件

将源服务密钥中公钥&#xff08;以pub结尾的&#xff09;复制或拷贝密文&#xff0c;粘贴到目标服务器中的/root/.ssh/authorized_keys文件中&#xff1b; 测试连接&#xff1a;ssh -p2129 root172.129.162.537&#xff0c;如果使用默认端口22 -p参数可省略&#xff0c;注意这…

德克萨斯扑克(德扑)笔记

文章目录 比牌方法(大小)发牌下注位置一些牌面的简称QT是什么意思89s是什么意思AT是什么意思ATs是什么意思 89o是什么意思 其他术语Action 叫注/说话 - 一个玩家的决定Betting Rounds 押注圈其他术语 团建或和小伙伴聚会的时候经常玩德扑&#xff0c;一是凑手&#xff0c;二是聚…