前言
前一段时间在迁移Oracle到一个openGauss系数据 库时,发现存储过程里有一条SQL报错,简化后如下
gaussdb=# select
gaussdb-# case when 1=1 then 1 end case
gaussdb-# from dual;
ERROR: syntax error at or near "case"
LINE 2: case when 1=1 then 1 end case^
gaussdb=#
这里似乎只要把end case
改成end
即可正常执行,于是就写了这么一条语法替换规则。但按这个规则改了一批存储过程后,就发现已经迈入了ORACLE的case when
语法陷阱。下面这个语句,原本用end case
不报错,改成end
后反而报错了
gaussdb=# begin
gaussdb$# case when 1=1
gaussdb$# then null;
gaussdb$# end case;
gaussdb$# end;
gaussdb$# /
ANONYMOUS BLOCK EXECUTE
gaussdb=# begin
gaussdb$# case when 1=1
gaussdb$# then null;
gaussdb$# end ;
gaussdb$# end;
gaussdb$# /
ERROR: syntax error at end of input
LINE 5: end ^
QUERY: DECLARE
BEGIN
case when 1=1
then null;
end
gaussdb=#
分析
先来看看这两个包含case when语法的语句是什么含义
1. case expression
https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/CASE-Expressions.html
在ORACLE官方文档中,case表达式的语法如下
CASE { simple_case_expression| searched_case_expression}[ else_clause ]ENDsimple_case_expression::=
expr{ WHEN comparison_expr THEN return_expr }...searched_case_expression::=
{ WHEN condition THEN return_expr }... else_clause::=
ELSE else_expr
在case关键字之后,可以是简单case表达式或者查找case表达式。
使用了简单case表达式的例子为
case col when 1 then 'yes' when 0 then 'no' else 'others' end
使用了查找case表达式的例子为
case when col=1 then 'yes' when col=0 then 'no' else 'others' end
表达式(expression)常见的使用位置为 变量赋值、select 列表、where列表、order by列表、group by列表、函数参数等等一切需要表达一个值的地方。
表达式的目的不是执行什么指令,因此,词法语法内也不会出现分号。
本文开头的例子中,case when 1=1 then 1 end case
,其中的case when 1=1 then 1 end
就属于使用了查找case表达式的case表达式,而最后的这个case,在语法上属于select 列表中的列别名,也就是说这段的完整表达形式应该为case when 1=1 then 1 end as case
。不带as使用关键字作为别名会报错,这正是很多openGauss系数据库常见的一个问题。
2. case statement
https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/CASE-statement.html
在ORACLE官方文档中,case 语句的语法如下
simple_case_statement ::=
CASE selector WHEN (selector_value | dangling_predicate) [, selector_value | dangling_predicate]... THEN statement[statement]...; [WHEN (selector_value | dangling_predicate) [, selector_value | dangling_predicate]... THEN statement[statement]...;]...[ELSE statement[statement]...] END CASE [label] ;searched_case_statement ::=
[ <<label>> ] CASEWHEN boolean_expression THEN statement [ statement ]... ;[ WHEN boolean_expression THEN statement [ statement ]... ; ]...[ ELSE statement [ statement ]... ;END CASE [ label ];
case语句有两种,简单case语句和查找case语句。
简单case语句的例子:
declare
v1 number;
v2 varchar2(20);
begin
case v1
when 1 then
v2:='yes'
when 0 then
v2:='no'
else
v2:='others'
end case;
end;
查找case语句的例子:
declare
v1 number;
begin
case when v1=1 then
v2:='yes'
when v1=1 then
v2:='no'
else
v2:='others'
end case;
end;
对比case表达式,case语句中最显著的差异就是,一定会有END CASE
,并且一定会有分号,因为CASE语句的目的是执行一个指令,而不是表示一个值。
这里可以把case语句理解成和if语句完全等价,if语句一定会以end if结尾,那么case语句也一定要以end case结尾,去掉end后的case就会语法错误。
antlr4的问题
antlr4的grammars-v4的PlSql语法里,只有case_statement,没有case_expression,并且把case_statement放到了表达式unary_expression下面,这样就会导致无法正确解析出是case表达式还是case语句,也就无法识别case表达式的end后的case是否为别名。
case_statement /*TODO [boolean isStatementParameter]
TODO scope {boolean isStatement;
}
@init {$case_statement::isStatement = $isStatementParameter;}*/: searched_case_statement| simple_case_statement;// CASEsimple_case_statement: label_declaration? ck1 = CASE expression simple_case_when_part+ case_else_part? END CASE? label_name?;simple_case_when_part: WHEN expression THEN (/*TODO{$case_statement::isStatement}?*/ seq_of_statements | expression);searched_case_statement: label_declaration? ck1 = CASE searched_case_when_part+ case_else_part? END CASE? label_name?;searched_case_when_part: WHEN condition THEN (/*TODO{$case_statement::isStatement}?*/ seq_of_statements | expression);case_else_part: ELSE (/*{$case_statement::isStatement}?*/ seq_of_statements | expression);
unary_expression: ('-' | '+') unary_expression| PRIOR unary_expression| CONNECT_BY_ROOT unary_expression| /*TODO {input.LT(1).getText().equalsIgnoreCase("new") && !input.LT(2).getText().equals(".")}?*/ NEW unary_expression| DISTINCT unary_expression| ALL unary_expression| /*TODO{(input.LA(1) == CASE || input.LA(2) == CASE)}?*/ case_statement /*[false]*/| unary_expression '.' ((COUNT | FIRST | LAST | LIMIT)| (EXISTS | NEXT | PRIOR) '(' index += expression ')')| quantified_expression| standard_function| atom| implicit_cursor_expression;
他在END CASE的CASE这里加了个问号,表示这里CASE可以有也可以没有,明显不符合ORACLE的语法规则。
当然,原开发者可能知道这里有问题,写了注释 /*TODO{$case_statement::isStatement}?*/
,不过这一todo,几十年过去了也没do出来。
于是我加了个case_expression语法规则,并且将表达式节点中的case_statement换成了case_expression。
https://github.com/antlr/grammars-v4/commit/04096e42803e91f99eedeaf86b0c31821075f7ab
case_expression: searched_case_expression| simple_case_expression;simple_case_expression: CASE expression simple_case_when_part+ case_else_part? END;searched_case_expression: CASE searched_case_when_part+ case_else_part? END;
其实完全可以按ORACLE的语法组成来加,但是我这里偷懒了,因为之前的这几段part节点里,写了seq_of_statements | expression
,我就直接复用了原本的这几个part,真要1:1做成和ORACLE一样的话,这里都得完全拆开。
语法转换程序
修改了语法解析规则后,文章开头的那个问题其实就与case没有关系了,这里正常的解决方式就是识别到别名是关键字,加as就好了
// 非保留关键字作为字段名自动加as@Override public Void visitColumn_alias(PlSqlParser.Column_aliasContext ctx) {// 检查是否使用了非保留关键字且没有带 ASif (ctx.identifier().id_expression().regular_id() != null && ctx.AS() == null) {if (ctx.identifier().id_expression().regular_id().non_reserved_keywords_in_gaussdb() != null ||ctx.identifier().id_expression().regular_id().non_reserved_keywords_pre12c() != null ||ctx.identifier().id_expression().regular_id().non_reserved_keywords_in_12c() != null ||ctx.identifier().id_expression().regular_id().non_reserved_keywords_in_18c() != null ) {// 自动补充 ASrewriter.insertBefore(ctx.identifier().getStart(), "AS ");}}return visitChildren(ctx);}
总结
本次通过研究case when 语法发现了case表达式和case语句的差异,顺便修正了antlr4中的bug,让语法转换程序可以识别到这种语法来自动转换成目标库兼容的语法。
- 本文作者: DarkAthena
- 本文链接: https://www.darkathena.top/archives/syntax-traps-of-the-case-when-statement
- 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处