基于语法树的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 TokenStreamRewriterclass 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)breakreturn 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 许可协议。转载请注明出处

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于单片机智能温室大棚监测系统

本设计以单片机为核心的智能温室大棚监测系统&#xff0c;用于监测大棚内的温湿度、土壤湿度、CO2浓度和光照强度。该系统以STM32F103C8T6芯片为核心控制单元&#xff0c;涵盖电源、按键、NB-IoT模块、显示屏模块、空气温湿度检测、土壤湿度检测、二氧化碳检测和光敏电阻等模块…

JavaScript逆向爬虫教程-------基础篇之常用的编码与加密介绍(python和js实现)

目录 一、编码与加密原理 1.1 ASCII 编码1.2 详解 Base64 1.2.1 Base64 的编码过程和计算方法1.2.2 基于编码的反爬虫设计1.2.3 Python自带base64模块实现base64编码解码类封装 1.3 MD5消息摘要算法 1.3.1 MD5 介绍1.3.2 Python实现md5以及其他常用消息摘要算法封装 1.4 对称加…

RHCSA学习超详细知识点2命令篇

输入命令行的语法 终端中执行命令需要遵照一定的语法&#xff0c;输入命令的格式如下&#xff1a; 命令 参数命令 -选项 参数 输入命令时可以包含多个选项&#xff0c;假如一个命令有-a,-b,-c,-d四个选项&#xff0c;可以写作 命令 -a -b -c -d 参数 这里的多个选项可以“提…

Java结合ElasticSearch根据查询关键字,高亮显示全文数据。

由于es高亮显示机制的问题。当全文内容过多&#xff0c;且搜索中标又少时&#xff0c;就会出现高亮结果无法覆盖全文。因此需要根据需求手动替换。 1.根据es的ik分词器获取搜索词的分词结果。 es部分&#xff1a; //中文分词解析 post /_analyze {"analyzer":"…

5. langgraph中的react agent使用 (从零构建一个react agent)

1. 定义 Agent 状态 首先&#xff0c;我们需要定义 Agent 的状态&#xff0c;这包括 Agent 所持有的消息。 from typing import (Annotated,Sequence,TypedDict, ) from langchain_core.messages import BaseMessage from langgraph.graph.message import add_messagesclass …

STL序列式容器之list

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

Science Robotics 封面论文:视触觉传感器的手内操作

现在&#xff0c;随便丢给机械手一个陌生物体&#xff0c;它都可以像人类一样轻松拿捏了。除了苹果&#xff0c;罐头、乐高积木、大象玩偶、骰子&#xff0c;都不在话下&#xff1a; 这就是来自Meta FAIR团队最新的NeuralFeels技术&#xff0c;通过融合触觉和视觉&#xff0c;机…

定时器简介

TIM(Timer定时器)简介 在第一部分,我们主要讲的是定时器基本定时的功能&#xff0c;也就是定一个时间&#xff0c;然后让定时器每隔这个时间产生一个中断&#xff0c;来实现每隔一个固定时间执行一段程序的目的&#xff0c;比如你要做个时钟、秒表&#xff0c;或者使用一些程序…

【电子设计】按键LED控制与FreeRTOS

1. 安装Keilv5 打开野火资料,寻找软件包 解压后得到的信息 百度网盘 请输入提取码 提取码:gfpp 安装526或者533版本都可以 下载需要的 F1、F4、F7、H7 名字的 DFP pack 芯片包 安装完 keil 后直接双击安装 注册操作,解压注册文件夹后根据里面的图示步骤操作 打开说明 STM…

阅读2020-2023年《国外军用无人机装备技术发展综述》笔记_技术趋势

目录 文献基本信息 序言 1 发展概况 2 重点技术发展 2.1 人工智能技术 2.1.1 应用深化 2.1.2 作战效能提升 2.2 航空技术 2.2.1螺旋桨设计创新 2.2.2 发射回收技术进步 2.3 其他相关技术 2.3.1 远程控制技术探 2.3.2 云地控制平台应用 3 装备系统进展 3.1 无人作…

redis类型介绍

