大家好,这里是公众号 DBA学习之路,致力于分享数据库领域相关知识。
社群交流
为了给大家提供一些技术交流的平台,目前已成立的技术交流群:
Oracle 数据库交流群 国产数据库交流群 Linux 技术交流群 ChatGPT 4o 免费体验群
需要进群可以添加微信:Lucifer-0622,备注对应的群名即可。
本文作者:DarkAthena
本文转载自:https://www.darkathena.top/archives/A-Scenario-of-Syntactic-Ambiguity-Caused-by-Allowing-Keywords-as-Aliases
前言
最近在看 SQL 语法解析器,发现了antlr4 提供的 PLSQL 语法树存在一个BUG,然后我顺着这个BUG,构造了一条 SQL,在Oracle 执行,如下:
然后神奇的事情出现了,这个查询竟然没有返回行!t1 表左关联 t2,而且对 t1 表没有过滤条件,那么无论如何 t1 表中的数据应该是全部展现的!
然后有人可能认为 Oracle 就是这样的,但下面这个图的出现可能会让人瞬间抓狂!
两条长得完全一样的 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 join 或right join,对于join 则不能简单的去掉,因为join 的时候,(+) 就有意义了。
于是乎,我们试图通过语法树来看是否可以区分出join 和left 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 leftjoin 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)))))))))))))))))))))))))))) ; )
结果发现这里的 left 竟然被当成了 t1 表的别名!Oracle 对于关键字作为表别名的一个重要特点就是不能加 as,而且 Oracle 也的确支持 left 作为表的别名。于是乎这里的语法似乎也可以说得过去,不是外关联,而是 left 这个表和 t2 表做join。(其实是开源的语法解析器的问题 https://github.com/antlr/grammars-v4/issues/1726)
而 Oracle 自然是不会允许这种情况发生的,Oracle 在语法解析的时候,读到 left,会往后匹配一个词,如果是join 或outer 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 (
select 'explorer.exe' c from dual union all
select 'еxplorеr.exe' from dual union all
select 'еxplorer.exe' from dual union all
select 'explorеr.exe' from dual)
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;
我使用以上 SQL 在 MogDB 5.0、崖山 YashanDB v23.2、达梦 DM8、南大通用 GBase 8s、金仓 KingbaseES V9 等国产数据库上执行也是同样的结果!
总结
虽然在国内的实际生产应用中,这里出现俄文的е,概率是极低的,但养成良好的开发习惯,不过于依赖 ORACLE 这种宽松的 SQL 标准,比如别名就得加as,不要使用关键字作为标识符,不要混用join 和(+) 等,这样能在异构数据库兼容改造时,省去不少麻烦。
点击阅读原文