StarRocks 添加SQL 语法

Posted by danner on May 5, 2023

从 SQL 文本到分布式物理执行计划, 在 StarRocks 中,需要经过以下 5 个步骤:

1、SQL Parse: 将 SQL 文本转换成一个 AST(抽象语法树)

2、SQL Analyze:基于 AST 进行语法和语义分析 => 本文

3、SQL Logical Plan: 将 AST 转换成逻辑计划

4、SQL Optimize:基于关系代数、统计信息、Cost 模型,对逻辑计划进行重写、转换,选择出 Cost “最低” 的物理执行计

5、生成 Plan Fragment:将 Optimizer 选择的物理执行计划转换为 BE 可以直接执行的 Plan Fragment

之前 StarRocks 学习:AST2LogicalPLanStarRocks SQL Binder 两篇文章大致分析步骤二和三。本文以添加新的窗口聚合函数 NTH_VALUE 方式来熟悉 StarRocks Parse,主要逻辑在StarRocksParserAstBuilder

Antlr4

本文不重点分析 antlr 语法,只稍微介绍。

语法分析器(parser)是用来识别语言的程序,本身包含两个部分:词法分析器(lexer)和语法分析器(parser)。词法分析阶段主要解决的关键词以及各种标识符,例如 INT、ID 等,语法分析主要是基于词法分析的结果,构造一颗语法分析树。

在 StarRocks 中有两个文件:StarRocks.g4、StarRocksLex.g4

  • StarRocksLex.g4:可以简单理解为是宏定义 - 词法,在StarRocks.g4 会用到
    • INTEGER_VALUE:代表数字
    • SET:代表字符串 ‘set’
  • StarRocks.g4:定义具体的SQL语法
functionCall
    : EXTRACT '(' identifier FROM valueExpression ')'                                     #extract
    ...
    | windowFunction over                                                                 #windowFunctionCall
windowFunction
    : name = ROW_NUMBER '(' ')'
    | name = RANK '(' ')'
    | name = DENSE_RANK '(' ')'
    | name = NTILE  '(' expression? ')'
    | name = LEAD  '(' (expression ignoreNulls? (',' expression)*)? ')' ignoreNulls?
    | name = LAG '(' (expression ignoreNulls? (',' expression)*)? ')' ignoreNulls?
    | name = FIRST_VALUE '(' (expression ignoreNulls? (',' expression)*)? ')' ignoreNulls?
    | name = LAST_VALUE '(' (expression ignoreNulls? (',' expression)*)? ')' ignoreNulls?
    
ignoreNulls
    : IGNORE NULLS
    ;

定义完语法,那么窗口函数怎么写呢,以row_number 举例

ROW_NUMBER() over()

StarRocks.g4、StarRocksLex.g4 通过antlr4-maven-plugin 插件会生成对应的 Listener、Visitor、Parser。其中最重要的是Parser,StarRocks 会生成 StarRocksParser

StarRocksParser

StarRocksParser 会将输入的 SQL 语句解析成语法分析树,本质是包含各种类型的 Context。Astbuilder 将 Context 转化成不同的Node(这个我们就熟悉了)。

以上面的 windowFunction 为例,分析下StarRocks.g4StarRocksParser 有怎样的关系。

// com.starrocks.sql.parser.StarRocksParser.WindowFunctionContext
public static class WindowFunctionContext extends ParserRuleContext {
	public Token name;
	public TerminalNode ROW_NUMBER() { return getToken(StarRocksParser.ROW_NUMBER, 0); }
	public TerminalNode RANK() { return getToken(StarRocksParser.RANK, 0); }
	public TerminalNode DENSE_RANK() { return getToken(StarRocksParser.DENSE_RANK, 0); }
	public TerminalNode NTILE() { return getToken(StarRocksParser.NTILE, 0); }
	public List<ExpressionContext> expression() {
		return getRuleContexts(ExpressionContext.class);
	}
	public ExpressionContext expression(int i) {
		return getRuleContext(ExpressionContext.class,i);
	}
	public TerminalNode LEAD() { return getToken(StarRocksParser.LEAD, 0); }
	public List<IgnoreNullsContext> ignoreNulls() {
		return getRuleContexts(IgnoreNullsContext.class);
	}
	public IgnoreNullsContext ignoreNulls(int i) {
		return getRuleContext(IgnoreNullsContext.class,i);
	}
	public TerminalNode LAG() { return getToken(StarRocksParser.LAG, 0); }
	public TerminalNode FIRST_VALUE() { return getToken(StarRocksParser.FIRST_VALUE, 0); }
	public TerminalNode LAST_VALUE() { return getToken(StarRocksParser.LAST_VALUE, 0); }
	public TerminalNode INTEGER_VALUE() { return getToken(StarRocksParser.INTEGER_VALUE, 0); }
	public TerminalNode FROM() { return getToken(StarRocksParser.FROM, 0); }
	public TerminalNode FIRST() { return getToken(StarRocksParser.FIRST, 0); }
	public TerminalNode LAST() { return getToken(StarRocksParser.LAST, 0); }
	...
  • WindowFunctionContext: StarRocks.g4 中类似windowFunction ,会自动生成 Context;同理可以找到FunctionCallContextIgnoreNullsContext
  • = : 等号会生成Tokenname = => public Token name;
  • 词法:StarRocksLex.g4 定义中出现的词法会变成 TerminalNode(包含Token)

了解这么多,已足够去添加一个新的语法。

AstBuilder

同样是 windowFunction 为例,如何将Context 转换成Node

// com.starrocks.sql.parser.AstBuilder#visitWindowFunctionCall
public ParseNode visitWindowFunctionCall(StarRocksParser.WindowFunctionCallContext context) {
		FunctionCallExpr functionCallExpr = (FunctionCallExpr) visit(context.windowFunction());
    // 构建 over 语句 node,本文不分析
		return buildOverClause(functionCallExpr, context.over(), createPos(context));
}

@Override
public ParseNode visitWindowFunction(StarRocksParser.WindowFunctionContext context) {
		FunctionCallExpr functionCallExpr = new FunctionCallExpr(context.name.getText().toLowerCase(),
						new FunctionParams(false, visit(context.expression(), Expr.class)), createPos(context));
		boolean ignoreNull = CollectionUtils.isNotEmpty(context.ignoreNulls())
						&& context.ignoreNulls().stream().anyMatch(Objects::nonNull);

		functionCallExpr.setIgnoreNulls(ignoreNull);
		return functionCallExpr;
}

重点是visitWindowFunction 函数,构造一个 FunctionCallExpr,设置相应的属性。

