简单sql字段解析器实现参考
用例:有一段sql语句,我们需要从中截取出所有字段部分,以便进行后续的类型推断,请给出此解析方法。
想来很简单吧,因为 sql 中的字段列表,使用方式有限,比如 a as b, a, a b...
1. 解题思路
如果不想做复杂处理,最容易想到的,就是直接用某个特征做分割即可。比如,先截取出 字段列表部分,然后再用逗号',' 分割,就可以得到一个个的字段了。然后再要细分,其实只需要用 as 进行分割就可以了。
看起来好像可行,但是存在许多漏洞,首先,这里面有太多的假设:各种截取部分要求必须符合要求,必须没有多余的逗号,必须要有as 等等。这明显不符合要求了。
其二,我们可以换一种转换方式。比如先截取到field部分,然后先以 as 分割,再以逗号分割,然后取最后一个词作为field。
看起来好像更差了,截取到哪里已经完全不知道了。即原文已经被破坏殆尽,而且同样要求要有 as 转换标签,而且对于函数觊觎有 as 的场景,就完全错误了。
其三,最好还是自行一个个单词地解析,field 字段无外乎几种情况,1. 普通字段如 select a; 2. 带as的普通字段如 select a as b; 3. 带函数的字段如 select coalesce(a, b); 4. 带函数且带as的字段如 select coalesce(a, b) ab; 5. 函数内带as的字段如 select cast(a as string) b; ... 我们只需依次枚举对应的情况,就可以将字段解析出来了。
看起来是个不错的想法。但是具体实现如何?
2. 具体解析实现
主要分两个部分,1. 需要定义一个解析后的结果数据结构,以便清晰描述字段信息; 2. 分词解析sql并以结构体返回;
我们先来看看整个算法核心:
/**
* 功能描述: 简单sql字段解析器
*
* 样例如1:
* select COALESCE(t1.xno, t2.xno, t3.xno) as xno,
* case when t1.no is not null then 1 else null end as xxk001,
* case when t2.no is not null then 1 else null end as xxk200,
* case when t3.xno is not null then 1 else null end as xx3200
* from xxk001 t1
* full join xxkj100 t2 on t1.xno = t2.xno
* full join xxkj200 t3 on t1.xno = t3.xno;
*
* 样例如2:
* select cast(a as string) as b from ccc;
*
* 样例如3:
* with a as(select cus,x1 from b1), b as (select cus,x2 from b2)
* select a.cus as a_cus from a join b on a.cus=b.cus where xxx;
*
* 样例如4:
* select a.xno,b.xx from a_tb as a join b_tb as b on a.id = b.id
*
* 样例如5:
* select cast \t(a as string) a_str, cc (a as double) a_double from x
*
*/
public class SimpleSqlFieldParser { /**
* 解析一段次标签sql 中的字段列表
*
* @param sql 原始sql, 需如 select xx from xxx join ... 格式
* @return 字段列表
*/
public static List<SelectFieldClauseDescriptor> parse(String sql) {
String columnPart = adaptFieldPartSql(sql);
int deep = 0;
List<StringBuilder> fieldTokenSwap = new ArrayList<>();
StringBuilder currentTokenBuilder = new StringBuilder();
List<SelectFieldClauseDescriptor> fieldList = new ArrayList<>();
fieldTokenSwap.add(currentTokenBuilder);
int len = columnPart.length();
char[] columnPartChars = columnPart.toCharArray();
for(int i = 0; i < len; i++) {
// 空格忽略,换行忽略,tab忽略
// 字符串相接
// 左(号入栈,++deep;
// 右)号出栈,--deep;
// deep>0 忽略所有其他直接拼接
// as 则取下一个值为fieldName
// case 则直接取到end为止;
//,号则重置token,构建结果集
char currentChar = columnPartChars[i];
switch (currentChar) {
case '(':
++deep;
currentTokenBuilder.append(currentChar);
break;
case ')':
--deep;
currentTokenBuilder.append(currentChar);
break;
case ',':
if(deep == 0) {
addNewField(fieldList, fieldTokenSwap, true);
fieldTokenSwap = new ArrayList<>();
currentTokenBuilder = new StringBuilder();
fieldTokenSwap.add(currentTokenBuilder);
break;
}
currentTokenBuilder.append(currentChar);
break;
case ' ':
case '\t':
case '\r':
case '\n':
if(deep > 0) {
currentTokenBuilder.append(currentChar);
continue;
}
if(currentTokenBuilder.length() == 0) {
continue;
}
// original_name as --> alias
if(i + 1 < len) {
int j = i + 1;
// 收集连续的空格
StringBuilder spaceHolder = new StringBuilder();
boolean isNextLeftBracket = false;
do {
char nextChar = columnPart.charAt(j++);
if(nextChar == ' ' || nextChar == '\t'
|| nextChar == '\r' || nextChar == '\n') {
spaceHolder.append(nextChar);
continue;
}
if(nextChar == '(') {
isNextLeftBracket = true;
}
break;
} while (j < len);
if(isNextLeftBracket) {
currentTokenBuilder.append(currentChar);
}
if(spaceHolder.length() > 0) {
currentTokenBuilder.append(spaceHolder);
i += spaceHolder.length();
}
if(isNextLeftBracket) {
// continue next for, function begin
continue;
}
}
if(fieldTokenSwap.size() == 1) {
if(fieldTokenSwap.get(0).toString().equalsIgnoreCase("case")) {
String caseWhenPart = CommonUtil.readSplitWord(
columnPartChars, i, " ", "end");
currentTokenBuilder.append(caseWhenPart);
if(caseWhenPart.length() <= 0) {
throw new BizException("语法错误,未找到case..when的结束符");
}
i += caseWhenPart.length();
}
}
addNewField(fieldList, fieldTokenSwap, false);
currentTokenBuilder = new StringBuilder();
fieldTokenSwap.add(currentTokenBuilder);
break;
// 空格忽略
default:
currentTokenBuilder.append(currentChar);
break;
} }
// 处理剩余尚未存储的字段信息
addNewField(fieldList, fieldTokenSwap, true);
return fieldList;
} /**
* 新增一个字段描述
*
* @param fieldList 字段容器
* @param fieldTokenSwap 候选词
*/
private static void addNewField(List<SelectFieldClauseDescriptor> fieldList,
List<StringBuilder> fieldTokenSwap,
boolean forceAdd) {
int ts = fieldTokenSwap.size();
if(ts == 1 && forceAdd) {
// db.original_name,
String fieldName = fieldTokenSwap.get(0).toString();
String alias = fieldName;
if(fieldName.contains(".")) {
alias = fieldName.substring(fieldName.lastIndexOf('.') + 1);
}
fieldList.add(new SelectFieldClauseDescriptor(fieldName, alias));
return;
}
if(ts < 2) {
return;
}
if(ts == 2) {
// original_name alias,
if(fieldTokenSwap.get(1).toString().equalsIgnoreCase("as")) {
return;
}
fieldList.add(new SelectFieldClauseDescriptor(
fieldTokenSwap.get(0).toString(),
fieldTokenSwap.get(1).toString()));
}
else if(ts == 3) {
// original_name as alias,
fieldList.add(new SelectFieldClauseDescriptor(
fieldTokenSwap.get(0).toString(),
fieldTokenSwap.get(2).toString()));
}
else {
throw new BizException("字段语法解析错误,超过3个以字段描述信息:" + ts);
}
} // 截取适配 field 字段信息部分
private static String adaptFieldPartSql(String fullSql) {
int start = fullSql.lastIndexOf("select ");
int end = fullSql.lastIndexOf(" from");
String columnPart = fullSql.substring(start + "select ".length(), end);
return columnPart.trim();
} }
应该说是比较简单的,一个for, 一个 switch ,就搞定了。其他的,更多的是逻辑判定。
下面我们来看看字段描述类的写法,其实就是两个字段,源字段和别名。
/**
* 功能描述: sql字段描述 select 字段描述类
*
*/
public class SelectFieldClauseDescriptor {
private String fieldName;
private String alias; public SelectFieldClauseDescriptor(String fieldName, String alias) {
this.fieldName = fieldName;
this.alias = alias;
} public String getFieldName() {
return fieldName;
} public String getAlias() {
return alias;
} @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SelectFieldClauseDescriptor that = (SelectFieldClauseDescriptor) o;
return Objects.equals(fieldName, that.fieldName) &&
Objects.equals(alias, that.alias);
} @Override
public int hashCode() {
return Objects.hash(fieldName, alias);
} @Override
public String toString() {
return "SelectFieldClauseDescriptor{" +
"fieldName='" + fieldName + '\'' +
", alias='" + alias + '\'' +
'}';
}
}
它存在的意义,仅仅是为了使用方更方便取值,以为更进一步的解析提供了依据。
3. 单元测试
其实像写这种工具类,最是方便简单。因为最初的结果,我们早已预料,以测试驱动开发最合适不过了。而且,基本上一出现不符合预期的值时,很快速就定位问题了。
/**
* 功能描述: sql字段解析器测试
**/
public class SimpleSqlFieldParserTest { @Test
public void testParse() {
String sql;
List<SelectFieldClauseDescriptor> parsedFieldList;
sql = "select COALESCE(t1.xno, t2.xno, t3.xno) as xno,\n" +
" case when t1.xno is not null then 1 else null end as xxk001,\n" +
" case when t2.xno is not null then 1 else null end as xxk200,\n" +
" case when t3.xno is not null then 1 else null end as xx3200\n" +
" from xxk001 t1\n" +
" full join xxkj100 t2 on t1.xno = t2.xno\n" +
" full join xxkj200 t3 on t1.xno = t3.xno;";
parsedFieldList = SimpleSqlFieldParser.parse(sql);
System.out.println("result:");
parsedFieldList.forEach(System.out::println);
Assert.assertEquals("字段个数解析不正确",
4, parsedFieldList.size());
Assert.assertEquals("字段别名解析不正确",
"xno", parsedFieldList.get(0).getAlias());
Assert.assertEquals("字段别名解析不正确",
"xx3200", parsedFieldList.get(3).getAlias()); sql = "select cast(a as string) as b from ccc;";
parsedFieldList = SimpleSqlFieldParser.parse(sql);
System.out.println("result:");
parsedFieldList.forEach(System.out::println);
Assert.assertEquals("字段个数解析不正确",
1, parsedFieldList.size());
Assert.assertEquals("字段别名解析不正确",
"b", parsedFieldList.get(0).getAlias()); sql = "with a as(select cus,x1 from b1), b as (select cus,x2 from b2)\n" +
" select a.cus as a_cus, cast(a \nas string) as a_cus2, " +
"b.x2 b2 from a join b on a.cus=b.cus where xxx;";
parsedFieldList = SimpleSqlFieldParser.parse(sql);
System.out.println("result:");
parsedFieldList.forEach(System.out::println);
Assert.assertEquals("字段个数解析不正确",
3, parsedFieldList.size());
Assert.assertEquals("字段别名解析不正确",
"a_cus", parsedFieldList.get(0).getAlias());
Assert.assertEquals("字段别名解析不正确",
"b2", parsedFieldList.get(2).getAlias()); sql = "select a.xno,b.xx,qqq from a_tb as a join b_tb as b on a.id = b.id";
parsedFieldList = SimpleSqlFieldParser.parse(sql);
System.out.println("result:");
parsedFieldList.forEach(System.out::println);
Assert.assertEquals("字段个数解析不正确",
3, parsedFieldList.size());
Assert.assertEquals("字段别名解析不正确",
"xno", parsedFieldList.get(0).getAlias());
Assert.assertEquals("字段别名解析不正确",
"qqq", parsedFieldList.get(2).getAlias()); sql = "select cast (a.a_int as string) a_str, b.xx, coalesce \n( a, b, c) qqq from a_tb as a join b_tb as b on a.id = b.id";
parsedFieldList = SimpleSqlFieldParser.parse(sql);
System.out.println("result:");
parsedFieldList.forEach(System.out::println);
Assert.assertEquals("字段个数解析不正确",
3, parsedFieldList.size());
Assert.assertEquals("字段别名解析不正确",
"a_str", parsedFieldList.get(0).getAlias());
Assert.assertEquals("字段原始名解析不正确",
"cast (a.a_int as string)", parsedFieldList.get(0).getFieldName());
Assert.assertEquals("字段别名解析不正确",
"qqq", parsedFieldList.get(2).getAlias());
Assert.assertEquals("字段原始名解析不正确",
"coalesce \n( a, b, c)", parsedFieldList.get(2).getFieldName());
}
}
至此,一个简单的字段解析器完成。小工具,供参考!
简单sql字段解析器实现参考的更多相关文章
- kotlin 写的一个简单 sql 查询解析器
package com.dx.efuwu.core import org.apache.commons.lang.StringUtils import java.sql.PreparedStateme ...
- atitit.java解析sql语言解析器解释器的实现
atitit.java解析sql语言解析器解释器的实现 1. 解析sql的本质:实现一个4gl dsl编程语言的编译器 1 2. 解析sql的主要的流程,词法分析,而后进行语法分析,语义分析,构建sq ...
- FFmpeg的H.264解码器源代码简单分析:解析器(Parser)部分
===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...
- c# 怎样能写个sql的解析器
c# 怎样能写个sql的解析器 本示例主要是讲明sql解析的原理,真实的源代码下查看 sql解析器源代码 详细示例DEMO 请查看demo代码 前言 阅读本文需要有一定正则表达式基础 正则表达式基础教 ...
- 自己动手实现一个简单的JSON解析器
1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...
- FFmpeg的HEVC解码器源代码简单分析:解析器(Parser)部分
===================================================== HEVC源代码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpe ...
- FFmpeg的HEVC解码器源码简单分析:解析器(Parser)部分
===================================================== HEVC源码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpeg ...
- 一个简单的json解析器
实现一个简单地json解析器. 两部分组成,词法分析.语法分析 词法分析 package com.mahuan.json; import java.util.LinkedList; import ja ...
- 用c#自己实现一个简单的JSON解析器
一.JSON格式介绍 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着很多优点.例如易读性更好,占用空间更 ...
随机推荐
- IT培训软件测试怎么样,问问“过来人”!
经常看到有人在网上发帖子问:"XX培训(IT培训机构)怎么样,学过的老哥可以出来讲讲真话吗?"问这种问题的同学,来,站起来!我不得不在这儿说你两句:你要想知道一家IT培训机构到底怎 ...
- 仅用 CSS 实现多彩、智能的阴影
背景 有没有想过如何创建从前景元素中继承某些颜色的阴影效果?阅读本文并找出如何实现方法吧! 前几天我经过家得宝(Home Depot,美国家得宝公司,全球领先的家居建材用品零售商),他们正在大规模展销 ...
- 使用MindSpore的线性神经网络拟合非线性函数
技术背景 在前面的几篇博客中,我们分别介绍了MindSpore的CPU版本在Docker下的安装与配置方案.MindSpore的线性函数拟合以及MindSpore后来新推出的GPU版本的Docker编 ...
- 推荐系统论文之序列推荐:KERL
KERL: A Knowledge-Guided Reinforcement Learning Modelfor Sequential Recommendation 摘要 时序推荐是基于用户的顺序行 ...
- Redis学习笔记六:持久化实验(AOF,RDB)
作者:Grey 原文地址:Redis学习笔记六:持久化实验(AOF,RDB) Redis几种持久化方案介绍和对比 AOF方式:https://blog.csdn.net/ctwctw/article/ ...
- redis分布式锁-spring boot aop+自定义注解实现分布式锁
接这这一篇redis分布式锁-java实现末尾,实现aop+自定义注解 实现分布式锁 1.为什么需要 声明式的分布式锁 编程式分布式锁每次实现都要单独实现,但业务量大功能复杂时,使用编程式分布式锁无疑 ...
- 服务器硬件必须支持M2 或PCIE才能支持NVME
兆芯服务器不支持NVME. 服务器硬件必须支持M2 或PCIE才能支持NVME.1 因为物理接口只有M2 SATA 和PCIE这三中但是NVME只支持M2 和PCIE这2种2所以 NVME不支持SAT ...
- libvirtd 启动成功,但却没有监听
现象: 执行 systemctl start libvirtd在其它机器上进行测试是否监听:virsh -c qemu+tcp://host/system libvirtd启动成功,没有报错,但却没有 ...
- MyBaits 全局配置文件(mybatis-config.xml)
什么是 MyBatis 全局配置文件 MyBatis 全局配置文件包含影响 MyBatis 框架正常使用的功能设置和属性信息. 它的作用好比手机里的设置图标,点击这个图标就可以帮助我们查看手机的属性信 ...
- 6.1 tar:打包备份
tar命令 在Linux系统里,tar是将多个文件打包在一起,并且可以实现解压打包的文件的命令.是系统管理员最常用的命令之一,tar命令不但可以实现对多个文件进行打包,还可以对多个文件打包后进 ...