记录博主与2021年起研究sql解析，以及数据血缘提取与应用的文章
Aloudata BIG 全球首个实现算子级血缘解析的主动元数据平台
哈喽大家好，很久没有更新博客了，12月15日本周五公司进行了产品发布会，我也借此写篇博客，算个 milestone。 这是公司的宣传图，说实话，我作为血缘解析模块的owner，觉得这个“全球首个实现算子级血缘”的头衔还是挺有压力的 ，哈哈哈，应该至少是首个带入到商业化产品的算子级血缘主动元数据平台。 去年第一年，主要是以概念设立，能力验证为主，可以说是个算子血缘demo，能力还比较单一，只有表血缘、列血缘、算子血缘及加工口径信息，并将他们可视化出来，相当于给数据资产拍了个“CT”。 随着今年不断在客户现场打磨， 归总了很多客户的建议，最终产出了 1.0 正式版本，在算子血缘底座的基础上，加入了资产管理、打标扩散、变更通知、口径合并、血缘穿透等热门客户问题。 而我目前仍然从事着SQL解析、血缘分析、图谱构建及应用的工作，为打造更好的元数据平台。 当然，随着时间的推进，客户的增多，也衍生了越来越多的工作，从一开始简单SQL的解析，到后面命令语句、存储过程的解析，从一开始单一SQL的解析，到后面完整脚本的解析，不断的演进、完善、壮大，也拆分出了许多原子能力供公司其他产品使用，现在的BIG又进化到了一个新阶段。 也希望在之后的时间里我能在元数据领域继续钻研，保持行业第一的血缘解析能力，兼容各种复杂的场景，在精准、全面、智能的指标下做最棒的元数据平台，能够帮助到更多企业实现数据管理效率和质量的飞跃！ [scode type="green"]直播回放[/scode] [scode type="green"]【数据资产管理标杆案例】招商银行基于列算子血缘的模型优化和变更协同应用实践[/scode] 2023年12月20日 Aloudata 两项成果获评 2023 信通院大数据“星河”案例标杆案例。 随着招商银行业务数据化程度不断加深，数据规模快速扩大，数据关系日益复杂，其未来数据管理工作面临着如何持续有效保持数据架构先进性、如何高效开展全链路变更协同的挑战。 传统数据技术和专家主导的运动式治理方式已不能满足招商银行数据管理需要。为此，Aloudata 支持招商银行建设了契合其数据中台战略的新数据架构，形成了一套支持各中心开展模型设计、数据开发和数据服务的长效管理机制，主要举措如下： 元数据应用智能化 算子血缘、代码解构、图谱挖掘，将元数据智能化能力推广应用到业务场景中； 链路保障自动化 变更协同、精准评估、分级保障，将全链路质量保障能力沉淀到平台系统中； 架构治理长效化 数据去重、架构扁平、时效提升，将模型优化能力赋能到研发工具和工作中。 在该项目中，Aloudata BIG 主动元数据平台有力支撑招商银行构建了从源端到消费端的全链路算子血缘图谱，将列算子血缘分析技术和主动元数据管理能力整合到大数据研发平台，实现了 ETL Copilot 功能，并应用到模型优化和变更协同等场景，服务全行数据开发人员；在数仓重构项目中，辅助开发人员主动发现模型优化机会，大幅提升模型重构效率，判别重复资产，推动数仓快速实现资产数量下降 40%、平均链路缩短 50%；在日常链路保障场景中，自动监测上游变更，自动评估精准到字段的端到端影响，帮助运维人员预防或快速定位数据风险，让上下游协同丝滑衔接。 该方案为同业解决类似数据管理“老大难”问题提供了思路和方法，利用 Aloudata BIG 精准的列算子血缘分析能力，创新性地解决了以往依赖大量专家和经验才能实现效果的模型优化和链路保障难题。通过与研发平台等工具产品的集成，可以推广辅助更广泛的人群进行研发提效、质量保障和数据智能应用。 Aloudata BIG 解决了数据“看不清”、“管不住”的问题，其基于列算子级精准血缘构建的“主动元数据”能力也是国际前沿的 Data Fabric 架构主张的核心能力之一。该案例更是全球首个“算子级血缘”技术的落地实践，具有全球领先性。
上一篇讲到了 Trino/Presto的方言扩展，这一篇来说calcite如何使用Antlr去解析 首先你可以选择新建一个项目，引入calcite来做扩展，或者直接修改源码，这里我选择新建一个项目：demo地址
对于 Trino/Presto 的parser模块的基本梳理，可以参考这篇，这里以 sparkSQL为例，大家如果还想扩展其他方言是同理的 前言 （官方博客） 扩展流程 主要是基于 core/trino-parser 模块进行扩展 引入词法文件g4，这里以SparkSQL为例 编译生成词法分析器SparkSQLlexer，语法分析器SparkSQLparser，和SparkSQLBaseVisitor 新增SparkSQLAstBuilder，构建 Node 修改SqlParser的入口，可以选择 SparkSQL 新增语法Node，添加到AstVisitor中 在对应方言的AstBuilder中构建它 在SqlFormatter/ExpressionFormatter中添加它的组装逻辑 单测 （5~8步是重复操作） 这里我以扩展SparkSQL里的一个Any_vaule函数为例 大家可以根据更新的类文件更清晰看到改动点：demo地址 public class AnyValue extends Expression { private final Expression expression; private final Boolean isIgnoreNulls; public AnyValue(Expression expression, Boolean isIgnoreNulls) { this(Optional.empty(), expression, isIgnoreNulls); } public AnyValue(NodeLocation location, Expression expression, Boolean isIgnoreNulls) { this(Optional.of(location), expression, isIgnoreNulls); } protected AnyValue(Optional<NodeLocation> location, Expression expression, Boolean isIgnoreNulls) { super(location); this.expression = expression; this.isIgnoreNulls = isIgnoreNulls; } public Expression getExpression() { return expression; } public Boolean getIsIgnoreNulls() { return isIgnoreNulls; } @Override protected <R, C> R accept(AstVisitor<R, C> visitor, C context) { return visitor.visitAnyValue(this, context); } @Override public List<? extends Node> getChildren() { ImmutableList.Builder<Node> nodes = ImmutableList.builder(); nodes.add(expression); return nodes.build(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } AnyValue anyValue = (AnyValue) o; return Objects.equals(expression, anyValue.expression) && Objects.equals(isIgnoreNulls, anyValue.isIgnoreNulls); } @Override public int hashCode() { return Objects.hash(expression, isIgnoreNulls); } } Trino的Node设计的比较简单，需要重写的方法比较少，很适合新手选做入门AST起手练习。 我修改了parsingOptions这个类，可以选择解析的方言，调用不同的解析器。 debug看下类和结果： 更多的词法文件可以从antlr/gammer里获得
Trino 是 PrestoSQL在2020年改名后的项目，二者都是都一个创世团队开发的 概览 Presto地址：https://github.com/prestodb/presto Presto Parser模块： g4 Presto也是使用Antlr解析的，词法规则和语法规则在一个文件里：SqlBase.g4。TypeCalculation.g4 处理的内容比较简单，就是 SQL列的四则运算，但是这个g4应该是已经废弃了（2015年），这块的定义已经在SqlBase里了：（包括基于g4生成的实现类也只有SqlBase的） （注：presto使用antlr的版本是4.7.1，Trino为4.11.1） SQL解析器和AST构建器 parser包下放解析器入口（SqlParser）和AST构建器（AstBuilder），包括SQL进行词法解析语法解析会使用到的辅助类。其中核心类是SqlParser和AstBuilder SqlParser 其中SqlParser作为解析入口，主要工作是对SQL进行词法分析和语法分析生成解析树 ParseTree。 核心方法是 invokeParser() private Node invokeParser(String name, String sql, Function<SqlBaseParser, ParserRuleContext> parseFunction, ParsingOptions parsingOptions) { try { // 词法解析（这里CaseInsensitive进行了大写处理） SqlBaseLexer lexer = new SqlBaseLexer(new CaseInsensitiveStream(CharStreams.fromString(sql))); CommonTokenStream tokenStream = new CommonTokenStream(lexer); // 语法解析 SqlBaseParser parser = new SqlBaseParser(tokenStream); initializer.accept(lexer, parser); // Override the default error strategy to not attempt inserting or deleting a token. // Otherwise, it messes up error reporting parser.setErrorHandler(new DefaultErrorStrategy() { @Override public Token recoverInline(Parser recognizer) throws RecognitionException { if (nextTokensContext == null) { throw new InputMismatchException(recognizer); } else { throw new InputMismatchException(recognizer, nextTokensState, nextTokensContext); } } }); parser.addParseListener(new PostProcessor(Arrays.asList(parser.getRuleNames()), parsingOptions.getWarningConsumer())); lexer.removeErrorListeners(); lexer.addErrorListener(LEXER_ERROR_LISTENER); parser.removeErrorListeners(); if (enhancedErrorHandlerEnabled) { parser.addErrorListener(PARSER_ERROR_HANDLER); } else { parser.addErrorListener(LEXER_ERROR_LISTENER); } // 解析后的语法树的根节点 ParserRuleContext tree; try { // first, try parsing with potentially faster SLL mode parser.getInterpreter().setPredictionMode(PredictionMode.SLL); tree = parseFunction.apply(parser); } catch (ParseCancellationException ex) { // if we fail, parse with LL mode tokenStream.reset(); // rewind input stream parser.reset(); parser.getInterpreter().setPredictionMode(PredictionMode.LL); tree = parseFunction.apply(parser); } // 通过AstBuilder生成自定义AST return new AstBuilder(parsingOptions).visit(tree); } catch (StackOverflowError e) { throw new ParsingException(name + " is too large (stack overflow while parsing)"); } } 这里的返回值Node是Presto的自定义AST 这里需要说明的是 Presto 定义的 SqlBase.g4 里只接受大写的关键字Keywords和Identifier，CaseInsensitiveStream通过实现CharStream重写了LA方法： @Override public int LA(int i) { int result = stream.LA(i); switch (result) { case 0: case IntStream.EOF: return result; default: return Character.toUpperCase(result); } } 例如： select a as c1, 'a' as c2 from t1 这个直接交给 SqlBase.g4 识别是会报错的： 全部大写后： SELECT A AS C1, 'a' AS C2 FROM T2 AstBuilder SqlParser和AstBuilder是结合用的，我们可以看到 invokeParser() 最下面为 return new AstBuilder(parsingOptions).visit(tree); class AstBuilder extends SqlBaseBaseVisitor<Node> { private int parameterPosition; private final ParsingOptions parsingOptions; private final Consumer<ParsingWarning> warningConsumer; AstBuilder(ParsingOptions parsingOptions) { this.parsingOptions = requireNonNull(parsingOptions, "parsingOptions is null"); this.warningConsumer = requireNonNull(parsingOptions.getWarningConsumer(), "warningConsumer is null"); } @Override public Node visitSingleStatement(SqlBaseParser.SingleStatementContext context) { return visit(context.statement()); } // ... } AST 自定义的 AST 类都在 tree包下。（共177个类） 基类是 Node： public abstract class Node { private final Optional<NodeLocation> location; protected Node(Optional<NodeLocation> location) { this.location = requireNonNull(location, "location is null"); } /** * Accessible for {@link AstVisitor}, use {@link AstVisitor#process(Node, Object)} instead. */ protected <R, C> R accept(AstVisitor<R, C> visitor, C context) { return visitor.visitNode(this, context); } public Optional<NodeLocation> getLocation() { return location; } public abstract List<? extends Node> getChildren(); // Force subclasses to have a proper equals and hashcode implementation @Override public abstract int hashCode(); @Override public abstract boolean equals(Object obj); @Override public abstract String toString(); } Node 下面有相应的实现类：Statement、Expression、Relation等。Presto建的类比较多。 AstBuilder构建的过长就是构建这个Node的过程。 工具类与单测 SqlFormatter 和 ExpressionFormatter 可以将构建好的 Node / Expression 树结构还原为原始 SQL 或 表达式。其相应的单测可以参考 TestStatementBuilder 。 Sql转化为Node的单测可以参考：TestSqlParser。
从事SQL血缘解析一年后，谈谈我对它的理解
今天是2023年2月12日，据我上一次发表文章已经过去了半年多的时间，这篇文章主要是针对过去一年里参与解析工作的一些感悟，希望大家可以从中有所收获。 [scode type="share"]本文持续更新中[/scode] 目前博主在公司内具体研究方向为SQL解析与应用，在 BIG 产品中目前的SQL列级血缘解析在客户现场经验证已达到99%的准确率（未支持的特殊语法属于变量）。 由于讨论范围属于公司核心技术领域，本文只是稍微写一些我做解析一年多来的认识，可以给没有做过SQL血缘解析、或准备做其他解析的同学一些思路。我在2022年前3个月份发布了一些文章，那些都是我刚接触这一块时所记录的文章，现在看里面有许多错误的地方，那些文章就可以不用再看了。 SQL血缘解析所带来的价值和应用，这里就不再阐述了，这里直接上重点部分。 在此之前你可能需要先了解一些概念： DSL 词法解析，语法解析 抽象语法树（AST） 关系代数和逻辑执行计划 如果你不了解这几个概念，或者不是太熟悉，建议先在网上查阅一些资料。 那么，针对SQL的血缘解析，这里要分两个东西去看：解析 和 血缘 。当然，是 解析在前、血缘在后。 很明显，解析是血缘的基础，血缘是对解析的应用，如果把“SQL血缘解析”解释的再详细一些，那么就是“解析SQL并提取其中的血缘关系”。那么，当我们拿到血缘解析这个命题的时候，要分两步去做： 解析SQL 提取血缘 那么这里就牵出两个新的问题 如何解析SQL 如何提取血缘 当我们最早学习Mysql的时候，或多或少都有敲过它的一些语法，拿最简单的：SELECT c1 FROM t1;，如果我们把FROM不小心打成了FRPM，这个时候把命令提交给数据库引擎后就会报错，提示错误的语法等提示，那么这里需要思考的是，引擎它是怎么知道我不应该输入 FRPM，而是 FROM。 无论是SQL，还是JAVA还是其他语言，本质上都是一种DSL(domain special language)，而我们在输入某种特定的DSL时，其相应的程序第一步要做的事情就是校验你输入的语句是否符合正确的语法，简而言之就是会去解析你输入的语句。 在《编译原理》中，通常对文本的解析会包含词法分析，语法分析和语义分析，在语法分析和语义分析阶段都会产生相应的抽象语法树AST。因为对于一个文本 string ，我们对其里面的信息是无从下手的，所以我们需要将其转化成一种对象结构，就是指这里的抽象语法树AST。 （如果你对词法分析、语法分析、抽象语法树还不了解的话，需要先了解这些知识） 由此可知，我们解析一条sql，有以下两个作用： 判断该sql的语法是否正确 得到该sql的抽象语法树AST 那么我们可以通过哪些工具解析sql得到ast呢？ Antlr Calcite Druid （其他类似druid手写解析器） 所以，这里建议大家可以先了解下 Antlr 和 calcite，这里有我的一个对比 我主要详细说下 Antlr 的，去年1月份写的那篇文章可能已经过期了， Antlr 的词法文件还是很全的，可以在官方社区里找到： antlr/grammars-v4/tree/master/sql 我们可以看到主流的sql语法这里都支持了，对于其他的，例如sparkSQL，clickhouse内部也是使用的antlr，在他们对应的代码中我们也可以找到相应的g4文件。 Antlr的使用这里我也不过多赘述，起码我们解析一条sql，拿到生成的抽象语法树应该不成问题。 [scode type="green"]2023年12月19日更新[/scode] 当我们有了相应的解析树/抽象语法树（这里有两种不同的叫法，我觉得严格意义上都可以算作AST），这个时候我们可以选择从AST中去拿表表依赖关系了，只用递归遍历就好，用到的表都属于来源表，insert/create位置算输出表，应该表表血缘是最好提取，几乎不会失败。 然而列与列的依赖关系就复杂了一个量级，由于SQL本身结构的千变万化，如果直接从 AST 阶段直接分析列与列之间的依赖关系十分复杂，不同的方言之间又会造成 AST 不同，到后面只会不断地支持新语法，准确度也不了了之。 其实我们可以借鉴 Calcite，在抽象语法树之后经过校验转换到关系代数 RelNode 模型，在这一个模型上进行优化，这个模型已经和方言无关了，而且是一些数学领域的表达式，方便后续根据编排规则优化。 这里其实我建议也要设计一个方便于分析血缘的一个模型，这个模型与方言无关，突出列与列之间的依赖关系。这样你写好从该分析模型提取血缘的代码之后，之后需要补充的大多就是抽象语法树到该分析模型的代码了。 概括来说就是： SQL ----> AST ----> AnalyzeModel ----> Result 当然我在 BIG 中所做的要更加复杂，自研的分析模型除了要满足列与列之间的依赖关系，也要有列与算子之间的依赖关系，来做到算子级的血缘，核心点就在于这个分析模型如何去设计。2023年12月15日我们公司也进行了产品发布会，作为全球首个算子级血缘的主动元数据平台，这篇文章我记了个 milestone。
up目前的主要工作是做SQL解析，提取元数据信息和血缘信息。对于市场上相对成熟的SQL解析工具，如 calcite、druid（内含解析），这些活跃度高、解析速度也快，但是有一个通病是方言支持度不高，虽然可以扩展，但是也有些难度。（calcite使用jj，而druid是手写的词法解析和语法解析）。 从易扩展的角度目前我先选择了使用antlr解析SQL，因为g4方便扩展，github社区也有很多现成的g4文件可以使用。 而我目前主要解析 hive ，antlr官方语法库 grammer-v4 里也有相应的g4文件，我便直接使用进行解析。 但是在使用中就发现了 expression 规则定义的问题：and/or 没有优先级。 举个例子 SELECT c1, c2 FROM t1 WHERE c1 > 1 AND c2 < 3; 我们看看修改前的解析树长什么样： 我们可以看到右下部分中，WHERE条件通过操作符构成的树是不平衡的，也就是并没有区分 > AND < 这三个操作符的优先级。如果你的解析力度并不是很高，可以忽略此问题，但是很明显如果你要做还原的话这里就会出些问题，或者说会很繁琐。 那么，我们来看看修改后的解析树： 很显然，更改之后的解析树平衡了，而且可以根据 AND/OR 间断还原左右 expression，这对我自己接下来的开发工作带来了很大的帮助。 但是我毕竟修改也都修改过了，何不比直接提交给antlr官方，之后就不会有人遇到相应的问题了： PR 我不仅修改了 AND/OR 优先级的问题，同时对原有 expression 中相应语法规则名称进行了重定义，因为原先的定义有些冗余、乱，对解析带来了不小的麻烦。 提交给antlr之后，也是很快，审核人就给我修改的部分提出了一些 suggestion，我就根据他的 suggestion 又修改了部分代码，之后就是进入了漫长的自动化测试，一共二十多个，终于在晚饭结束之后，我到了 PR 被合并的通知。 我也是十分高兴，非常感谢 #Ivan Kochurkin 对我的建议和认可，我也跟高兴能为antlr仓库带来一些贡献！ 我们公司的 #绝云大 对此与给予我了肯定和鼓励： [scode type="green"]‒ 给 #皆非 点赞 antlr/grammars-v4/pull/2700 给顶级开源项目 antlr 贡献了很有价值的 PR 且得到项目核心成员的肯定。 我觉得开源也是我们与世界的一种相处模式：充满好奇，追求自由，坚持 DRY，平等包容，打磨技艺等等，这是这群人共同信奉的东西。 所以大家能跨越大洋国境的界限一同协作解决一个有趣的问题，让世界变得更好一点点。以开放待世界，世界也会回以拥抱，开源社区常说“保持正确的态度，有趣的事情自然会找到你”。 这是一个挺值得工程师去学习成长的地方，祝 #皆非 在社区走得更远。[/scode] 这将是我在开源的道路上留下的又一个重要的脚印，继续加油，砥砺前行！
1.定位 首先我们需要理解两个工具的定位，他们俩不是同一类工具，只是在解析这一块有小部分交集。 antlr是指可以根据输入自动生成语法树并可视化的显示出来的开源语法分析器,也就是不仅局限于解析sql，他可以分析任意定义好的语法。 calciteApache Calcite 是一款开源SQL解析工具, 可以将各种SQL语句解析成抽象语法术AST(Abstract Syntax Tree), 之后通过操作AST就可以把SQL中所要表达的算法与关系体现在具体代码之中 2.扩展性 可以看出来，antlr定位主要在解析方面，只要有语法、词法g4文件，我就可以解析任意的语言，支持面广，扩展性强（但不意味着就易扩展） 而calcite仅服务于sql（部分sql：mysql、oracle等） 3.功能 antlr仅限于解析，就是提取出来字符串中的信息，其余的任何操作都是你需要自己去完善的。 而calcite功能做的全面，已经封装好了很多功能：SQL 解析，SQL 校验，RBO、CBO优化等 4.思路 有了以上的区别，下面就是在实际解析思路上的区别。 首先是关于AST抽象语法树这一块，我们拿到sql时它只是一个字符串，java识别也是一个字符串，除了做一些字符串的操作外我们做不了其他事情。 calcite在对这个字符串进行解析的时候将其封装到了自己的一个抽象语法树中，这个AST抽象语法树是由若干 Select、From、Where、Join等类递归构建的，其父类为SqlNode，即 calcite将 String ————> SqlNode 原本仅仅是一个字符串的sql，calcite将其转化成了SqlNode，那么就可以递归的去访问SqlNode拿到一些内容 但是在之前我写的antlr解析的文章中是在解析的过程中就直接获取血缘关系，calcite为什么还要先将其转化一层。 我的理解是calcite不光光是解析，还有后面的校验和优化，都需要前面的结果做铺垫，所以先把字符串sql转化成SqlNode之后方便后续的操作，也节约了再解析的时间，而且直接对AST抽象语法树访问提取血缘关系也要比解析过程中直接提取简单一些（影响关系和依赖关系） 所以，如果我们要使用antlr去解析血缘关系，推荐是自己封装一套AST语法树（包含Select、From等等语句，把他们的父类抽象出来作为“节点“），然后在解析的时候先把sql解析为自己定义的抽象语法树，之后在对其做一些分析工作。 5.效率 据非官网测试，calcite对sql的解析效率比antlr高
[scode type="yellow"]本文内容已过期，请移步至“从事SQL血缘解析一年后，谈谈我对它的理解”[/scode] 上一篇介绍完了基于Antlr的sql血缘解析，这一篇我来说一说基于Calcite的 简介 Apache Calcite 是一款开源SQL解析工具, 可以将各种SQL语句解析成抽象语法语法树AST(Abstract Syntax Tree), 之后通过操作AST就可以把SQL中所要表达的算法与关系体现在具体代码之中。 calcite常用类介绍，和解析流程 calcite自定义driver、sql、优化等详解 以下两个github有不少calcite的test，可以参考，但是由于calcite版本比较老，所以一些代码已经废弃： calcite-test Calcite中文学习文档 基本使用 我们直入正题： 解析 我们先将sql语句解析为语法树，根节点为SqlNode（你可以类比为antlr的RuleText） SqlNode.getKind()可以获得当前节点是哪种节点，例如SELECT、INSERT、JOIN、AS等等 如果你会看antlr的解析树，其实你debug SqlNode的结构时其实差不多，都是一级一级往下找 先上代码： 依赖 org.apache.calcite calcite-core 1.29.0 SqlNode 网上很多的写法例如ConfigBuilder这种已经废弃了，尽量参考calcite源码中SqlParseTest中的方法： @Test public void testnormal() throws Exception{ String sql = "select a from t1"; SqlParser sqlParser = getSqlParser(sql, null); SqlNode sqlNode = sqlParser.parseStmt(); System.out.println(sqlNode.toString()); } public SqlParser getSqlParser(String sql, SqlDialect sqlDialect) { return getSqlParser(new SourceStringReader(sql), getTransform(sqlDialect)); } protected SqlParser getSqlParser(Reader source, UnaryOperator<SqlParser.Config> transform) { final SqlParser.Config configBuilder = SqlParser.config() .withParserFactory(parserImplFactory()); final SqlParser.Config config = transform.apply(configBuilder); return SqlParser.create(source, config); } private static UnaryOperator<SqlParser.Config> getTransform(SqlDialect dialect) { return dialect == null ? UnaryOperator.identity() : dialect::configureParser; } protected SqlParserImplFactory parserImplFactory() { return SqlParserImpl.FACTORY; } 递归遍历SqlNode 这里只举个例子： public static List<String> getDependencies(SqlNode sqlNode, List<String> result) throws SqlParseException { if (sqlNode.getKind() == JOIN) { SqlJoin sqlKind = (SqlJoin) sqlNode; System.out.println("-----join"); getDependencies(sqlKind.getLeft(), result); getDependencies(sqlKind.getRight(), result); } if (sqlNode.getKind() == IDENTIFIER) { System.out.println("-----identifier"); result.add(sqlNode.toString()); } if (sqlNode.getKind() == INSERT) { SqlInsert sqlKind = (SqlInsert) sqlNode; System.out.println("-----insert"); getDependencies(sqlKind.getSource(), result); } if (sqlNode.getKind() == SELECT) { SqlSelect sqlKind = (SqlSelect) sqlNode; System.out.println("-----select"); getDependencies(sqlKind.getFrom(), result); } if (sqlNode.getKind() == AS) { SqlBasicCall sqlKind = (SqlBasicCall) sqlNode; System.out.println("----as"); getDependencies(sqlKind.getOperandList().get(0), result); } if (sqlNode.getKind() == UNION) { SqlBasicCall sqlKind = (SqlBasicCall) sqlNode; System.out.println("----union"); getDependencies(sqlKind.getOperandList().get(0), result); getDependencies(sqlKind.getOperandList().get(1), result); } if (sqlNode.getKind() == ORDER_BY) { SqlOrderBy sqlKind = (SqlOrderBy) sqlNode; System.out.println("----order_by"); getDependencies(sqlKind.getOperandList().get(0), result); } return result; } 第一次传入的sqlNode就是很节点，这里的result集合是记录源表表名的，这里建议大家去打个断点debug一下sqlNode，往里面扒一扒，你就能搞明白calcite构建sqlNode的一个逻辑了 所以这里要强调的是，Calcite对SqlNode的构建是一个递归的构建，我们对sqlNode语法树的解析是一个从外到内、从左向右的一个先序深度遍历，这点和antlr不同，要额外注意。（这一块也确实比较费脑子） 校验 以上只是对一条sql一个基本解析，并没有校验，对于select *这种我们无法得知 里的内容，所以校验阶段就是根据已经有的元数据去校验该sql语句是否合法（表、字段名存不存在），最重要的一点是它会把 替换为该表的所有字段名，之后我们再重复上述解析即可： public class TestOne { public static class TestSchema { public final Triple[] rdf = {new Triple("s", "p", "o")}; } public static void main(String[] args) { SchemaPlus schemaPlus = Frameworks.createRootSchema(true); //给schema T中添加表 schemaPlus.add("T", new ReflectiveSchema(new TestSchema())); Frameworks.ConfigBuilder configBuilder = Frameworks.newConfigBuilder(); //设置默认schema configBuilder.defaultSchema(schemaPlus); FrameworkConfig frameworkConfig = configBuilder.build(); SqlParser.ConfigBuilder paresrConfig = SqlParser.configBuilder(frameworkConfig.getParserConfig()); //SQL 大小写不敏感 paresrConfig.setCaseSensitive(false).setConfig(paresrConfig.build()); Planner planner = Frameworks.getPlanner(frameworkConfig); SqlNode sqlNode; RelRoot relRoot = null; try { //parser阶段 sqlNode = planner.parse("select \"a\".\"s\", count(\"a\".\"s\") from \"T\".\"rdf\" \"a\" group by \"a\".\"s\""); //validate阶段 planner.validate(sqlNode); //获取RelNode树的根 relRoot = planner.rel(sqlNode); } catch (Exception e) { e.printStackTrace(); } RelNode relNode = relRoot.project(); System.out.print(RelOptUtil.toString(relNode)); } } public class Triple { public String s; public String p; public String o; public Triple(String s, String p, String o) { super(); this.s = s; this.p = p; this.o = o; } } Meta接口 除了我们自己手动递归sqlNode以外，我们也可以通过Meta接口下的相关API来获取sql的血缘： （待补充） 自定义sql解析 这个主要是针对原有parse.jj中可能并没有处理我们特殊sql语句的关键字，我们需要对其扩展补充，来实现自定义sql解析，具体可以看我们最上面放的连接，calcite-test里有现成的自定义解析sql代码，但是要注意最新的calcite-core有不少改动，不能完全按照它里面的思路写
[scode type="yellow"]本文内容已过期，请移步至“从事SQL血缘解析一年后，谈谈我对它的理解”[/scode] 上一篇文章讲了antlr的一个简单上手使用，这篇主要说下他的一个重要作用，就是解析sql，并提取其中表、字段信息，同时记录血缘关系，最后将其存入atlas中。 首先我们需要有 postgreSql的g4文件，当然这个肯定不是让你写的，否则累死了，我能在网上找到的postgreSql.g4是在两个地方，首先是 antlr 的github官网 有这个，但很可惜他解析出来的是csharp文件，我们java用不了，所以使用的是另一个民间大神的g4文件 antlr_psql 有了 g4 文件，我们便可以生成最重要的 Parser 文件： 其次我要说下antlr里一些比较重要的概念 RuleNode 我们g4文件里定义的一个个规则在 antlr 看来都是一个个的节点，比如 select、from、GROUP BY等，而正是这么多若干的RuleNode最后形成了我们一颗大的解析树： 类似这个解析树里的一个个节点：select_stmt、colum_list等，这些也都是一开始定义在g4文件里的 然后是访问解析树的两种模式：监听者模式 和 访问者visitor模式，我这里主要说visitor模式 Visitor模式 PostgreSQLParserVisitor.java是个访问器接口，定义了一些访问RuleNode的接口方法，可以通过实现它来完成自定义的功能。而PostgreSQLParserBaseVisitor.java是该接口的默认实现，它为每个接口方法提供了空实现。 我们可以自己写一个类继承 PostgreSQLParserBaseVisitor ，并重写自己想要处理节点的方法，即可做自己的处理： 例如上图，我们能从解析树中看到 select_stmt这个节点，我们可以通过访问这个节点拿到它下面的列，再而即可拿到字段名字，而重写方法名只用在前面加上visit即可： public class PostgreSqlFieldLineageParser extends PostgreSQLParserBaseVisitor { @Override public Object visitSelect_stmt(PostgreSQLParser.Select_stmtContext ctx) { ctx.selector_clause().column_list().expr().forEach( exprContext -> { exprContext.identifier().forEach(identifierContext -> { System.out.println("visitSelect_stmt : " + identifierContext.getRuleContext().getText()); }); }); return super.visitSelect_stmt(ctx); } } 这样我们就可以在 antlr 访问解析树时拿到想要的字段，其中PostgreSQLParser.Select_stmtContext ctx 就是select_stmt节点的上下文，我们可以拿到它父子节点的值。 但是主要我们最后要 return 父级该方法，这是和监听器机制的不同： [scode type="yellow"]注意：ANTLR的运行库提供了两种遍历树的机制：监听器机制与访问器机制。与访问器不同的是，监听器的方法会被ANTLR提供的遍历器对象（比如ParseTreeWalker）自动调用，而在访问器的方法中，必须显示调用visit方法来访问子节点。如果没有调用visit方法就会导致对应的子树不被访问。而且监听器方法是没有返回值的（即返回类型是void）。[/scode] 我们调用visit方法即可访问节点信息： //调用该方法 public void parseSqlFieldLineage(String sql) { PostgreSqlFieldLineageParser visitor = new PostgreSqlFieldLineageParser(); visitor.visit(getParseTree(sql)); } private ParseTree getParseTree(String sql) { CharStream input = CharStreams.fromString(sql); PostgreSQLLexer lexer = new PostgreSQLLexer(input); CommonTokenStream tokenStream = new CommonTokenStream(lexer); PostgreSQLParser parser = new PostgreSQLParser(tokenStream); return parser.root(); } 如果我们要拿到 from 后的表名，同理： @Override public Object visitFrom_clause(PostgreSQLParser.From_clauseContext ctx) { PostgreSqlFieldModel postgreSqlFieldModel = map.get(lastHashcode); ctx.from_item().forEach(from_itemContext -> { //别名 if (from_itemContext.alias() != null) { System.out.println("from : " + from_itemContext.alias().identifier().getRuleContext().getText()); } //表名 if (from_itemContext.table_name_() != null) { System.out.println("from : " + from_itemContext.table_name_().getRuleContext().getText()); } }); return super.visitFrom_clause(ctx); } 由此我们还可以得出结论，当有子查询的时候visitSelect_stmt方法会访问多次，因为select_stmt节点可能会有多个，每次只能得到当前节点子节点的一些值，但我们需要知道 visitor模式访问解析树是深度优先 （记录父子节点关系的具体代码后续更新，列血缘比较麻烦，有一些别名、计算类的情况没有完善到） 存入atlas 当我们有了表与表、列于列之间的关系的时候，便可以通过rest API存入atlas，我这里通过 AtlasClientV2