基于ANTLR4的大數(shù)據(jù)SQL編輯器解析引擎實踐
一、背景
二、ANTLR4 簡介
1. ANTLR4 特性
2. ANTLR4 的應(yīng)用場景
3. ANTLR4入門
三、SparkSQL介紹
四、技術(shù)實現(xiàn)
1. 語法設(shè)計
2. 語法補(bǔ)全
3. 語法校驗
4. 性能
5.編輯器應(yīng)用
五、大模型下的SQL編輯器應(yīng)用
1. NL2SQL應(yīng)用場景
2. NL2SQL自動補(bǔ)全
六、總結(jié)
一、背景
隨著得物離線業(yè)務(wù)的快速增長,為了脫離全托管服務(wù)的一些限制和享受技術(shù)發(fā)展帶來的成本優(yōu)化,公司提出了大數(shù)據(jù)Galaxy開源演進(jìn)項目,將離線業(yè)務(wù)從全托管且封閉的環(huán)境遷移到一個開源且自主可控的生態(tài)系統(tǒng)中,而離線開發(fā)治理套件是Galaxy自研體系中一個核心的項目,在數(shù)據(jù)開發(fā)IDE中最核心的就是SQL編輯器,我們需要一個SQL解析引擎在SQL編輯提供適配得物自研Spark引擎的語法定義,實時語法解析,語法補(bǔ)全,語法校驗等能力,結(jié)合業(yè)內(nèi)dataworks和dataphin的實踐,我們最終選用ANTLR作為SQL解析引擎底座。
二、ANTLR4 簡介
ANTLR(一種語法解析引擎工具)是一個功能強(qiáng)大的解析器生成器,用于讀取、處理、執(zhí)行或翻譯結(jié)構(gòu)化文本或二進(jìn)制文件。它廣泛用于構(gòu)建語言、工具和框架。ANTLR可以根據(jù)語法規(guī)則文件生成一個可以構(gòu)建和遍歷解析樹的解析器。
ANTLR4 特性
ANTLR4 是一個強(qiáng)大的工具,適合用于語言處理、編譯器構(gòu)建、代碼分析等多種場景。它的易用性、靈活性和強(qiáng)大的特性使得它成為開發(fā)者的熱門選擇。
- 強(qiáng)大的文法定義:ANTLR4 允許用戶使用簡單且易讀的文法語法來定義語言的結(jié)構(gòu)。這使得創(chuàng)建和維護(hù)語言解析器變得更加直觀,同時在復(fù)雜文法構(gòu)造上支持左遞歸文法、嵌套結(jié)構(gòu)以及其他復(fù)雜的文法構(gòu)造,使得能夠解析更復(fù)雜的語言結(jié)構(gòu)。
- 抽象語法樹遍歷:ANTLR4 可以生成抽象語法樹,使得在解析源代碼時能夠更容易地進(jìn)行分析和變換。AST 是編譯器和解釋器的核心組件。同時提供了簡單的 API 來遍歷生成的語法樹,使得實現(xiàn)代碼分析、轉(zhuǎn)換等操作變得簡單
- 自動語法錯誤處理:ANTLR4 提供了內(nèi)置的錯誤處理機(jī)制,可以在解析過程中自動處理語法錯誤,并且可以自定義錯誤消息和處理邏輯
- 可擴(kuò)展性:ANTLR4 允許用戶擴(kuò)展和自定義生成的解析器的行為。例如,您可以自定義解析器的方法、錯誤處理以及其他功能。
- 工具&社區(qū)生態(tài):ANTLR4 提供了豐富的工具支持,包括命令行工具、集成開發(fā)環(huán)境插件和可視化工具,可以幫助您更輕松地開發(fā)和調(diào)試解析器。同時擁有活躍的社區(qū),提供了大量的文檔、示例和支持。這使得新用戶能夠快速上手,并得到必要的幫助。
ANTLR4 的應(yīng)用場景
Apache Spark: 流行的大數(shù)據(jù)處理框架,使用ANTLR作為其SQL解析器的一部分,支持SQL查詢。
Twitter: Twitter 使用ANTLR來解析和分析用戶的查詢語言,這有助于他們的搜索和分析功能。
IBM: IBM使用ANTLR來支持一些其產(chǎn)品和工具中的DSL(領(lǐng)域特定語言)解析需求,例如,在其企業(yè)集成解決方案中。
ANTLR4入門
ANTLR元語言
為了實現(xiàn)一門計算機(jī)編程語言,我們需要構(gòu)建一個程序來讀取輸入語句,對其中的詞組和符號進(jìn)行識別處理,即我們需要語法解釋器或者翻譯器來識別出一門特定語言的所有詞組,子詞組,語句。我們將語法分析過程拆分為兩個獨立的階段則為詞法分析和語法分析。
圖片
ANTLR語法遵循了一種專門用來描述其他語言的語法,我們稱之為ANTLR元語言(ANTLR’s meta-language)。ANTLR元語句是一個強(qiáng)大的工具,可以用來定義編程語言的語法。通過定義詞法和語法規(guī)則,可以基于antlr生成解析器和詞法分析器。
1.自頂向下
在語言結(jié)構(gòu)中,整體的辨識都是從最粗的粒度開始,一直進(jìn)行到最詳細(xì)的層次,并把它們編寫成為語法規(guī)則,ANTLR4就是采用自頂向下的,詞法語法分離,上下文無關(guān)的語法框架來描述語言。
// MyGLexer.g4
lexer grammar MyGLexer;
SEMICOLON: ';';
LEFT_PAREN: '(';
RIGHT_PAREN: ')';
COMMA: ',';
DOT: '.';
LEFT_BRACKET: '[';
RIGHT_BRACKET: ']';
LEFT_BRACES: '{';
RIGHT_RACES: '}';
EQ: '=';
FUNCTOM: 'FUNCTION';
LET: 'LET';
CONST: 'CONST';
VAR: 'VAR';
IF: 'IF';
ELSE: 'ELSE';
WHILE: 'WHILE';
FOR: 'FOR';
RETURN: 'RETURN';
// MyGParser.g4
parser grammar MyGParser;
options {
tokenVocab = MyGLexer;
}
// 入口規(guī)則
program: statement* EOF;
statement:
variableDeclaration
| functionDeclaration
| expressionStatement
| blockStatement
| ifStatement
| whileStatement
| forStatement
| returnStatement;
......
2.語言模式
計算機(jī)語言常見4種語言模式:序列(sequence)、選擇(choice)、詞法符號依賴 (token dependency),以及嵌套結(jié)構(gòu)(nested phrase)。以下是ANTLR對4種模式的語法規(guī)則描述。
圖片
3.語法歧義
在自頂向下的語法和手工編寫的遞歸下降語法分析器中,處理表達(dá)式都是一件相當(dāng)棘手的事情,這首先是因為大多數(shù)語法都存在歧義,其次是因為大多數(shù)語言的規(guī)范使用了一種特殊的遞歸方式,稱為左遞歸。
expr : expr '*' expr
| expr '+' expr
| INT
;
我們舉個運(yùn)算符優(yōu)先級帶來的語法歧義問題,同樣的規(guī)則可以匹配多個輸入字符流。
圖片
在其他語法工具中,通常通過指定額外的標(biāo)記來指定運(yùn)算符優(yōu)先級。而在ANTLR4中通過備選分支的排序來指定優(yōu)先級,越靠前優(yōu)先級越高。
代碼自動生成
ANTLR可以根據(jù)lexer.g4和parser.g4自動生成詞法分析器,語法分析器,監(jiān)聽器,訪問器等。
antlr4ng -Dlanguage=TypeScript -visitor -listener -Xexact-output-dir -o ./src/lib ./src/grammar/*.g
圖片
語法解析與業(yè)務(wù)邏輯解耦
在ANTLR4中語法解析和業(yè)務(wù)邏輯的高度解耦是一個重要的設(shè)計理念,優(yōu)點就是同一個 AST 結(jié)構(gòu)能夠在不同的業(yè)務(wù)邏輯實現(xiàn)之間實現(xiàn)復(fù)用。不同的業(yè)務(wù)邏輯(如執(zhí)行、轉(zhuǎn)換、優(yōu)化等)可以對同一個 AST 進(jìn)行不同的處理,而不需要關(guān)心解析過程。核心幾個設(shè)計方案如下:
- 訪問者模式:ANTLR4通過訪問者模式支持業(yè)務(wù)代碼可訪問特定“詞法”或“語法”節(jié)點執(zhí)行自定義的操作,通過這個方式完全解耦A(yù)ST(抽象語法樹)生成和業(yè)務(wù)邏輯,詞法分析器和解釋器專注于AST生成,而業(yè)務(wù)可以通過訪問器的擴(kuò)展支持業(yè)務(wù)定制化訴求。
- 語法和語義的獨立性:ANTLR4中可以獨立進(jìn)行語法解析和語義分析,可以在 AST 中進(jìn)行語義檢查和業(yè)務(wù)邏輯處理。這種分離使得開發(fā)者可以更靈活地處理輸入的語法和語義。
- AST生成:ANRL4通過語法解析器生成結(jié)構(gòu)化AST(抽象語法樹),不同業(yè)務(wù)邏輯可以不斷復(fù)用同一個AST。
- 上下文模式:解析器在處理輸入數(shù)據(jù)時,上下文會在解析樹中傳遞信息。每當(dāng)進(jìn)入一個新的語法規(guī)則時,都會創(chuàng)建一個新的上下文實例上下文可以存儲解析過程中需要的臨時信息,例如變量的值、數(shù)據(jù)類型等。上下文信息主要結(jié)合訪問器模式進(jìn)行使用,同時也解決了在解析復(fù)雜語句如多層嵌套結(jié)構(gòu)的層級調(diào)用問題。
三、SparkSQL介紹
Spark SQL 是 Apache Spark 的一個模塊,專門用于處理結(jié)構(gòu)化數(shù)據(jù),Spark SQL 的特點包括:
- 高效的查詢執(zhí)行:通過 Catalyst 優(yōu)化器和 Tungsten 執(zhí)行引擎,Spark SQL 能夠優(yōu)化查詢執(zhí)行計劃,提升查詢性能。
- 與 Hive 的兼容性:Spark SQL 支持 HiveQL 語法,使得用戶可以輕松遷移現(xiàn)有的 Hive 查詢。
- 支持多種數(shù)據(jù)源:Spark SQL 可以從多種數(shù)據(jù)源讀取數(shù)據(jù),包括 HDFS、Parquet、ORC、JDBC 等。
四、技術(shù)實現(xiàn)
語法設(shè)計
在Aparch Spark源碼中就是使用ANTLR4來解析和處理SQL語句,以下為Apach Spark中基于ANTLR元語言定義的詞法分析器和語法分析器,在語法定義上我們只需要基于這套標(biāo)準(zhǔn)的SparkSQL語法去適配得物自研引擎的能力,做能力對齊。
Lexer.g4
https://github.com/apache/spark/blob/master/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
Parser.g4
https://github.com/apache/spark/blob/master/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
語法補(bǔ)全
以下我們以字段補(bǔ)全場景為例解析從語法定義,語法解析,語法補(bǔ)全,上下文信息采集各個流程節(jié)點剖析最后完成的表字段信息精準(zhǔn)推薦。在下列語法場景中,存在多層Select語法嵌套,同時表du_emr_test.empsalary tableB和表du_emr_test.hujh_type_tk AS tableB設(shè)置了同一別名, 如圖在父子查詢中都使用了同一個表別名(tableB),當(dāng)用戶在父子查詢中分別輸入tableB.時,這時候需要結(jié)合當(dāng)前上下文語境,對tableB別名推薦不同表的字段。
SELECT
tableB.c1
FROM
(
SELECT
tableB.empno,
tableC.department
FROM
du_emr_test.empsalary as tableB
LEFT JOIN du_emr_test.employees AS tableC
WHERE tableC.department = tableB.depname
) AS tableA
LEFT JOIN du_emr_test.hujh_type_tk AS tableB
WHERE tableB.c1 = tableA.dename
圖片
圖片
圖片
圖片
在子查詢中我們期望推薦tableB來自du_emr_test.empsalary tableB的字段信息,而在最外層中我們期望的是du_emr_test.hujh_type_tk的字段,如上圖。
基于以上場景我們核心要解決2個問題:
問題1:當(dāng)前光標(biāo)應(yīng)該提示哪些推薦語法類型
目前,開源方案ANTLR-C3引擎就能完美解決我們問題,用戶在編輯器實時輸入時,獲取當(dāng)前光標(biāo)位置,實時做語法解析,然后基于開源的ANTLR-C3引擎能力結(jié)合ANTLR 生成的AST即可獲取當(dāng)前光標(biāo)位置所需要的語法規(guī)則。
問題2: 獲取當(dāng)前上下文信息以實現(xiàn)精準(zhǔn)推薦
根據(jù)不同業(yè)務(wù)場景需要采集的上下文信息不同,基于字段推薦的場景,我們需要獲取當(dāng)前光標(biāo)位置處可以推薦的表信息,表別名信息,結(jié)合編輯器能力實時獲取表對應(yīng)的字段信息進(jìn)行字段推薦補(bǔ)全,而上下文信息的采集,我們可以通過ANTLR生成的監(jiān)聽器來實現(xiàn)。
語法定義
以下我們用ANTLR元語言實現(xiàn)一段簡化版的SQL查詢場景的語法規(guī)則(QueryStatment),方便我們理解。
lexer grammar SqlLexer;
// 基礎(chǔ)詞法
COMMA: ',';
LEFT_PAREN: '(';
RIGHT_PAREN: ')';
IDENTIFY: (LETTER | DIGIT | '_' | '.')+;
fragment DIGIT: [0-9];
fragment LETTER: [A-Z];
SEMICOLON: ';';
parser grammar SqlParser;
program: statment* EOF;
statment: queryStatment SEMICOLON?;
// 查詢語句
queryStatment:
SELECT columnNames FROM (
tableName
| (LEFT_PAREN queryStatment LEFT_PAREN)
) whereExpression? relationsExpresssion? SEMICOLON?;
// 字段
columnNames: columnName (COMMA columnName)*;
tableName: IDENTIFY AS? tableAlis;
tableAlis: IDENTIFY;
columnName: IDENTIFY AS? columnAlis;
columnAlis: IDENTIFY;
whereExpression: WHERE booleanExpression;
booleanExpression: (NOT | BANG) booleanExpression # logicalBinary
| left = booleanExpression operator = AND right = booleanExpression # logicalBinary
| left = booleanExpression operator = OR right = booleanExpression # logicalBinary;
relationsExpresssion:
LEFT JOIN tableName whereExpression?
| RIGHT JOIN tableName whereExpression?;
代碼生成
圖片
圖片
以下是部分生成代碼:
1.詞法分析器
// SqlLexer.ts
public static readonly COMMA = 1;
public static readonly LEFT_PAREN = 2;
public static readonly RIGHT_PAREN = 3;
public static readonly IDENTIFY = 4;
public static readonly SEMICOLON = 5;
// 詞法分析器可以使用的通道
public static readonly channelNames = [
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
];
// 包含了所有字面量記號的名稱
public static readonly literalNames = [
null, "','", "'('", "')'", null, "';'"
];
// 包含為每個記號分配的符號名,這些符號在生成解析器時用于標(biāo)識記號
public static readonly symbolicNames = [
null, "COMMA", "LEFT_PAREN", "RIGHT_PAREN", "IDENTIFY", "SEMICOLON"
];
// ANTLR 生成的類中的一個字段,列出了所有定義的規(guī)則
public static readonly ruleNames = [
"COMMA", "LEFT_PAREN", "RIGHT_PAREN", "IDENTIFY", "DIGIT", "LETTER",
"SEMICOLON",
];
2.語法分析器
ANTLR自動為每個規(guī)則生成了一個解析方法,以下是tableName的 ANTLR 中的解析器方法,具備了處理標(biāo)識符、可選的別名和錯誤處理的能力。
// SQLParse.ts
// ANTLR自動生成了一個解析 SQL 表名的 ANTLR 中的解析器方法,具備了處理標(biāo)識符、可選的別名和錯誤處理的能力
public tableName(): TableNameContext {
let localContext = new TableNameContext(this.context, this.state);
this.enterRule(localContext, 8, SqlParser.RULE_tableName);
let _la: number;
try {
this.enterOuterAlt(localContext, 1);
{
this.state = 60;
this.match(SqlParser.IDENTIFY);
this.state = 62;
this.errorHandler.sync(this);
_la = this.tokenStream.LA(1);
if (_la === 8) {
{
this.state = 61;
this.match(SqlParser.AS);
}
}
this.state = 64;
this.tableAlis();
}
}
catch (re) {
if (re instanceof antlr.RecognitionException) {
this.errorHandler.reportError(this, re);
this.errorHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localContext;
}
自動補(bǔ)全
ANTLR4代碼補(bǔ)全核心(antlr4-c3) 是一個開創(chuàng)性的工具,它為ANTLR4生成的解析器提供了一個通用的代碼補(bǔ)全解決方案。無論你的項目是處理哪種編程語言或領(lǐng)域特定語言(DSL),只要是基于ANTLR就能夠利用這個庫實現(xiàn)精準(zhǔn)的代碼建議和自動補(bǔ)全,極大地增強(qiáng)開發(fā)體驗。通過antlr4-c3 能力我們通過手動配置需要收集的語法規(guī)則,獲取在當(dāng)前光標(biāo)處需要推薦的語法規(guī)則類型。
1.語法規(guī)則
通過ANTLR4工具我們可以自動生成Sqllexer.ts詞法解析器,SqlParser.ts語法解析器,SqlParserLister.ts訪問器,SqlParseVisitor.ts監(jiān)聽器,在SqlParser 語法解析器自動生成了我們在語法定義中的語法規(guī)則。
preferredRules = new Set([
SqlParser.RULE_tableName,
SqlParser.RULE_columnName,
]);
2.代碼補(bǔ)全
以下我們實現(xiàn)一套簡化版的代碼補(bǔ)全能力。
當(dāng)用戶在編輯器實時輸入時,調(diào)用getSuggestionAtCaretPosition獲取當(dāng)前語境中需要推薦的信息,包含語法規(guī)則,關(guān)鍵詞,上下文信息,在結(jié)合業(yè)務(wù)層數(shù)據(jù)做自動補(bǔ)全,其中包含5個核心步驟:
- 獲取當(dāng)前語法解析器實例。
- 獲取當(dāng)前光標(biāo)位置對應(yīng)的Token。
- 生成AST。
- 獲取當(dāng)前語境上下文信息。
- 通過ANTLR-C3獲取當(dāng)前位置候選語法規(guī)則。
public getSuggestionAtCaretPosition(
sqlContent: string,
caretPosition: CaretPosition
preferredRules: Set
): Suggestions | null {
// 1、 使用SqlParse解析器獲取
const sqlParserIns = new SqlParse(sqlContent)
// 2、獲取當(dāng)前光標(biāo)處token
const charStreams = CharStreams.fromString(sqlContent);
const lexer = new SqlLexer(charStreams);
const tokenStream = new CommonTokenStream(lexer);
tokenStream.fill()
const allTokens = tokenStream.getTokens();
let caretTokenIndex = findCaretToken(caretPosition, allTokens);
// 3、獲取AST抽象語法樹
const parseTree = sqlParserIns.program()
// 4、通過監(jiān)聽器采集上下文表信息(下面上下文分析部分闡述細(xì)節(jié))
const tableEntity = getTableEntitys()
// 異常場景兼容存在多條sql, 獲取有效最小SQL范圍給到antlr4-c3做推薦。
const statementCount = splitListener.statementsContext?.length;
const statementsContext = splitListener.statementsContext;
// 5、antlr4-c3接入獲取推薦語法規(guī)則
let tokenIndexOffset: number = 0;
const core = new CodeCompletionCore(sqlParserIns);
// 推薦規(guī)則 來自SQLparse解析器的規(guī)則(元語言定義)
core.preferredRules = preferredRules;
// 通過AST和當(dāng)前光標(biāo)Token獲取推薦類型
const candidates = core.collectCandidates(caretTokenIndex, parseTree);
// ruleType -> preferredRules
// const [rules, tokens] = candidate;
const rules = [];
const keywords = [
for (let candidate of candidates.rules) {
const [ruleType] = candidate;
let synContextType;
switch (ruleType) {
case SqlParser.RULE_tableName: {
syntaxContextType = 'table';
break;
}
case SqlParser.RULE_columnName: {
syntaxContextType = 'column';
break;
}
default:
break;
}
if (synContextType) {
rules.push(syntaxContextType)
}
}
// 獲取對應(yīng)keywords
for (let candidate of candidates.tokens) {
const displayName = sqlParserIns.vocabulary.getDisplayName(candidate[0]);
const keyword = displayName.startsWith("'") && displayName.endsWith("'")
? displayName.slice(1, -1)
: displayName
keywords.push(keyword);
}
return {
rules,
keywords,
tableEntity
};
}
在這里我們簡化了流程,忽略了很多異常case的處理,自動補(bǔ)全的前提是在當(dāng)前語法規(guī)則正確,而在多級子查詢嵌套場景我們需要考慮到過濾異常QueryStatment, 在當(dāng)前光標(biāo)出最小范圍有效的QueryStatment做補(bǔ)全。這時候需要配合監(jiān)聽器去做上下文采集做容錯性更高的自動補(bǔ)全。
上下文分析
圖片
如圖:每個table都?xì)w屬于一個QueryStatment表達(dá)式, 查詢中又存在子層級查詢的嵌套。我們需要通過上下文收集以下信息:
- 每個查詢語句的信息,包含Position位置信息,記錄當(dāng)前的查詢開始行,結(jié)束行,開始列,結(jié)束列。
- 查詢語句的關(guān)聯(lián)關(guān)系,即記錄當(dāng)前查詢語句父級查詢語句對象。
- 表實體信息包含表名,表位置信息,表別名信息,當(dāng)前表歸屬于那個查詢語句。
則我們需要監(jiān)聽3個語法規(guī)則包含QueryStatment, TableName,TableAlias, 采集QueryStatment信息,Table信息同時將table與當(dāng)前歸屬的QueryStatment做關(guān)聯(lián), 還有與別名信息作配對關(guān)聯(lián)。這就要求在不同監(jiān)聽器之間的信息需要做共享,上下文信息需要做傳遞和保留。ANTLR常用的3種信息共享方案包含:
- 使用訪問器方法來返回值,
- 使用類成員在事件方法之間共享數(shù)據(jù),
- 在語法定義中使用樹標(biāo)記來存儲信息。
在這里我們使用第二種(在這里我們簡化了SQL的語法定義,在實際場景中語法層級深度和復(fù)雜度遠(yuǎn)比當(dāng)前高,這也使得方案1和3實際操作起來更麻煩,規(guī)則嵌套層級深使得方案一和方案三開發(fā)成本和維護(hù)成本更高)
1.監(jiān)聽器(SqlParserLister)
通過ANTLR4工具我們可以自動生成SqlParserLister.ts監(jiān)聽器進(jìn)行自定義擴(kuò)展。
// SqlParserListener.ts
export class QueryStatmentContext extends antlr.ParserRuleContext {
public override enterRule(listener: SqlParserListener): void {
if(listener.enterQueryStatment) {
listener.enterQueryStatment(this);
}
}
public override exitRule(listener: SqlParserListener): void {
if(listener.exitQueryStatment) {
listener.exitQueryStatment(this);
}
}
}
export class TableNameContext extends antlr.ParserRuleContext {
public override enterRule(listener: SparkSqlParserListener): void {
if(listener.enterTableName) {
listener.enterTableName(this);
}
}
public override exitRule(listener: SparkSqlParserListener): void {
if(listener.exitTableName) {
listener.exitTableName(this);
}
}
}
// ....
export class TableAliasContext extends antlr.ParserRuleContext {
public KW_AS(): antlr.TerminalNode | null {
return this.getToken(SparkSqlParser.KW_AS, 0);
}
public override enterRule(listener: SparkSqlParserListener): void {
if(listener.enterTableAlias) {
listener.enterTableAlias(this);
}
}
public override exitRule(listener: SparkSqlParserListener): void {
if(listener.exitTableAlias) {
listener.exitTableAlias(this);
}
}
}
2.自定義監(jiān)聽器擴(kuò)展
通過SqlParserListener我們可以自定義采集上下文信息。在
- 監(jiān)聽進(jìn)入QueryStatment表達(dá)式采集當(dāng)前表達(dá)式信息到_queryStmtsStack。
- 監(jiān)聽退出TableNameToken時采集當(dāng)前Table信息,并關(guān)聯(lián)當(dāng)前QueryStatment。
- 監(jiān)聽退出TableAliasToken時采集信息,并關(guān)聯(lián)到Table實體。
- 監(jiān)聽退出QueryStatment表達(dá)式推出_queryStmtsStack
// tableEntityCollect
export class SqlEntityCollector implements SqlParserListener {
super() {
this._tableEntitiesSet = new Set();
this._queryStmtsStack = [];
this._tableAliasStack = [];
this._currentTable = '';
}
enterQueryStatment(ctx: QueryStatmentContext) {
this.pushQueryStmt(ctx);
}
exitQueryStatment(ctx: QueryStatmentContext) {
this.popQueryStmt();
}
exitTableName(ctx: TableNameContext) {
this.pushTableEntity(ctx);
this.setCurrentTable(ctx);
}
exitTableAlias(ctx: TableAliasContext) {
this.pushTableEntity(ctx);
}
pushQueryStmt() {} // 采集QueryStmt信息
popQueryStmt() {} // 推出當(dāng)前QueryStmt,進(jìn)入下個同級Stmt
pushTableEntity() {} // 采集當(dāng)前表信息,關(guān)聯(lián)當(dāng)前Stmt
pushTableEntity() {} // 采集關(guān)聯(lián)表
enterProgram() {} // 清空重置
getTableEntity() {
return this.TableEntity(ctx)
}
}
在這里我們簡化了語法定義的規(guī)則便于講解,但在實際中語法規(guī)則的整體嵌套層級是很深的,從以下的SparkSql語法定義中我們可以看到右側(cè)聚合的表達(dá)式高達(dá)200+個,單個表達(dá)式的備選分支最多高達(dá)140+,這也加大了上下文分析采集的復(fù)雜度,即我們無法簡單的從QueryStmt當(dāng)前QueryStatmentContext中獲取全量信息。
圖片
3.觸發(fā)監(jiān)聽器采集上下文信息
getTableEntitys() {
const collectListener = new SqlEntityCollector(sqlContent, caretTokenIndex);
const parse = new SqlParse(sqlContent);
const parseTree= sqlParserIns.program();
ParseTreeWalker.DEFAULT.walk(collectListener, parseTree);
return collectListener.getTableEntity()
}
語法校驗
ANRLR在生成語法分析器中內(nèi)置了自動錯誤報告和恢復(fù)策略,能夠在遇到句法錯誤時自動產(chǎn)生錯誤消息,為每個句法錯誤產(chǎn)生一條錯誤消息。
詞法錯誤
常見的詞法錯誤包含字符遺漏,詞法錯誤。舉個例子,在spark標(biāo)準(zhǔn)語法定義中 tableName規(guī)則不支持表變量場景(${variable}),如果要兼容這里詞法,就需要在語法定義中變更tableName的語法規(guī)則定義。
以下是語法定義變更:
- 新增詞法規(guī)則$, {, }。
- 新增語法規(guī)則identifyVar支持變量模式。
SqlLexer.g4
// 新增詞法
LEFT_BRACE : '{';
RIGHT_BRACE : '}';
VARIABLE : '$';
SqlParse.g4
// before tableName: IDENTIFY AS? tableAlis;
tableName: identifyVar AS? tableAlis;
identifyVar
: IDENTIFY // odps_table_a
| IDENTIFY? VARIABLE LEFT_BRACE IDENTIFY RIGHT_BRACE IDENTIFY? // odps_table_a_${variable} odps_table_a_${prefix_variable}_abs
自動恢復(fù)機(jī)制
語法分析器不應(yīng)該在遇到非法的成員定義時結(jié)束,而是應(yīng)盡最大可能匹配到一個合法的類定義,ANRTL4自動錯誤恢復(fù)機(jī)制能在語法分析器在發(fā)現(xiàn)語法錯誤后還能繼續(xù)進(jìn)行嘗試語法解析和自動恢復(fù)。
1.異常捕獲
ANRLT自動生成的語法解析器中自動為每個規(guī)則包裹異常捕獲能力,并在catch中嘗試錯誤恢復(fù)。
圖片
2.恢復(fù)策略
一般情況下,語法分析器在遇到無法匹配的錯誤時會嘗試最簡單的符號補(bǔ)全和移除來嘗試解析,都不管用時,這時候就會用更高階的策略來進(jìn)行恢復(fù)。包括掃描后續(xù)詞法符號來恢復(fù),從不匹配的詞法符號中恢復(fù),從子規(guī)則的錯誤中恢復(fù),捕獲失敗的語義判定。
雖然ANTLR提供了很多策略來進(jìn)行錯誤恢復(fù),但在實際業(yè)務(wù)場景中,需要結(jié)合考慮語法、語境的復(fù)雜度去權(quán)衡性能與更友好的錯誤提示之間的抉擇。在復(fù)雜場景中ANTLR表現(xiàn)并不理想,在一些復(fù)雜語法和語境的情況下解析器在檢測錯誤時難以做出合理的決策,例如:遞歸和嵌套結(jié)構(gòu)中會使得錯誤恢復(fù)變得很復(fù)雜,導(dǎo)致解析器無法做出合理決策。還有在上下文敏感的語境中,錯誤恢復(fù)機(jī)制基本無法提供有效恢復(fù)。
性能
在 ANTLR 4 中,語法復(fù)雜度、語法歧義、語法規(guī)則嵌套深度與預(yù)測算法的選擇都會顯著影響解析器的性能和準(zhǔn)確性。Spark SQL語法規(guī)則達(dá)200+,備選分支最高達(dá)140, 嵌套深度達(dá)20+,同時又存在負(fù)責(zé)循環(huán)嵌套場景, 這也意味著在整個語法解析,語法錯誤的處理過程是很復(fù)雜的,當(dāng)遇到復(fù)雜大SQL量和一片狼籍的語法錯誤SQL,會導(dǎo)致語法解析過程變得緩慢引發(fā)性能問題。目前在性能優(yōu)化上,有以下幾個方向。
緩存優(yōu)化
在antlr4中詞法解析和語法解析能力和業(yè)務(wù)是完全解耦的,這也意味著底層基于同個SQL內(nèi)容解析出來的tokens和parserTree都是可以在不同業(yè)務(wù)邏輯應(yīng)用里復(fù)用。我們可以通過緩存tokens,parseTree減少詞法解析和語法解析的損耗。
語法優(yōu)化
通過減少語法樹的層級和優(yōu)化表達(dá)式減少解析過程中“二義性”的次數(shù),可以加速語法解析的速度,優(yōu)化AST生成性能。合理使用語法定義中用法,例如樹標(biāo)記(用于上下文通信數(shù)據(jù)共享),在語法解析過程中會為每個標(biāo)記生成上下文,這也意味著每個局部結(jié)果都會保留,會有更大的內(nèi)存消耗。
預(yù)測模型選擇
在語法解析中不同預(yù)測模型的選擇對解析性能有顯著影響,針對不同的場景需要評估時效性與正確性之間的衡量。
ANTLR4預(yù)測模型:
https://www.antlr.org/api/Java/org/antlr/v4/runtime/atn/PredictionMode.html
我們可以選擇性價比更高的SLL預(yù)測模型作為語法分析策略,結(jié)合定制化的錯誤監(jiān)聽器做錯誤糾正。
編輯器應(yīng)用
編輯器集成
與MonacoEditor集成流程可查看此文章 https://blog.shizhuang-inc.com/article/MTUzNzY?fromType=personal_blog
輔助編程
1.信息項提示(表,函數(shù),字段)
圖片
圖片
圖片
2.自動補(bǔ)全(庫,表,字段,語法)
圖片
圖片
圖片
五、大模型下的SQL編輯器應(yīng)用
隨著大模型的蓬勃發(fā)展,在數(shù)據(jù)產(chǎn)品中的應(yīng)用也逐步得到了驗證和落地,目前,Galaxy還沒有接入Copilot, 內(nèi)部暫時還沒有基于SQL的Copilot。業(yè)界較成熟的是阿里云的Dataworks, DataWorks于2023年推出了Copilot 產(chǎn)品, 核心2個方向,一個方向是智能 SQL 編程助手,輔助 SQL 編程,支持 NL2SQL 及 SQL 代碼補(bǔ)全;另一個方向是 AI Agent,提供 LUI(自然語言用戶界面),以提升產(chǎn)品功能操作的便捷性和用戶體驗。
NL2SQL應(yīng)用場景
基于SQL的Copilot一般在以下幾個應(yīng)用場景比較深入和廣泛的落地效果:簡單數(shù)據(jù)查詢,SQL 優(yōu)化與轉(zhuǎn)換,SQL 語法查詢與講解, 函數(shù)查詢,功能咨詢,注釋生成,SQL 解釋,SQL 一鍵糾錯。
NL2SQL自動補(bǔ)全
代碼補(bǔ)全是編程類 Copilot 的主要場景和能力,單市場上主流的編程類 Copilot 對 SQL 支持的好的并不多見。眾所周知,SQL 代碼補(bǔ)全比其他高級語言的代碼補(bǔ)全更具挑戰(zhàn)性,主要原因有以下幾個方面:
- 上下文和環(huán)境的依賴性:SQL 代碼不是獨立存在的,而是依賴于數(shù)據(jù)表的元數(shù)據(jù)信息以及表與表之間的關(guān)聯(lián)關(guān)系。
- SQL 語義多樣性:實現(xiàn)同一種查詢結(jié)果,可以有多種 SQL 寫法,如何實現(xiàn)“最佳”寫法存在挑戰(zhàn)。
- 語法簡潔但高度專業(yè)化:SQL 語法簡潔但每一個關(guān)鍵字、函數(shù)或語法都有特定的含義,大模型要準(zhǔn)確理解這些得通過針對性的訓(xùn)練學(xué)習(xí)。
- 執(zhí)行計劃和性能考量: 這跟數(shù)據(jù)庫底層的執(zhí)行計劃有關(guān),需要考慮如何書寫才能使 SQL 的性能最優(yōu)。
- 數(shù)據(jù)庫特異性:市面上不同的數(shù)據(jù)庫往往存在不同的 SQL 方言,存在差異,針對這種差異性我們要投入大量時間做 SQL 數(shù)據(jù)集準(zhǔn)備、數(shù)據(jù)標(biāo)注、模型微調(diào)。
- 高度業(yè)務(wù)相關(guān)性:SQL 語句通常與特定業(yè)務(wù)高度相關(guān),比如一個指標(biāo)存在特定的計算口徑,這是與公司業(yè)務(wù)相關(guān),通用的大模型也無法提前學(xué)習(xí)。
目前較成熟的代碼補(bǔ)全核心場景主要在有規(guī)律的代碼連續(xù)推薦場景(例如:字段、字段別名推薦,注釋推薦、分區(qū)字段推薦、Group by 字段推薦,上下文自動聯(lián)想推薦等)。
六、總結(jié)
通過SQL引擎能力建設(shè)我們在Galaxy數(shù)據(jù)研發(fā)IDE上支持了個性化詞法規(guī)則定制能力,包含字段別名支持中文, 表變量等場景, 同時通過語法解析和監(jiān)聽器能力,支持實時識別各類的語法規(guī)則,包含表,函數(shù),字段等做輔助編程提示和做精準(zhǔn)化的庫,表,字段代碼補(bǔ)全和推薦。
后續(xù)我們?nèi)悦媾R很大的挑戰(zhàn),在非專業(yè)的數(shù)據(jù)開發(fā)背景、復(fù)雜的業(yè)務(wù)定制需求、語言定義的復(fù)雜性和嵌套深度等因素共同導(dǎo)致了解析器的開發(fā)難度。目前,在語法校驗自動糾錯提示上,雖然ANTLR的提供了自動錯誤恢復(fù)機(jī)制但整體表現(xiàn)并不理想,后續(xù)2個方向,第一,接入大模型的能力。第二,從基礎(chǔ)語法定義上進(jìn)行重構(gòu),減少語法歧義和層級優(yōu)化。為了應(yīng)對這些挑戰(zhàn),我們需要加強(qiáng)對 ANTLR 和 Spark SQL語言,數(shù)據(jù)處理的理解,以便順利使用和擴(kuò)展解析器。
參考資料
- ANTLR
- ANTLR4-C3
- DataWorks Copilot:大模型時代數(shù)據(jù)開發(fā)的新范式
- ANTLR4權(quán)威指南 - [美] 特恩斯·帕爾 著