  • fnName 属性:Token context.name 获取字符串,得到函数名
  • ignoreNulls 属性:TerminalNode context.ignoreNulls() 是否为空
    • 若SQL 语句包含 IGNORE NULLS ,那么此TerminalNode 不为空,且会包含两个 Token,分别是 star=’IGNORE’ 和stop=’NULLS’。
    • 如果SQL 语句存在 IGNORE NIL,会发生什么?StarRocksParser.parse 中就会报错,因为未定义IGNORE NIL语法

再找个例子,判断Token 是否相等,后续添加语法会用到。

sortItem

StarRocks.g4

sortItem
    : expression ordering = (ASC | DESC)? (NULLS nullOrdering=(FIRST | LAST))?
    ;

StarRocksParser

public static class SortItemContext extends ParserRuleContext {
	public Token ordering;
	public Token nullOrdering;
	public ExpressionContext expression() {
		return getRuleContext(ExpressionContext.class,0);
	}
	public TerminalNode NULLS() { return getToken(StarRocksParser.NULLS, 0); }
	public TerminalNode ASC() { return getToken(StarRocksParser.ASC, 0); }
	public TerminalNode DESC() { return getToken(StarRocksParser.DESC, 0); }
	public TerminalNode FIRST() { return getToken(StarRocksParser.FIRST, 0); }
	public TerminalNode LAST() { return getToken(StarRocksParser.LAST, 0); }
	public SortItemContext(ParserRuleContext parent, int invokingState) {
		super(parent, invokingState);
	}
	...

AstBuilder

public ParseNode visitSortItem(StarRocksParser.SortItemContext context) {
		return new OrderByElement(
						(Expr) visit(context.expression()),
						getOrderingType(context.ordering),
						getNullOrderingType(getOrderingType(context.ordering), context.nullOrdering),
						createPos(context));
}
private static boolean getOrderingType(Token token) {
		if (token == null) {
				return true;
		}

		return token.getType() == StarRocksLexer.ASC;
}

注意ordering 在三个文件里的作用

  • StarRocks.g4 定义了 ordering 不是ASC ,就是DESC
  • StarRocksParser 定义一个Token 名为 ordering
  • AstBuilder 展示如何使用 ordering => 如何获取到StarRocks.g4 中定义的一个变量取值

NTH_VALUE

NTH_VALUE ( <value expression>, <nth row>)
      [ FROM {FIRST | LAST} ] [{ IGNORE | RESPECT } NULLS ]
      OVER ([PARTITION BY] [ORDER BY])
# 默认是FIRST  RESPECT NULLS

OVER 之后的语法可以不分析,StarRocks.g4 已实现

参考已有的windowFunction 语法,不难得到 NTH_VALUE 语法

windowFunction
    : name = ROW_NUMBER '(' ')'
    | name = NTH_VALUE '(' (expression ',' expression) ')' (FROM direction =(FIRST | LAST))? (ignoreNulls | respectNulls)?
    ;

ignoreNulls
    : IGNORE NULLS
    ;
respectNulls
    : RESPECT NULLS
    ;

编译后自动 StarRocksParser 文件,我们需要做的是如何在AstBuilderNTH_VALUE 参数信息获取到。

有了上面的知识,依葫芦画瓢。

// com.starrocks.sql.parser.AstBuilder#visitWindowFunction
public ParseNode visitWindowFunction(StarRocksParser.WindowFunctionContext context) {
		FunctionCallExpr functionCallExpr = new FunctionCallExpr(context.name.getText().toLowerCase(),
						new FunctionParams(false, visit(context.expression(), Expr.class)), createPos(context));
		boolean ignoreNull = CollectionUtils.isNotEmpty(context.ignoreNulls())
						&& context.ignoreNulls().stream().anyMatch(Objects::nonNull);

		functionCallExpr.setIgnoreNulls(ignoreNull);
		functionCallExpr.setAsc(getDirectionType(context.direction));
		return functionCallExpr;
}
private static boolean getDirectionType(Token token) {
		if (token == null) {
				return true;
		}

		return token.getType() == StarRocksLexer.FIRST;
}

// com.starrocks.analysis.Expr
private boolean ignoreNulls = false;

// com.starrocks.analysis.FunctionCallExpr
private  boolean isAsc = true;

分析下面SQL 如何取参数

SELECT NTH_VALUE(tc, 1) from last IGNORE NULLS over(partition by ta order by tg) FROM tall
  • expression:设置到 FunctionParams,本例包含两个 Expr
    • SlotRef:tc 列
    • IntLiteral: int 常量 1
  • direction:获取Token context.direction ,比较是否为 First/Last
  • ignoreNulls respectNulls:获取 IgnoreNullsContext context.ignoreNulls() 判断;已有代码,逻辑一致,没有修改

参考资料

StarRocks Parser 源码解析