1. 字符串&#xff08;String&#xff09;&#xff1a; • 简介&#xff1a;最基础的数据类型&#xff0c;可以存储任何形式的字符串&#xff0c;包括文本数据和数字数据。 • 常用操作&#xff1a;SET、GET、INCR、DECR等。 2. 列表&#xff08;List&#xff09;&#xff1a; …

免费送源码:Java+Springboot+MySQL Springboot多租户博客网站的设计 计算机毕业设计原创定制

Springboot多租户博客网站的设计 摘 要 博客网站是当今网络的热点&#xff0c;博客技术的出现使得每个人可以零成本、零维护地创建自己的网络媒体&#xff0c;Blog站点所形成的网状结构促成了不同于以往社区的Blog文化&#xff0c;Blog技术缔造了“博客”文化。本文课题研究的“…

家政服务小程序,家政行业数字化发展下的优势

今年以来&#xff0c;家政市场需求持续增长&#xff0c;市场规模达到了万亿级别&#xff0c;家政服务行业成为了热门行业之一&#xff01; 家政服务种类目前逐渐呈现了多样化&#xff0c;月嫂、保姆、做饭保洁、收纳、维修等家政种类不断出现&#xff0c;满足了居民日益增长的…

炼码LintCode--数据库题库(级别:简单;数量:55道)--刷题笔记_02

目录 炼码LintCode--数据库题库&#xff08;级别&#xff1a;简单&#xff1b;数量&#xff1a;55道&#xff09;--刷题笔记_023618 耗时前三的任务&#xff08;日期差&#xff09;题&#xff1a;sql&#xff1a;解释&#xff1a;DATEDIFF 天数差order by 别名TIMESTAMPDIFF 月…

如何使用正则表达式验证域名

下面是一篇关于如何使用正则表达式验证域名的教程。 如何使用正则表达式验证域名 简介 域名是互联网上网站的地址&#xff0c;每个域名由多个标签&#xff08;label&#xff09;组成&#xff0c;标签之间用点 . 分隔。域名规则有很多细节&#xff0c;但基本要求是&#xff1a…

猫狗识别之BUG汇总

一、github登不上去问题 下载watt toolkit 下载地址&#xff1a;https://steampp.net/ 可以下载后加速&#xff0c;访问github 二、猫狗总体参考核心 B哥的博客 https://github.com/bubbliiiing/classification-keras?tabreadme-ov-file 三、CSDN很多会员才能阅读问题 根据…

MATLAB 使用教程 —— 命令窗口输入命令,工作区显示变量

命令在命令窗口输入变量在工作区显示 MATLAB 桌面包含的面板如下&#xff1a; 当前文件夹 - 此面板允许访问项目文件夹和文件。命令窗口 - 这是主要区域&#xff0c;用户在命令行中输入命令&#xff0c;命令提示符(>>).工作区 - 工作区显示所有变量&#xff0c;无论是创…

nodejs入门(1):nodejs的前后端分离

一、引言 我关注nodejs还是从前几年做了的一个电力大数据展示系统开始的&#xff0c;当然&#xff0c;我肯定是很多年的计算机基础的&#xff0c;万变不离其宗。 现在web网站都流行所谓的前后端结构&#xff0c;不知不觉我也开始受到这个影响&#xff0c;以前都是前端直接操作…

前端开发之打印功的使用和实例(vue-print-nb)

通过插件来进行实现 前言效果图1、安装插件vue2vue32、 引入Vue项目2、 使用2.1、在项目中创建按钮并且使用v-print绑定绑定打印事件2.2、编写要打印的内容,给内容附加唯一的id2.3、绑定的时间的方法和参数3、整体代码(此代码是通过vue3来进行实现的但是逻辑都是一样的)前言…

一文简单了解Android中的input流程

在 Android 中&#xff0c;输入事件&#xff08;例如触摸、按键&#xff09;从硬件传递到应用程序并最终由应用层消费。整个过程涉及多个系统层次&#xff0c;包括硬件层、Linux 内核、Native 层、Framework 层和应用层。我们将深入解析这一流程&#xff0c;并结合代码逐步了解…