文章目錄
- 前言
- 本章節源碼
- 一、基于 Calcite 實現一個自定義 SQL 解析器
- 1.1、認識Calcite解析器
- 二、實戰案例
- 2.1、快速構建一個可擴展sql語法的模板工程(當前暫無自定義擴展sql示例)
- 步驟1:拉取calcite源碼,復制codegen代碼
- 步驟2:配置pom插件實現JavaCC 編譯( FreeMarker 模版插件、javacc插件)
- 步驟3:執行命令生成SqlParserImpl自定義解析器類
- 步驟過程
- 插件生成源碼原理
- 實際使用生成出來的工廠類
- 額外說明maven-dependency-plugin插件
- 2.2、基于2.1工程擴展自定義SQL
- 參考學習案例(強推)
- 詳細步驟如下
- 步驟1:自定義SQL語法
- 步驟2:定義解析結果類SqlCreateFunction及SqlProperty
- 步驟3:語法模板 parserImpls.ftl
- 步驟4:配置配置模板 config.fmpp
- 步驟5:javacc編譯生成代碼
- 實際測試自定義語法
- 未完待續
- 擴展
- 參考文章
- 資料獲取

前言
博主介紹:?目前全網粉絲4W+,csdn博客專家、Java領域優質創作者,博客之星、阿里云平臺優質作者、專注于Java后端技術領域。
涵蓋技術內容:Java后端、大數據、算法、分布式微服務、中間件、前端、運維等。
博主所有博客文件目錄索引:博客目錄索引(持續更新)
CSDN搜索:長路
視頻平臺:b站-Coder長路
本章節源碼
當前文檔配套相關源碼地址:
- gitee:https://gitee.com/changluJava/demo-exer/tree/master/java-sqlparser/demo-calcite
- github:https://github.com/changluya/Java-Demos/tree/master/java-sqlparser/demo-calcite
一、基于 Calcite 實現一個自定義 SQL 解析器
可搭配學習:https://zhuanlan.zhihu.com/p/509681717
1.1、認識Calcite解析器
Calcite 默認使用 JavaCC 生成 SQL 解析器,可以很方便的將其替換為 Antlr 作為代碼生成器 。JavaCC 全稱 Java Compiler Compiler,是一個開源的 Java 程序解析器生成器,生成的語法分析器采用遞歸下降語法解析,簡稱 LL(K)。主要通過一些模版文件生成語法解析程序(例如根據 .jj 文件或者 .jjt 等文件生產代碼)。
Calcite 的解析體系是將 SQL 解析成抽象語法樹, Calcite 中使用 SqlNode 這種數據結構表示語法樹上的每個節點,例如 “select 1 + 1 = 2” 會將其拆分為多個 SqlNode。
SqlNode 有幾個重要的封裝子類,SqlLiteral、SqlIdentifier 和 SqlCall。 SqlLiteral:封裝常量,也叫字面量。SqlIdentifier:SQL 標識符,例如表名、字段名等。SqlCall:表示一種操作,SqlSelect、SqlAlter、SqlDDL 等都繼承 SqlCall。
二、實戰案例
2.1、快速構建一個可擴展sql語法的模板工程(當前暫無自定義擴展sql示例)
案例工程:demo1
📎demo1.zip
步驟1:拉取calcite源碼,復制codegen代碼
**拉取calcite源碼1.21.0源碼:**https://github.com/apache/calcite
📎calcite.zip(這里給出core、server模塊源碼,需要其他可去官網獲取)
將這 部分代碼拷貝到我們自己新建的工程:
步驟2:配置pom插件實現JavaCC 編譯( FreeMarker 模版插件、javacc插件)
以下配置均在pom.xml完成
定義caliate版本:
<properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><calcite.version>1.21.0</calcite.version>
</properties><build><plugins></plugins>
</build>
插件1:maven-resources-plugin 插件
說明:這個插件用于將指定的資源文件復制到構建目錄中。在這個例子中,它將src/main/codegen
目錄下的文件復制到${project.build.directory}/codegen
目錄。
<plugin><!-- 指定插件的artifactId,這里是maven-resources-plugin --><artifactId>maven-resources-plugin</artifactId><executions><!-- 定義插件的執行階段 --><execution><!-- 為這個執行階段設置一個唯一的id --><id>copy-fmpp-resources</id><!-- 指定這個執行應該在哪個Maven生命周期階段執行,這里是initialize階段 --><phase>initialize</phase><goals><!-- 指定要執行的目標 --><goal>copy-resources</goal></goals><configuration><!-- 配置插件的參數 --><outputDirectory>${project.build.directory}/codegen</outputDirectory><!-- 定義要復制的資源 --><resources><resource><!-- 指定資源的目錄 --><directory>src/main/codegen</directory><!-- 設置是否對資源文件進行過濾,這里設置為false --><filtering>false</filtering></resource></resources></configuration></execution></executions>
</plugin>
插件2:fmpp-maven-plugin 插件
說明:用于使用FreeMarker模板引擎生成源代碼。它依賴于FreeMarker庫,并且配置了模板和配置文件的位置,以及生成源代碼的輸出目錄。
<plugin><!-- 指定插件的groupId和artifactId,這里是fmpp-maven-plugin --><groupId>com.googlecode.fmpp-maven-plugin</groupId><artifactId>fmpp-maven-plugin</artifactId><version>1.0</version><dependencies><!-- 定義插件依賴 --><dependency><groupId>org.freemarker</groupId><artifactId>freemarker</artifactId><version>2.3.28</version></dependency></dependencies><executions><execution><id>generate-fmpp-sources</id><phase>generate-sources</phase><goals><goal>generate</goal></goals><configuration><!-- 配置FreeMarker的配置文件位置 --><cfgFile>${project.build.directory}/codegen/config.fmpp</cfgFile><!-- 指定生成的源代碼輸出目錄 --><outputDirectory>target/generated-sources</outputDirectory><!-- 指定模板文件的位置 --><templateDirectory>${project.build.directory}/codegen/templates</templateDirectory></configuration></execution></executions>
</plugin>
插件3:javacc-maven-plugin 插件
說明:用于使用JavaCC(Java Compiler Compiler)工具生成Java解析器。它配置了JavaCC源文件的位置、包含的文件模式、lookAhead參數、是否生成靜態代碼以及輸出目錄。
<plugin><!-- 注釋說明這個插件必須在fmpp-maven-plugin之后執行 --><!-- 指定插件的groupId和artifactId,這里是javacc-maven-plugin --><groupId>org.codehaus.mojo</groupId><artifactId>javacc-maven-plugin</artifactId><version>2.4</version><executions><execution><phase>generate-sources</phase><id>javacc</id><goals><goal>javacc</goal></goals><configuration><!-- 指定JavaCC源文件的目錄 --><sourceDirectory>${project.build.directory}/generated-sources/</sourceDirectory><!-- 指定包含的文件模式 --><includes><include>**/Parser.jj</include></includes><!-- 配置JavaCC的lookAhead參數,必須與Apache Calcite保持同步 --><lookAhead>1</lookAhead><!-- 設置是否生成靜態代碼,這里設置為false --><isStatic>false</isStatic><!-- 指定生成的JavaCC代碼的輸出目錄 --><outputDirectory>${project.build.directory}/generated-sources/</outputDirectory></configuration></execution></executions>
</plugin>
步驟3:執行命令生成SqlParserImpl自定義解析器類
步驟過程
在當前工程目錄下命令行執行命令:
mvn generate-sources
生成的內容如下:我們最終使用的就是其中的SqlParserImpl
插件生成源碼原理
我們主要使用的插件是兩個,一個是freemarker,另一個是javacc。
- freemarker:可以將我們指定提供的模板 + 自己傳入的動態值,生成我們想要的源碼或者模板文件。(當前場景是生成最終的parser.jj模板)
- javacc:根據freemarker替換得到最終的parser.jj文件后,對該xx.jj文件進行。
執行命令mvn generate-sources的中間過程:
可以這么理解,就是calcite官方給我們提供了一個模板文件以及附加配置文件及附加模板文件,我們通過使用這三個部分通過freemarker來將我們生成目標文件,這里也就是parser.jj,這個parser.jj文件
- 詳細細節可見這篇文章:Apache Calcite SQL解析及語法擴展 https://zhuanlan.zhihu.com/p/509681717
實際使用生成出來的工廠類
pom.xml中添加calcite核心包:
<dependency><groupId>org.apache.calcite</groupId><artifactId>calcite-core</artifactId><version>${calcite.version}</version>
</dependency>
接著此時我們在Main.java中寫一個main方法來看下:
package com.changlu;
import extend.impl.SqlParserImpl;
import org.apache.calcite.avatica.util.Casing;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.dialect.HiveSqlDialect;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.validate.SqlConformanceEnum;public class Main {public static void main(String[] args) throws SqlParseException {// 提供sql語句String sql = "select * from emps where id = 1";// 生成sql解析配置SqlParser.Config config = SqlParser.configBuilder()// 這里引用的類名為當前自定義擴展的.setParserFactory(SqlParserImpl.FACTORY).setUnquotedCasing(Casing.UNCHANGED).setQuotedCasing(Casing.UNCHANGED).setCaseSensitive(false).setConformance(SqlConformanceEnum.MYSQL_5).build();SqlParser sqlParser = SqlParser.create(sql, config);SqlNode sqlNode = sqlParser.parseQuery(sql);System.out.println("sqlNode:\n" + sqlNode);System.out.println();String transferSql = sqlNode.toSqlString(HiveSqlDialect.DEFAULT).getSql();System.out.println("轉換hivesql:\n" + transferSql);}
}
依舊正常能夠運行:
額外說明maven-dependency-plugin插件
關于部分工程中引入的maven-dependency-plugin插件:
<plugin><!-- Extract parser grammar template from calcite-core.jar and putit under ${project.build.directory} where all freemarker templates are. --><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><executions><execution><id>unpack-parser-template</id><phase>initialize</phase><goals><goal>unpack</goal></goals><configuration><artifactItems><artifactItem><groupId>org.apache.calcite</groupId><artifactId>calcite-core</artifactId><version>1.21.0</version><type>jar</type><overWrite>true</overWrite><outputDirectory>${project.build.directory}/</outputDirectory><includes>**/Parser.jj</includes></artifactItem></artifactItems></configuration></execution></executions>
</plugin>
該插件主要是將源碼calcite-core指定版本的Parser.jj復制到target目錄當中去,實際上如果我們做了步驟1的話,無需將該插件引入,如果說我們的工程里不想放入Parser.jj文件,只想要放置如下目錄:
那么就可以將該插件添加進去,執行mave命令的時候自然會將Parser.jj拷貝進來,相當于我們自己預先在工程里拷貝Parser.jj而已。
2.2、基于2.1工程擴展自定義SQL
案例工程:demo2
📎demo2.zip
參考學習案例(強推)
大量互聯網上參考的都是這個:
-
Apache Calcite教程 -目錄(博客):https://blog.csdn.net/QXC1281/article/details/89070285
-
github地址:https://github.com/quxiucheng/apache-calcite-tutorial/tree/a7d63273d0c7585fc65ad250c99a67a201bcb8b5
-
- Apache Calcite系列專欄(先鋒,字節跳動 大數據后臺開發):https://zhuanlan.zhihu.com/p/614668529 【這篇博文是跟著這個github倉庫學習的,可以搭配看】
代碼拉下來后看這個工程,里面帶上了README.md:
接下來學習該案例,下面的步驟會以該案例進行同步操作實踐。
詳細步驟如下
步驟1:自定義SQL語法
create function function_name as class_name
[method]
[with] [(key=value)]
實際舉例:
# 創建函數關鍵字
create function
# 函數名hr.custom_function
# as關鍵字
as
# 類名稱'com.github.quxiucheng.calcite.func.CustomFunction'
# 可選 方法名稱
method 'eval'
# 可選 備注信息
comment 'comment'
# 可選 附件變量
property ('a'='b','c'='1')
步驟2:定義解析結果類SqlCreateFunction及SqlProperty
- 對于org.apache.calcite.sql.parser.ddl包是之后給生成代碼放的。
SqlCreateFunction.java:解析結果類
package org.apache.calcite.sql.ddl;import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlWriter;
import org.apache.calcite.sql.parser.SqlParserPos;import java.util.List;public class SqlCreateFunction extends SqlCall {private SqlNode functionName;private String className;private SqlNodeList properties;private String methodName;private String comment;public SqlCreateFunction(SqlParserPos pos,SqlNode functionName, String className, String methodName, String comment,SqlNodeList properties) {super(pos);this.functionName = functionName;this.className = className;this.properties = properties;this.methodName = methodName;}public SqlNode getFunctionName() {return functionName;}public String getClassName() {return className;}public String getMethodName() {return methodName;}public SqlNodeList getProperties() {return properties;}public String getComment() {return comment;}@Overridepublic SqlOperator getOperator() {return null;}@Overridepublic List<SqlNode> getOperandList() {return null;}@Overridepublic SqlKind getKind() {return SqlKind.OTHER_DDL;}@Overridepublic void unparse(SqlWriter writer, int leftPrec, int rightPrec) {writer.keyword("CREATE");writer.keyword("FUNCTION");functionName.unparse(writer, leftPrec, rightPrec);writer.keyword("AS");writer.print("'" + className + "'");if (methodName != null) {writer.newlineAndIndent();writer.keyword("METHOD");writer.print("'" + methodName + "'");}if (properties != null) {writer.newlineAndIndent();writer.keyword("PROPERTY");SqlWriter.Frame propertyFrame = writer.startList("(", ")");for (SqlNode property : properties) {writer.sep(",", false);writer.newlineAndIndent();writer.print(" ");property.unparse(writer, leftPrec, rightPrec);}writer.newlineAndIndent();writer.endList(propertyFrame);}if (comment != null) {writer.newlineAndIndent();writer.keyword("COMMENT");writer.print("'" + comment + "'");}}
}
SqlProperty.java:解析key=value語句
package org.apache.calcite.sql.ddl;import com.google.common.collect.ImmutableList;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlSpecialOperator;
import org.apache.calcite.sql.SqlWriter;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.util.NlsString;import java.util.List;import static java.util.Objects.requireNonNull;public class SqlProperty extends SqlCall {/*** 定義特殊操作符*/protected static final SqlOperator OPERATOR =new SqlSpecialOperator("Property", SqlKind.OTHER);private SqlNode key;private SqlNode value;public SqlProperty(SqlParserPos pos, SqlNode key, SqlNode value) {super(pos);this.key = requireNonNull(key, "Property key is missing");this.value = requireNonNull(value, "Property value is missing");}@Overridepublic SqlOperator getOperator() {return OPERATOR;}@Overridepublic List<SqlNode> getOperandList() {return ImmutableList.of(key, value);}@Overridepublic SqlKind getKind() {return SqlKind.OTHER;}@Overridepublic void unparse(SqlWriter writer, int leftPrec, int rightPrec) {key.unparse(writer, leftPrec, rightPrec);writer.keyword("=");value.unparse(writer, leftPrec, rightPrec);}public SqlNode getKey() {return key;}public SqlNode getValue() {return value;}public String getKeyString() {return key.toString();}public String getValueString() {return ((NlsString) SqlLiteral.value(value)).getValue();}}
步驟3:語法模板 parserImpls.ftl
在codegen/includes/parserImpls.ftl中添加如下配置:
- 這里會使用到SqlCreateFunction、SqlProperty類。
- 這里大量使用到了javacc的語法,例如其中的關鍵字、if判斷、java代碼等。
// 創建函數SqlNode SqlCreateFunction() :{// 聲明變量SqlParserPos createPos;SqlParserPos propertyPos;SqlNode functionName = null;String className = null;String methodName = null;String comment = null;SqlNodeList properties = null;}{// create 關鍵字<CREATE>{// 獲取當前token的行列位置createPos = getPos();}// function 關鍵字<FUNCTION>// 函數名functionName = CompoundIdentifier()// as關鍵字<AS>// 類名{ className = StringLiteralValue(); }// if語句[// method關鍵字<METHOD>{// 方法名稱methodName = StringLiteralValue();}]// if[// property 關鍵字,設置初始化變量<PROPERTY>{// 獲取關鍵字位置propertyPos = getPos();SqlNode property;properties = new SqlNodeList(propertyPos);}<LPAREN>[property = PropertyValue(){properties.add(property);}(<COMMA>{property = PropertyValue();properties.add(property);})*]<RPAREN>]// if[<COMMENT> {// 備注comment = StringLiteralValue();}]{return new SqlCreateFunction(createPos, functionName, className, methodName, comment, properties);}}JAVACODE String StringLiteralValue() {SqlNode sqlNode = StringLiteral();return ((NlsString) SqlLiteral.value(sqlNode)).getValue();}/*** 解析SQL中的key=value形式的屬性值*/SqlNode PropertyValue() :{SqlNode key;SqlNode value;SqlParserPos pos;}{key = StringLiteral(){ pos = getPos(); }<EQ> value = StringLiteral(){return new SqlProperty(getPos(), key, value);}}
步驟4:配置配置模板 config.fmpp
。
定義package、class 和 imports:
- 這里package就是最終生成的輸出目錄,class為最終生成的實現類名稱,imports表示的是后續自定義class類中文件頂部會import引入的代碼位置
package: "org.apache.calcite.sql.parser.ddl"class: "CustomSqlParserImpl",imports: ["org.apache.calcite.sql.ddl.SqlCreateFunction","org.apache.calcite.sql.ddl.SqlProperty"]
定義關鍵字keywords:
keywords: ["PARAMS""COMMENT""PROPERTY" ]
定義自定義解析 statementParserMethods:
statementParserMethods: ["SqlCreateFunction()"]
步驟5:javacc編譯生成代碼
在當前工程目錄下執行命令進行編譯生成:
mvn generate-sources
將生成的代碼添加到之前的parser.ddl目錄:
此時大功告成,準備測試:
實際測試自定義語法
使用calicte原生的sql解析器工廠SqlParserImpl.FACTORY
package com.changlu.parser;
import org.apache.calcite.config.Lex;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.dialect.OracleSqlDialect;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.parser.impl.SqlParserImpl;public class SqlCreateFunctionMain {public static void main(String[] args) throws SqlParseException {// 解析配置 - mysql設置SqlParser.Config mysqlConfig = SqlParser.configBuilder()// 定義解析工廠.setParserFactory(SqlParserImpl.FACTORY).setLex(Lex.MYSQL).build();// 創建解析器SqlParser parser = SqlParser.create("", mysqlConfig);// Sql語句String sql = "create function " +"hr.custom_function as 'com.github.quxiucheng.calcite.func.CustomFunction' " +"method 'eval' " +"property ('a'='b','c'='1') ";// 解析sqlSqlNode sqlNode = parser.parseQuery(sql);// 還原某個方言的SQLSystem.out.println(sqlNode.toSqlString(OracleSqlDialect.DEFAULT));}
}
使用自定義解析工廠類測試
package com.changlu.parser;import org.apache.calcite.config.Lex;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.dialect.OracleSqlDialect;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.parser.ddl.CustomSqlParserImpl;public class SqlCreateFunctionMain {public static void main(String[] args) throws SqlParseException {// 解析配置 - mysql設置SqlParser.Config mysqlConfig = SqlParser.configBuilder()// 定義解析工廠.setParserFactory(CustomSqlParserImpl.FACTORY).setLex(Lex.MYSQL).build();// 創建解析器SqlParser parser = SqlParser.create("", mysqlConfig);// Sql語句String sql = "create function " +"hr.custom_function as 'com.github.quxiucheng.calcite.func.CustomFunction' " +"method 'eval' " +"property ('a'='b','c'='1') ";// 解析sqlSqlNode sqlNode = parser.parseQuery(sql);// 還原某個方言的SQLSystem.out.println(sqlNode.toSqlString(OracleSqlDialect.DEFAULT));}
}
成功解析:
未完待續
到了這里,我感覺想要后續實現一些自定義擴展語法有兩個難點:一個就是能夠熟悉javacc語法,另一個就是熟悉Calcite去進行解析構建AstNode樹的過程,因為支持部分自定義語法則需要去繼承實現諸如下面一些Sqlxxx(這個是calcite提供的實現):
擴展
其他sqlparser解析器有:Antlr 4
SQL Parser的方式有很多種,JAVA語言中,主要有兩個框架,一個是JavaCC,一個是Antlr4。比如像Apache Calcite就是用的JavaCC解析的SQL。而用Apache Calcite框架的,那是相當之多,如下:
參考文章
[1]. Calcite SQL 解析、語法擴展、元數據驗證原理與實戰(上):https://www.modb.pro/db/607373
[2]. Apache Calcite SQL解析及語法擴展:https://zhuanlan.zhihu.com/p/509681717
資料獲取
大家點贊、收藏、關注、評論啦~
精彩專欄推薦訂閱:在下方專欄👇🏻
- 長路-文章目錄匯總(算法、后端Java、前端、運維技術導航):博主所有博客導航索引匯總
- 開源項目Studio-Vue—校園工作室管理系統(含前后臺,SpringBoot+Vue):博主個人獨立項目,包含詳細部署上線視頻,已開源
- 學習與生活-專欄:可以了解博主的學習歷程
- 算法專欄:算法收錄
更多博客與資料可查看👇🏻獲取聯系方式👇🏻,🍅文末獲取開發資源及更多資源博客獲取🍅