sql字段解析器的实现示例

目录
  • 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字段解析器内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 解析mysql不重复字段值求和

    在使用mysql时,有时需要查询出某个字段不重复的记录,虽然mysql提供有distinct这个关键字来过滤掉多余的重复记录只保留一条,但往往只用它来返回不重复记录的条数,而不是用它来返回不重记录的所有值.其原因是distinct只能返回它的目标字段,而无法返回其它字段,这个问题让我困扰了很久,用distinct不能解决的话,我只有用二重循环查询来解决,而这样对于一个数据量非常大的站来说,无疑是会直接影响到效率的.所以我花了很多时间来研究这个问题,网上也查不到解决方案..下面先来看看例子:  

  • 解析如何用SQL语句在指定字段前面插入新的字段

    create proc addcolumn@tablename varchar(30), --表名@colname varchar(30), --要加的列名@coltype varchar(100), --要加的列类型@colid int --加到第几列as declare @colid_max intdeclare @sql varchar(1000) --动态sql语句--------------------------------------------------if not exist

  • MYSQL替换时间(年月日)字段时分秒不变实例解析

    写法1: update sas_order_supply_month_pay set RECEIVE_TIME=REPLACE(RECEIVE_TIME,DATE_FORMAT(RECEIVE_TIME,'%Y-%m-%d'),(select PERIOD_END from sas_task_supply_month_pay_period where belong='1729' and CREATE_TIME like '%2017-07-12%')) where ORDER_CODE='PO2

  • sql字段解析器的实现示例

    目录 1. 解题思路 2. 具体解析实现 3. 单元测试 用例:有一段sql语句,我们需要从中截取出所有字段部分,以便进行后续的类型推断或者别名字段抽取定义,请给出此解析方法. 想来很简单吧,因为 sql 中的字段列表,使用方式有限,比如 a as b, a, a b... 1. 解题思路 如果不想做复杂处理,最容易想到的,就是直接用某个特征做分割即可.比如,先截取出 字段列表部分,然后再用逗号',' 分割,就可以得到一个个的字段了.然后再要细分,其实只需要用 as 进行分割就可以了. 看起来好

  • Java Class 解析器实现方法示例

    最近在写一个私人项目,名字叫做ClassAnalyzer,ClassAnalyzer的目的是能让我们对Java Class文件的设计与结构能够有一个深入的理解.主体框架与基本功能已经完成,还有一些细节功能日后再增加.实际上JDK已经提供了命令行工具javap来反编译Class文件,但本篇文章将阐明我实现解析器的思路. Class文件 作为类或者接口信息的载体,每个Class文件都完整的定义了一个类.为了使Java程序可以"编写一次,处处运行",Java虚拟机规范对Class文件进行了严

  • SQL提取数据库表名及字段名等信息代码示例

    本文向大家介绍了使用SQL语句提取数据库所有表的表名.字段名的实例代码,在SQLserver 中进行了测试,具体内容如下: --查询所有用户表所有字段的特征 SELECT D.Name as TableName, A.colorder AS ColOrder, A.name AS Name, COLUMNPROPERTY(A.ID,A.Name, 'IsIdentity') AS IsIdentity, CASE WHEN EXISTS (SELECT 1 FROM dbo.sysobjects

  • MyBatis-Plus 动态表名SQL解析器的实现

    一.引言 先来说下动态名表在什么场景下需要使用呢? 拿小编的实际项目来说,小编公司手里掌握着国内各个部分地区的医院患者数据,那么一个医院的患者的数据流量肯定是很大的,这个时候如果全部放在同一张表中,那么可想而知数据量的庞大.所以数据库设计的时候可以一家医院对应一张表,分开来存储,表中的列名都是一样的,只是表名不同. 或者还可以做日志的存储,日志数据量也是很大的,可以分一个月对应一张表,比如:log_201907.log_201908等等之类的. 二.具体实现 动态表名SQL解析器也是基于MP分页

  • 使用JSX 建立组件 Parser(解析器)开发的示例

    目录 JSX 环境搭建 建立项目 初始化 NPM 安装 webpack 安装 Babel 配置 webpack 安装 Babel-loader 模式配置 引入 JSX JSX 基本用法 JSX 基础原理 实现 createElement 函数 实现自定义标签 这里我们一起从 0 开始搭建一个组件系统.首先通过上一篇<前端组件化基础知识>中知道,一个组件可以通过 Markup 和 JavaScript 访问的一个环境. 所以我们的第一步就是建立一个可以使用 markup 的环境.这里我们会学习使

  • 手写一个@Valid字段校验器的示例代码

    上次给大家讲述了 Springboot 中的 @Valid 注解 和 @Validated 注解的详细用法: 详解Spring中@Valid和@Validated注解用法 当我们用上面这两个注解的时候,需要首先在对应的字段上打上规则注解,类似如下. @Data public class Employee { /** 姓名 */ @NotBlank(message = "请输入名称") @Length(message = "名称不能超过个 {max} 字符", max

  • C++实现xml解析器示例详解

    目录 xml格式简单介绍 xml格式解析过程浅析 代码实现 实现存储解析数据的类——Element 关键代码1——实现整体的解析 关键代码2——解析所有元素 开发技巧 有关C++的优化 额外注意 xml格式简单介绍 <?xml version="1.0"?> <!--这是注释--> <workflow> <work name="1" switch="on"> <plugin name=&quo

  • SQL Server解析/操作Json格式字段数据的方法实例

    目录 1 json存储 2 json操作 3其他 总结 1 json存储 在sqlserver 中存储json ,需要用字符串类型进行存储,一般用nvarchar()或 varchar()进行存储,不要用text进行存储,用text时候,json的函数不支持. 2 json操作 主要介绍5个函数: (1)openJson:打开Json字符串 (2)IsJson:判断一个字符串是不是合法的Json格式.是返回1,否返回0,null返回null. (3)Json_Value:从Json字符串中提取值

  • Mybatis中的动态SQL语句解析

    这篇文章主要介绍了Mybatis中的动态SQL语句解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Mybatis中配置SQL有两种方式,一种是利用xml 方式进行配置,一种是利用注解进行配置. Mybatis使用注解配置SQL,但是由于配置功能受限,而且对于复杂的SQL而言可读性很差,所以很少使用. Mybatis常用xml配置的方式,使用xml的几个简单的元素,便能完成动态SQL的功能,大量的判断都可以在mybaties的映射xml里面配

  • mybatis-plus拦截器、字段填充器、类型处理器、表名替换、SqlInjector(联合主键处理)

    目录 组件介绍 表名处理器 字段填充器 类型处理器 补充 最近有个练手的小例子,大概就是配置两个数据源,从一个数据源读取数据写到另一个数据源,虽然最后做了出来,但是不支持事务...就当是对mybatis-plus/mybatis组件使用方式的记录吧,本次例子使用的仍是mybatis-plus 回忆一下mybatis核心对象: Configuration 初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如,插件,映射器,ObjectFactory和typeHandler对象,MyB

随机推荐