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

server/2025/2/11 0:10:11/

前言

前一段时间在迁移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 许可协议。转载请注明出处

http://www.ppmy.cn/server/166610.html

相关文章

Unity项目接入xLua的一种流程

1. 导入xlua 首先导入xlua&#xff0c;这个不用多说 2. 编写C#和Lua交互脚本 基础版本&#xff0c;即xlua自带的版本 using System.Collections; using System.Collections.Generic; using UnityEngine; using XLua; using System; using System.IO;[Serializable] public…

【CPP】CPP经典面试题

文章目录 引言1. C 基础1.1 C 中的 const 关键字1.2 C 中的 static 关键字 2. 内存管理2.1 C 中的 new 和 delete2.2 内存泄漏 3. 面向对象编程3.1 继承和多态3.2 多重继承 4. 模板和泛型编程4.1 函数模板4.2 类模板 5. STL 和标准库5.1 容器5.2 迭代器 6. 高级特性6.1 移动语义…

网络工程师 (23)OSI模型层次结构

前言 OSI&#xff08;Open System Interconnect&#xff09;模型&#xff0c;即开放式系统互联模型&#xff0c;是一个完整的、完善的宏观模型&#xff0c;它将计算机网络体系结构划分为7层。 OSI七层模型 1. 物理层&#xff08;Physical Layer&#xff09; 功能&#xff1a;负…

运维_Mac环境单体服务Docker部署实战手册

Docker部署 本小节&#xff0c;讲解如何将前端 后端项目&#xff0c;使用 Docker 容器&#xff0c;部署到 dev 开发环境下的一台 Mac 电脑上。 1 环境准备 需要安装如下环境&#xff1a; Docker&#xff1a;容器MySQL&#xff1a;数据库Redis&#xff1a;缓存Nginx&#x…

【Prometheus】如何通过golang生成prometheus格式数据

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Linux proc虚拟文件系统

文章目录 简介proc常用节点pid节点procfs接口参考 简介 测试环境&#xff1a;Linux dev-PC 5.18.17-amd64-desktop-hwe #20.01.00.10 SMP PREEMPT_DYNAMIC Thu Jun 15 16:17:50 CST 2023 x86_64 GNU/Linux proc虚拟文件系统是linux内核提供的一种让用户和内核内部数据结构进行交…

如何使用Spring Boot框架整合Redis:超详细案例教程

目录 # 为什么选择Spring Boot与Redis整合&#xff1f; 1. 更新 pom.xml 2. 配置application.yml 3. 创建 Redis 配置类 4. Redis 操作类 5. 创建控制器 6. 启动应用程序 7. 测试 # 为什么选择Spring Boot与Redis整合&#xff1f; 将Spring Boot与Redis整合可以充分利…

Linux系统编程:深入理解计算机软硬件体系和架构

一、硬件体系 首先我们要知道&#xff0c;我们最常见的计算机&#xff08;笔记本&#xff09;以及我们不常见的计算机&#xff08;服务器&#xff09;其实本质上都是一堆硬件的结合&#xff1a;cpu、网卡、显卡、内存、磁盘、显示器、键盘…… 但他们并不是毫无章法地放在一起…