SQL字段解析器如何实现,方法是什么
Admin 2022-08-04 群英技术资讯 329 次浏览
想来很简单吧,因为 sql 中的字段列表,使用方式有限,比如 a as b, a, a b...
如果不想做复杂处理,最容易想到的,就是直接用某个特征做分割即可。比如,先截取出 字段列表部分,然后再用逗号',' 分割,就可以得到一个个的字段了。然后再要细分,其实只需要用 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; ... 我们只需依次枚举对应的情况,就可以将字段解析出来了。
看起来是个不错的想法。但是具体实现如何?
主要分两个部分,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 + '\'' + '}'; } }
它存在的意义,仅仅是为了使用方更方便取值,以为更进一步的解析提供了依据。
其实像写这种工具类,单元测试最是方便简单。因为最初的结果,我们早已预料,以测试驱动开发最合适不过了。而且,基本上一出现不符合预期的值时,很快速就定位问题了。
/** * 功能描述: 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()); } }
至此,一个简单的字段解析器完成。小工具,供参考!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:mmqy2019@163.com进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容。
猜你喜欢
SQL 内部链接INNER JOIN关键字选择两个表中具有匹配值的记录。_下文的讲解详细,步骤过程清晰,对大家进一步学习和理解相关知识有一定的帮助。有这方面学习需要的朋友就继续往下看吧!
这篇文章给大家分享的是有关sql临时表的创建和使用的内容,小编觉得挺实用的,因此分享给大家做个参考,另外在使用sql临时表时,有几点问题是需要注意的,接下来一起跟随小编看看吧。
本文通过介绍默认使用索引、强制使用聚集索引、强制使用非聚集索引让我们知道对于检索所有列结果集使用主键的聚集索引是最佳选择。有兴趣的朋友可以看下
这篇文章介绍了SQL Server新特性SequenceNumber的用法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
这篇文章主要个代价介绍SQL SERVER CDC开启实操详细内容,文明芝麻官内容有详细代码及解说,需要的小伙伴可以参考一下
成为群英会员,开启智能安全云计算之旅
立即注册Copyright © QY Network Company Ltd. All Rights Reserved. 2003-2020 群英 版权所有
增值电信经营许可证 : B1.B2-20140078 粤ICP备09006778号 域名注册商资质 粤 D3.1-20240008