【ORACLE】一个允许关键字作为别名所引起的语法歧义场景

news/2024/12/20 0:20:01/

前言

最近在看SQL语法解析器,发现了antlr4提供的PlSql语法树存在一个BUG,然后我顺着这个BUG,构造了一条SQL,在ORACLE执行,如下

image-ghns.png

然后神奇的事情出现了,这个查询竟然没有返回行!t1表左关联t2,而且对t1表没有过滤条件,那么无论如何t1表中的数据应该是全部展现的!
然后有人可能认为ORACLE就是这样的,但下面这个图的出现可能会让人瞬间抓狂!

image-nzct.png

两条长得完全一样的SQL,就是简单的两个表关联查询,竟然出现了结果不一致的情况!

起因

在客户应用迁移到某国产数据库中,有一条类似这样的SQL

select * from t1 left join t2 on t1.id=t2.ids(+);

在该国产数据库中会报错 ERROR: Operator "(+)" can only be used in WhereClause of Select-Statement or Subquery.

即不允许操作符 (+)出现在select语句的where子句以外的地方。
于是我们开始分析这样的SQL基于语法树应该如何改写。
首先这个SQL是两个表的left join,一般正常的写法是

select * from t1,t2 where t1.id=t2.ids(+);

select * from t1 left join t2 on t1.id=t2.ids;

即两个表的左关联不需要同时使用 left join(+),两种用法重复了,最简单的改法就是在原SQL上去掉 (+)号。
但是只能针对两表的 left joinright join,对于 join则不能简单的去掉,因为 join的时候,(+)就有意义了。
于是乎,我们试图通过语法树来看是否可以区分出 joinleft join

join_clause: query_partition_clause? (CROSS | NATURAL)? (INNER | outer_join_type)? JOIN table_ref_aux query_partition_clause? (join_on_part| join_using_part)*| (CROSS | OUTER) APPLY table_ref_aux;
outer_join_type: (FULL | LEFT | RIGHT) OUTER?;

从语法解析规则文件来看,似乎是可以区分的,然后解析一下这条SQL:

select * from t1 left join t2 on t1.id=t2.ids;
^Z
(sql_script(unit_statement(data_manipulation_language_statements(select_statement(select_only_statement(subquery(subquery_basic_elements(query_block select(selected_list *)(from_clause from(table_ref_list(table_ref(table_ref_aux(table_ref_aux_internal(dml_table_expression_clause(tableview_name(identifier(id_expression(regular_id t1))))))(table_alias(identifier(id_expression(regular_id(non_reserved_keywords_pre12c left))))))(join_clause join(table_ref_aux(table_ref_aux_internal(dml_table_expression_clause(tableview_name(identifier(id_expression(regular_id t2)))))))(join_on_part on(condition(expression(logical_expression(unary_logical_expression(multiset_expression(relational_expression(relational_expression(compound_expression(concatenation(model_expression(unary_expression(atom(general_element(general_element(general_element_part(id_expression(regular_id t1)))) .(general_element_part(id_expression(regular_id(non_reserved_keywords_pre12c id)))))))))))(relational_operator =)(relational_expression(compound_expression(concatenation(model_expression(unary_expression(atom(general_element(general_element(general_element_part(id_expression(regular_id t2)))) .(general_element_part(id_expression(regular_id ids)))))))))))))))))))))))))))) ; <EOF>)

