從簡單SQL轉換理解Antlr4訪問者模式技術細節(jié)
A Mini Task
從一個小任務開始。
給定簡單CREATE TABLE的語法規(guī)則CreateLexer.g4和CreateParser.g4,采用Visitor模式實現(xiàn)下述語法的自動轉換輸出. 語法轉換規(guī)則:實現(xiàn)CREATE TABLE語法datetime類型向TIMESTAMP WITHOUT TIME ZONE類型的轉換. 示例1: 輸入:CREATE TABLE t1 (id integer, order_date datetime); 輸出:CREATE TABLE t1 (id integer, order_date TIMESTAMP WITHOUT TIME ZONE);
默認選擇IDEA開發(fā)環(huán)境,且所涉及插件和配置均已就緒。
語法規(guī)則解讀
一份語法由一個語法聲明和緊隨其后的若干條規(guī)則構成,具有如下通用形式:
/* 可選的Javadoc風格的注釋 */ grammer Name; options {...} import ...; // 導入其他語法規(guī)則,與當前語法規(guī)則合并. tokens {<
grammer關鍵字聲明語法名稱,與文件名一致。
復雜語法可以將詞法規(guī)則和文法規(guī)則拆分成兩個不同的文件,兩個規(guī)則文件滿足如下特征:
/* 詞法規(guī)則文件頭聲明 */ lexer grammer Name; /* 文法規(guī)則文件頭聲明. 需要額外聲明對應token流詞典對應的詞法規(guī)則. */ parser grammer Name; options { tokenVocab = Name; }
每個文法規(guī)則的產(chǎn)生式后面可以通過 #label給產(chǎn)生式分支加標簽命名,當備選產(chǎn)生分支較多時,標簽非常好用.
語法規(guī)則詳細參考文檔,請參考:
官方手冊<
Antlr官方提供的常用語法文件
本任務中,CREATE TABLE語法的核心規(guī)則文法,如下,
stat : createTable ; createTable : CREATE TABLE tableName createDefinitions ; createDefinitions : LR_BRACKET createDefinition (COMMA createDefinition)* RR_BRACKET ; createDefinition : uid columnDefinition ; columnDefinition : dataType ; dataType : typeName=( CHAR | VARCHAR | TINYTEXT | TEXT ) lengthOneDimension? BINARY? #stringDataType | typeName=( TINYINT | SMALLINT | MEDIUMINT | INT | INTEGER | BIGINT ) lengthOneDimension? UNSIGNED? #lengthOneDimensionDataType [...] | typeName=( BIT | TIME | TIMESTAMP | DATETIME | BINARY | VARBINARY | YEAR ) lengthOneDimension? #lengthOneDimensionDataType ; lengthOneDimension : LR_BRACKET decimalLiteral RR_BRACKET ; tableName : uid ; uid : ID;
語法解析樹可視化
文法規(guī)則文件stat規(guī)則處,右鍵選擇"Test rule stat",打開ANTLR Preview調試界面,輸出示例建表語句,如下圖。
右側自動生成對應的Parse Tree,可以直觀顯示解析結構,其中,葉子節(jié)點為詞法終結符,通常為SQL關鍵字或者變量標識符;非葉子節(jié)點為文法非終結符,通常為文法規(guī)則.
對應的層次結構,如下圖.
如果Parse Tree有飄紅,可能某個詞法或文法規(guī)則有問題,需要進一步定位修正.
詞法和文法規(guī)則.g4文件,配置只生成visitor模式接口類,一鍵式自動化生成所有基類文件.
Visitor模式主調流程
以賦值表達式 sp=100;為例,下圖(來源于官方手冊文檔)展示Antlr語法分析器根據(jù)字符串自動生成語法解析樹的過程。Parse Tree的葉子節(jié)點是輸入的詞法符號,比如,關鍵字,變量標識符等等。句子,即是符號的線性組合,本質上是語法分析樹在人腦中的串行化。
第1小節(jié)的Task,對應生成語法分析樹的基本流程代碼,如下。
// Step1. 新建CharStream,從標準輸入讀取數(shù)據(jù) CharStream input = CharStreams.fromString("CREATE TABLE t1 (id integer, order_date datetime);"); // Step2. 新建詞法分析器,處理輸入的charStream[!] CreateLexer lexer = new CreateLexer(input); // Step3. 新建詞法符號的緩沖區(qū),用于存儲詞法分析器將生成的詞法符號 CommonTokenStream tokens = new CommonTokenStream(lexer); // Step4. 新建語法分析器,處理詞法符號緩沖區(qū)的內容[!] CreateParser parser = new CreateParser(tokens); // Step5. 針對stat規(guī)則,開始語法分析. ParseTree tree = parser.stat(); System.out.println(); // 此處tree已經(jīng)記錄語法解析樹的基本信息,可以采用LISP風格打印生成的樹. System.out.println(tree.toStringTree(parser));
本任務語法解析樹進行深度優(yōu)先遍歷的實際訪問路徑如下圖所示。
實現(xiàn)中,通過將解析樹根節(jié)點tree傳入visit接口,即可達到遍歷語法解析樹的目的。當然,要真正達到應用項目特殊的轉換目的,需要重寫實現(xiàn)訪問路徑所到之處node類型對應的visit()接口。
// 遍歷語法分析樹,觸發(fā)visit函數(shù)的回調. CreateTableVisitorImpl visitor = new CreateTableVisitorImpl(); System.out.println(visitor.visit(tree));
Visitor模式技術細節(jié)
針對第4小節(jié)中提到的訪問者模式,本節(jié)將展開詳細的介紹。
訪問者模式是面向對象設計模式中一種經(jīng)典的行為模式。將更新封裝到一個類中(訪問操作),并由待更改類提供一個接收接口,從而實現(xiàn)需求變更和擴展。
Visitor模式的關鍵是雙分派(Double-Dispatch)的技術。其中,visit/accept操作是一個雙分派操作,意味著執(zhí)行操作取決于請求的種類和接收者的類型。通常,使用訪問者模式,涉及一個對象層次結構,其中,所有節(jié)點都是從根節(jié)點基類類型派生出來的。每個基類都有一個accept函數(shù)接收訪客,接下來,每個派生類重載此函數(shù),使得訪客可以訪問自己。
在對某個派生類對象Obj執(zhí)行visitor動作時,使用obj.accept(visitor)即可。Visitor模式的優(yōu)勢在于結構和行為分離。如果需要增加新行為,只需要增加一個新的Visitor,并實現(xiàn)所有類對應的行為即可。
Antlr語法解析器,正是借鑒訪問者模式,自動生成visitor模式的一系列抽象類和接口函數(shù)。類及類間關系如下圖所示。
CreateParserBaseVisitor 類繼承抽象類 AbstractParseTreeVisitor ,是接口類 CreateParserVisitor 的具體實現(xiàn)。如下代碼片段,根節(jié)點類型stat及其每個派生類類型均有對應的visit方法。
public class CreateParserBaseVisitor
前一小節(jié)visitor遍歷實現(xiàn)中,參數(shù)傳的是語法解析樹的根節(jié)點tree,visitor.visit(tree)調用的是抽象類接口
public abstract class AbstractParseTreeVisitor
根據(jù)根節(jié)點上下文類StatContext,調用其accept接口,并根據(jù)是否CreateParserVisitor類實例,調用不同的visit接口。
public static class StatContext extends ParserRuleContext { @Override public
至此,實現(xiàn)方法很顯而易見。開發(fā)只需要基于抽象類 CreateParserBaseVisitor實現(xiàn)即可(如類圖中紅色框出部分 CreateParserBaseVisitor ),并按需重寫抽象類生成的抽象接口。從訪問路線看,需要依次重寫實現(xiàn)遍歷Parse Tree的過程所到之處的node對應的visit函數(shù),即可達到轉換的目的。
派生類重載實現(xiàn)visitor函數(shù)
在遍歷語法解析樹過程中,需要對轉換涉及的節(jié)點進行改寫。此次Task需要對建表語句數(shù)據(jù)類型作轉換,將datetime類型轉成TIMESTAMP WITHOUT TIME ZONE類型,datetime掛在lengthOneDimensionDataType標簽的dataType分支中,所以,只要在關鍵函數(shù)visitLengthOneDimensionDataType的重寫中,實現(xiàn)datetime類型的改寫輸出即可。
// 對datetime類型自動轉換處理. if (ctx.typeName.getType() == CreateParser.DATETIME) { return String.format("TIMESTAMP WITHOUT TIME ZONE"); } else { return String.format("%s", ctx.typeName.getText()); }
總結
基于Antlr的開發(fā)應用,使用流程基本理解的基礎上,知道需要繼承哪個Visitor抽象類去實現(xiàn)以及怎么從各個派生類的Context去讀取節(jié)點類型的更多信息,即可掌握語言識別程序的開發(fā)密碼。
重難點主要在于如何從復雜的語法解析樹中快速解析和準確構造新的字符串。
參考
[1]. The Definitive ANTLR 4 Reference
[2]. https://github.com/antlr
[3]. Antlr官方提供的.g4語法文件
SQL 數(shù)據(jù)倉庫服務 GaussDB(DWS)
版權聲明:本文內容由網(wǎng)絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內刪除侵權內容。