暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【ORACLE】case when语句的语法陷阱

前言

前一段时间在迁移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 ]
     END

simple_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>> ] CASE
  WHEN 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
    ;

// CASE

simple_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) { // 检查是否使用了非保留关键字且没有带 AS if (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 ) { // 自动补充 AS rewriter.insertBefore(ctx.identifier().getStart(), "AS "); } } return visitChildren(ctx); }
复制

总结

本次通过研究case when 语法发现了case表达式和case语句的差异,顺便修正了antlr4中的bug,让语法转换程序可以识别到这种语法来自动转换成目标库兼容的语法。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论