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 相关文章推荐
SQL Server基本使用和简单的CRUD操作
Apr 05 SQL Server
SQL Server 数据库实验课第五周——常用查询条件
Apr 05 SQL Server
【HBU】数据库第四周 单表查询
Apr 05 SQL Server
mybatis调用sqlserver存储过程返回结果集的方法
May 08 SQL Server
解决sql server 数据库,sa用户被锁定的问题
Jun 11 SQL Server
SQL语句中JOIN的用法场景分析
Jul 25 SQL Server
MySQL 中如何归档数据的实现方法
Mar 16 SQL Server
详解在SQLPlus中实现上下键翻查历史命令的功能
Mar 18 SQL Server
SQL Server 中的事务介绍
May 20 SQL Server
SQL Server一个字符串拆分多行显示或者多行数据合并成一个字符串
May 25 SQL Server
SQL使用复合索引实现数据库查询的优化
May 25 SQL Server
解决sql server 数据库,sa用户被锁定的问题
在 SQL 语句中处理 NULL 值的方法
Jun 07 #SQL Server
sql中mod()函数取余数的用法
sql查询结果列拼接成逗号分隔的字符串方法
如何有效防止sql注入的方法
SQL 窗口函数实现高效分页查询的案例分析
mybatis调用sqlserver存储过程返回结果集的方法
You might like
基于mysql的论坛(4)
2006/10/09 PHP
PHP 金额数字转换成英文
2010/05/06 PHP
php gzip压缩输出的实现方法
2013/04/27 PHP
PHP中使用file_get_contents post数据代码例子
2015/02/13 PHP
PHP常见错误提示含义解释(实用!值得收藏)
2016/04/25 PHP
php从数据库中获取数据用ajax传送到前台的方法
2018/08/20 PHP
兼容ie、firefox的图片自动缩放的css跟js代码分享
2012/01/21 Javascript
JavaScript高级程序设计 阅读笔记(二十) js错误处理
2012/08/14 Javascript
javascript 星级评分效果(手写)
2012/12/24 Javascript
JSON中双引号的轮回使用过程中一定要小心
2014/03/05 Javascript
js中split和replace的用法实例
2015/02/28 Javascript
利用jQuery实现漂亮的圆形进度条倒计时插件
2015/09/30 Javascript
原生javascript实现解析XML文档与字符串
2016/03/01 Javascript
用vue构建多页面应用的示例代码
2017/09/20 Javascript
Vue实现剪切板图片压缩功能
2020/02/04 Javascript
js实现自动播放匀速轮播图
2020/02/06 Javascript
[02:41]辉夜杯现场一家三口 “我爸玩风行 我玩血魔”
2015/12/27 DOTA
[45:50]完美世界DOTA2联赛PWL S3 CPG vs Forest 第二场 12.16
2020/12/17 DOTA
python 写的一个爬虫程序源码
2016/02/28 Python
python的paramiko模块实现远程控制和传输示例
2017/10/13 Python
numpy.ndarray 交换多维数组(矩阵)的行/列方法
2018/08/02 Python
详解PANDAS 数据合并与重塑(join/merge篇)
2019/07/09 Python
python能在浏览器能运行吗
2020/06/17 Python
Python数据可视化实现多种图例代码详解
2020/07/14 Python
水芝澳美国官网:H2O Plus
2016/10/15 全球购物
英国领先的新鲜松露和最好的松露产品供应商:TruffleHunter
2019/08/26 全球购物
2013年高中生自我评价
2013/10/23 职场文书
校园报刊亭创业计划书
2014/01/02 职场文书
前厅收银主管岗位职责
2014/02/04 职场文书
协议书的格式
2014/04/23 职场文书
关于美容院的活动方案
2014/08/14 职场文书
市场调研项目授权委托书范本
2014/10/04 职场文书
2014年工程师工作总结
2014/11/25 职场文书
2014年小学图书室工作总结
2014/12/09 职场文书
2014年优秀班主任工作总结
2014/12/16 职场文书
解决linux下redis数据库overcommit_memory问题
2022/02/24 Redis