sql字段解析器的实现示例


Posted in SQL Server onJune 23, 2021
目录
  • 1. 解题思路
  • 2. 具体解析实现
  • 3. 单元测试

用例:有一段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字段解析器的实现示例的文章就介绍到这了,更多相关sql字段解析器内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

SQL Server 相关文章推荐
SqlServer 垂直分表(减少程序改动)
Apr 16 SQL Server
SQL Server中交叉联接的用法详解
Apr 22 SQL Server
SQL 窗口函数实现高效分页查询的案例分析
May 21 SQL Server
在 SQL 语句中处理 NULL 值的方法
Jun 07 SQL Server
sql server删除前1000行数据的方法实例
Aug 30 SQL Server
SQLServer中exists和except用法介绍
Dec 04 SQL Server
如何使用SQL Server语句创建表
Apr 12 SQL Server
使用MybatisPlus打印sql语句
Apr 22 SQL Server
SQL Server中搜索特定的对象
May 25 SQL Server
SQL解决未能删除约束问题drop constraint
May 30 SQL Server
SQL中的连接查询详解
Jun 21 SQL Server
在SQL Server中使用 Try Catch 处理异常的示例详解
Jul 15 SQL Server
解决sql server 数据库,sa用户被锁定的问题
在 SQL 语句中处理 NULL 值的方法
Jun 07 #SQL Server
sql中mod()函数取余数的用法
sql查询结果列拼接成逗号分隔的字符串方法
如何有效防止sql注入的方法
SQL 窗口函数实现高效分页查询的案例分析
mybatis调用sqlserver存储过程返回结果集的方法
You might like
咖啡界又出新概念,无需咖啡豆的分子咖啡
2021/03/03 咖啡文化
session 的生命周期是多长
2006/10/09 PHP
PHP新手上路(十三)
2006/10/09 PHP
php中的strpos使用示例
2014/02/27 PHP
php 购物车完整实现代码
2014/06/05 PHP
PHP中is_dir()函数使用指南
2015/05/08 PHP
PHP实现简单数字分页效果
2015/07/26 PHP
分享一个漂亮的php验证码类
2016/09/29 PHP
PHP依赖注入原理与用法分析
2018/08/21 PHP
js类中获取外部函数名的方法
2007/08/19 Javascript
JQuery Dialog的内存泄露问题解决方法
2010/06/18 Javascript
file模式访问网页时iframe高度自适应解决方案
2013/01/16 Javascript
jQuery老黄历完整实现方法
2015/01/16 Javascript
jquery获取url参数及url加参数的方法
2015/10/26 Javascript
Javascript中的prototype与继承
2017/02/06 Javascript
深入解析nodejs HTTP服务
2017/07/25 NodeJs
基于VUE移动音乐WEBAPP跨域请求失败的解决方法
2018/01/16 Javascript
vue 使用ref 让父组件调用子组件的方法
2018/02/08 Javascript
Python使用urllib2获取网络资源实例讲解
2013/12/02 Python
分析Python编程时利用wxPython来支持多线程的方法
2015/04/07 Python
python利用不到一百行代码实现一个小siri
2017/03/02 Python
Python使用pyodbc访问数据库操作方法详解
2018/07/05 Python
python中的不可变数据类型与可变数据类型详解
2018/09/16 Python
TensorBoard 计算图的查看方式
2020/02/15 Python
python GUI库图形界面开发之PyQt5滑块条控件QSlider详细使用方法与实例
2020/02/28 Python
Python 从attribute到property详解
2020/03/05 Python
欧洲高端品牌直销店:Fashionesta
2016/08/31 全球购物
波兰最大的儿童服装连锁店之一:5.10.15.
2018/02/11 全球购物
领导视察欢迎词
2014/01/15 职场文书
党的群众路线教育实践活动通讯稿
2014/09/10 职场文书
班主任先进事迹材料
2014/12/17 职场文书
办公用品管理制度
2015/08/04 职场文书
tensorflow中的数据类型dtype用法说明
2021/05/26 Python
Linux安装apache服务器的配置过程
2021/11/27 Servers
Ruby使用Mysql2连接操作MySQL
2022/04/19 Ruby
不想升级Win11?教你彻底锁定老版Windows系统的方法(附下载地址)
2022/09/23 数码科技