Skip to content

使用Antlr4实现StarRocks SQL的表血缘分析

Posted on:2022年8月30日 at 15:03 (5 min read)

TOC

Open TOC

前言

数据血缘(Data Lineage)又叫做数据起源(Data Provenance)或者数据家谱(Data Pedigree)。用来描述数据的来源以及数据随时间移动的位置关系。 数据血缘是数据资产的重要组成部分,用于分析表和字段从数据源到当前表的血缘路径,以及血缘字段之间存在的关系是否满足,并关注数据一致性以及表设计的合理。它描述了数据从收集,生产到服务的全链路的变化和存在形式。

实现思路

通常要实现基于 SQL 的数据血缘分析,需要先获取到 SQL AST,然后针对获取对应数据模式下的来源表名和写入表名。在 starrocks 中官方提供的插件不能够完整获取 sql ast,但是额外的官方提供了 antlr4 语法文件。因此我们可以借助 antlr4 对原始 starrocks sql 做词法和语法分析,获取 ast。借助 antlr4 提供的两种 ast 遍历模式:Visitor 方式和 Listener 方式。我们就可以实现对应 SQL 的表名获取。

表血缘

在 starrocks 中做数据处理,离不开中间表。通常的 sql 样式为:目标表永远只有一个,来源表是 select 中所有 from 的真实表。

INSERT INTO ads_user_log_di
SELECT user_id, count(1)  FROM view_log
WHERE date BETWEEN '2022-08-01' AND '2022-08-30'
GROUP BY user_id

针对这样的 sql,对应的 ast 为

我们期望获取其中 insert into 的表名和 from 的表名。这样就可以实现表维度的血缘关系分析。

Starrocks 语法解析

antlr4 语法文件:

StarRocks.g4 & StarRocksLex.g4 at main · StarRocks/starrocks

但是有两个问题需要解决:

  1. 语法文件中包含 java 语法,生成 Go 代码后不兼容
  2. 语法文件中部门函数名和 Go 接口名冲突

让我们先来解决第一个问题: 在StarRocksLex.g4 文件中存在下面两个 java 语法

@parser::members {public static long sqlMode;}

//....

LOGICAL_OR: '||' {setType((StarRocksParser.sqlMode & com.starrocks.qe.SqlModeHelper.MODE_PIPES_AS_CONCAT) == 0 ? LOGICAL_OR : StarRocksParser.CONCAT);};

可以移除或替换为go语法,例如
lexer grammar StarRocksLex;
// 移除java私有变量
// @parser::members {public static long sqlMode;}

// 或替换为go语法规则
// @parser::members {
//  var sqlMode int
// }

tokens {
    CONCAT
}

针对第二个问题: 可以参考下issue 3758

After looking over the generated files, and finding nothing obvious, I decided to check for the problem using automated means. I created a script (see below) that creates a new grammar with all parser rule symbols with a trailing underscore. It renames a symbol back to the original name without the underscore, and test how it builds.

The problem is “string”. A workaround is to rename it to something else, like “string_“. This needs to be added to the escaping code in the Antlr tool for Go.

所以解决办法也很简单,找到 StarRocks.g4 文件中如 string 等地方,替换成其他名称如 xstring string_ 等。

有个更简单的查询办法,先编译一版,运行时会提示不兼容。

这时根据对应的语法错误提示,就可以快速的找到对应的 token 名。然后替换下就可以了。 如:

literalExpression
: NULL                                                                                #nullLiteral
| booleanValue                                                                        #booleanLiteral
| number                                                                              #numericLiteral
| (DATE | DATETIME) stringx                                                            #dateLiteral
| string                                                                              #stringLiteral
| interval                                                                            #intervalLiteral
;

其中第 6 行的 string 就可以替换成 xstring 来解决这个问题。

更进一步:字段血缘

对于字段血缘实现会麻烦一点,因为要将每个结果字段的层层关系找到并最后对应上真实表的字段,可能中间还会有多个字段计算为一个字段,一个字段于下层多个字段有血缘,还会有表别名,字段别名的干扰。

这边可以考虑将每个 select 剥离出来存成一个对象,其中包括来源表(来源子 select 则为 null),select 字段,父 select 的 Index(第一层则为 null)。

在解析完成后所有 select 的对象存为一个数组,然后逐个对最外层的字段进行溯源找到真实的来源表。