结果发现这里的 left竟然被当成了t1表的别名!ORACLE对于关键字作为别名的一个重要特点就是可以不加 as,而且ORACLE也的确支持 left作为表的别名。于是乎这里的语法似乎也可以说得过去,不是外关联,而是 left这个表和 t2表做 join。(其实是开源的语法解析器的问题https://github.com/antlr/grammars-v4/issues/1726 )

而ORACLE自然是不会允许这种情况发生的,ORACLE在语法解析的时候,读到left,会往后匹配一个词,如果是 joinouter join,则一起识别为 left join,否则,就把left识别为表的别名。但这样就会导致要执行更多的判断逻辑。而其他数据库禁止使用left作为别名,或者禁止不带 as直接作为别名时,则不需要有这样判断,并且不会引起语法上的歧义。

答案揭晓

在做这个语法分析的过程中,我们发现了这一语法歧义,自然就可以想到,我们能不能通过某种手段,在oracle里让这个 left真的变为别名。
其实很简单,left里有一个特殊的字母 e,经常做信息安全方面的人,以及经常识别假冒账号的人,对这个 e可以说是非常熟悉,比如下面这4个进程名,去对比字符,可以发现是完全不同的四个进程名

explorer.exe
еxplorеr.exe
еxplorer.exe
explorеr.exe
SQL> with t as (2  select 'explorer.exe' c from dual union all3  select 'еxplorеr.exe' from dual union all4  select 'еxplorer.exe' from dual union all5  select 'explorеr.exe' from dual)6  select c,utl_raw.cast_to_raw(c) from t;C              UTL_RAW.CAST_TO_RAW(C)
-------------- --------------------------------------------------------------------------------
explorer.exe   6578706C6F7265722E657865
еxplorеr.exe   A7D678706C6F72A7D6722E657865
еxplorer.exe   A7D678706C6F7265722E657865
explorеr.exe   6578706C6F72A7D6722E657865

其实这里的 е不是 e,而是俄文的 е,没错,俄文里的 e,字形和英文字母的 e是一模一样的,但其实是两个不同的字符。
所以文章开始的那两个SQL就是下面的看似一样的两条SQL查出来的

with t1 as (select 1 id from dual),
t2 as (select 0 ids from dual)
select * from t1 lеft join t2 on id=ids;with t1 as (select 1 id from dual),
t2 as (select 0 ids from dual)
select * from t1 left join t2 on id=ids;

总结

虽然在国内的实际生产应用中,这里出现俄文的 е,概率是极低的,但养成良好的开发习惯,不过于依赖ORACLE这种宽松的SQL标准,比如别名就得加 as,不要混用 join(+)等,这样能在异构数据库兼容改造时,省去不少麻烦。

  • 本文作者: DarkAthena
  • 本文链接: https://www.darkathena.top/archives/A-Scenario-of-Syntactic-Ambiguity-Caused-by-Allowing-Keywords-as-Aliases
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处

http://www.ppmy.cn/news/1556511.html

相关文章

Windows如何安装go环境,离线安装beego

一、安装go 1、下载go All releases - The Go Programming Language 通过网盘分享的文件&#xff1a;分享的文件 链接: https://pan.baidu.com/s/1MCbo3k3otSoVdmIR4mpPiQ 提取码: hxgf 下载amd64.zip文件&#xff0c;然后解压到指定的路径 2、配置环境变量 需要新建两个环境…

【ArcGIS】基于R语言、MaxEnt模型融合技术的物种分布模拟、参数优化方法、结果分析制图与论文写作

第一章、以问题导入的方式&#xff0c;深入掌握原理基础【理论篇】 1、R语言入门&#xff1a; &#xff08;1&#xff09;安装R及集成开发环境&#xff08;IDE&#xff09;&#xff1b;&#xff08;2&#xff09;R语言基础语法与数据结构&#xff0c;包括&#xff1a;程序包安…

canal详解及demo

提示&#xff1a;如何保证Redis中的数据与数据库中的数据一致性&#xff1f;数据同步canal的介绍和demo、大型企业如何实现mysql到redis的同步&#xff1f;使用binlog实时更新redis缓存、canal的接入教程、win下canal的服务器端、canal客户端的创建、连接、测试教程、数据同步方…

idea | maven项目标红解决方案 | 强制刷新所有依赖

场景&#xff1a;父pom多模块&#xff0c;新增时&#xff0c;依赖正常&#xff0c;但是application.yml看起来没被springboot识别&#xff0c;试过rebuild、重开idea清除缓存&#xff0c;重新maven面板reload all maven projects, 试过pom文件的依赖先移除再重新粘贴导入进来&a…

在 Linux 下,将 shell 脚本打包成二进制程序

在 Linux 下&#xff0c;将 shell 脚本打包成二进制程序并不是一个直接的过程&#xff0c;因为 shell 脚本本质上是文本文件&#xff0c;由 shell 解释器执行。不过&#xff0c;你可以使用几种方法来实现类似的目的&#xff1a; ### 1. 使用 shc 工具 shc 是一个可以将 shell…

用python实现滑雪小游戏,附源码

一个简单的基于文本的滑雪小游戏示例代码&#xff0c;在这个游戏中玩家控制一个滑雪者在有障碍物的雪道上滑行&#xff0c;尽量避开障碍物并获取更高的分数。 ● Skier类表示滑雪者&#xff0c;有位置属性和移动、转向方法。 ● Obstacle类表示障碍物&#xff0c;有位置属…

Java连接chatGPT步骤(免费key获取方法)

1.首先&#xff0c;找到了这个网站&#xff0c;介绍了五个免费使用ChatGPT API的开源项目 https://www.51cto.com/article/786796.html2.然后本人选择使用GPT-API-free&#xff0c;如果选择gpt-3.5-turbo模型的话&#xff0c;每天可以请求100条&#xff1b;可以了&#xff0c;…

基于python对网页进行爬虫简单教程

python对网页进行爬虫 基于BeautifulSoup的爬虫—源码 """ 基于BeautifulSoup的爬虫### 一、BeautifulSoup简介1. Beautiful Soup提供一些简单的、python式的函数用来处理导航、搜索、修改分析树等功能。它是一个工具箱&#xff0c;通过解析文档为用户提供需要…