如何用Calcite SqlParser解析Spark SQL并按需扩展语法?
1. 如何使用Calcite解析Spark SQL?
你尝试的代码:
Config config = SqlParser.config() // .withLex(Lex.MYSQL) // .withConformance(SqlConformanceEnum.BABEL) .withParserFactory(SqlDdlParserImpl.FACTORY); SqlParser sqlParser = SqlParser.create("create temporary table test1 as select * from test",config); SqlNode sqlNode = sqlParser.parseQuery();
执行后报错:
Caused by: org.apache.calcite.sql.parser.ddl.ParseException: Encountered "temporary" at line 1, column 8.
Was expecting one of:
"FOREIGN" ...
"OR" ...
"MATERIALIZED" ...
"SCHEMA" ...
"TABLE" ...
"TYPE" ...
"VIEW" ...
"FUNCTION" ...at org.apache.calcite.sql.parser.ddl.SqlDdlParserImpl.generateParseException(SqlDdlParserImpl.java:45760) at org.apache.calcite.sql.parser.ddl.SqlDdlParserImpl.jj_consume_token(SqlDdlParserImpl.java:45571) at org.apache.calcite.sql.parser.ddl.SqlDdlParserImpl.SqlCreate(SqlDdlParserImpl.java:22975) at org.apache.calcite.sql.parser.ddl.SqlDdlParserImpl.SqlStmt(SqlDdlParserImpl.java:3653) at org.apache.calcite.sql.parser.ddl.SqlDdlParserImpl.SqlStmtEof(SqlDdlParserImpl.java:4194) at org.apache.calcite.sql.parser.ddl.SqlDdlParserImpl.parseSqlStmtEof(SqlDdlParserImpl.java:213) at org.apache.calcite.sql.parser.SqlParser.parseQuery(SqlParser.java:172) ... 28 more
解决方案
Calcite没有官方原生的Spark SQL解析器,但可以通过以下方式适配:
- 优先使用
SqlConformanceEnum.BABEL配置,它兼容更多SQL方言特性,比默认ANSI规则更灵活。 - 自定义
Lex:基于现有Lex(如Lex.MYSQL)扩展,添加Spark SQL特有关键词,移除与Spark冲突的MySQL专属关键字。 - 复用社区扩展:部分大数据项目(如Flink、Hive相关的Calcite扩展)已经实现了Spark方言适配,可以直接参考或复用其解析器配置。
- 临时适配:如果仅需支持
CREATE TEMPORARY TABLE这类简单语法,可以先启用Lex.MYSQL+SqlConformanceEnum.BABEL组合,后续针对Spark特有语法再单独扩展。
2. 如何按需扩展语法(比如解析create temporary table)?
你尝试的步骤:
- 复制Calcite中的Parser.jj文件
- 在
parserImpls.ftl中添加SqlNode SqlCreateTempTable() :... - 创建继承自SqlCall的
CreateTempTable类,定义getOperator、getOperandList、unparse方法 - 在
config.fmpp中添加相关配置 - 执行
mvn generate-sources生成TestSqlParserImpl
执行步骤5时报错:
FMPP processing session failed.
[ERROR] Caused by: freemarker.core.InvalidReferenceException: The following has evaluated to null or missing:
[ERROR] ==> default [in template "Parser.jj" at line 1124, column 43]
[ERROR]
[ERROR] ----
[ERROR] Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
[ERROR] ----
[ERROR]
[ERROR] ----
[ERROR] FTL stack trace ("~" means nesting-related):
[ERROR] - Failed at: #if (parser.createStatementParserMeth... [in template "Parser.jj" at line 1124, column 1]
问题分析与正确步骤
你的步骤存在关键错误:直接复制生成后的Parser.jj文件是错误的,该文件是FMPP模板生成的静态文件,包含未替换的FreeMarker变量,导致模板解析时变量缺失。正确的扩展流程如下:
基于模板文件扩展,而非复制生成后的Parser.jj
Parser.jj是FMPP的输出产物,不能直接修改。应通过修改Calcite的模板文件(parserImpls.ftl、config.fmpp)来扩展语法。修改
parserImpls.ftl添加解析规则
在SqlCreate方法中添加TEMPORARY分支,示例:SqlNode SqlCreate() : { SqlNode node; } { <#if parser.createStatementParserMethods??> <#list parser.createStatementParserMethods as m> ( <m.name>() ) { node = $1; } </#list> </#if> | ( "CREATE" ( "TEMPORARY" )? "TABLE" <#-- 原有表创建的解析逻辑 --> ) { node = new CreateTempTable(...); } }完善
config.fmpp配置
确保在配置中声明新增的解析方法,同时补全所有模板变量的默认值:parser.createStatementParserMethods = [ {name: "SqlCreateTempTable"} ]实现对应的SqlCall与Operator
CreateTempTable类需正确关联SqlOperator,并实现getOperandList、unparse等方法,保证语法解析与输出的一致性。重新生成解析器
执行mvn generate-sources前,确认FMPP配置路径正确,所有模板变量均已定义,避免变量缺失问题。
错误原因
直接复制Parser.jj后,文件中的FreeMarker模板变量(如${default})未被FMPP替换,导致模板解析时无法找到变量值,从而抛出InvalidReferenceException。
内容的提问来源于stack exchange,提问作者user25300193

