基于ANTLR4的大數(shù)據(jù)SQL編輯器解析引擎實(shí)踐
一、背景
二、ANTLR4 簡(jiǎn)介
1. ANTLR4 特性
2. ANTLR4 的應(yīng)用場(chǎng)景
3. ANTLR4入門
三、SparkSQL介紹
四、技術(shù)實(shí)現(xiàn)
1. 語法設(shè)計(jì)
2. 語法補(bǔ)全
3. 語法校驗(yàn)
4. 性能
5.編輯器應(yīng)用
五、大模型下的SQL編輯器應(yīng)用
1. NL2SQL應(yīng)用場(chǎng)景
2. NL2SQL自動(dòng)補(bǔ)全
六、總結(jié)
一、背景
隨著得物離線業(yè)務(wù)的快速增長,為了脫離全托管服務(wù)的一些限制和享受技術(shù)發(fā)展帶來的成本優(yōu)化,公司提出了大數(shù)據(jù)Galaxy開源演進(jìn)項(xiàng)目,將離線業(yè)務(wù)從全托管且封閉的環(huán)境遷移到一個(gè)開源且自主可控的生態(tài)系統(tǒng)中,而離線開發(fā)治理套件是Galaxy自研體系中一個(gè)核心的項(xiàng)目,在數(shù)據(jù)開發(fā)IDE中最核心的就是SQL編輯器,我們需要一個(gè)SQL解析引擎在SQL編輯提供適配得物自研Spark引擎的語法定義,實(shí)時(shí)語法解析,語法補(bǔ)全,語法校驗(yàn)等能力,結(jié)合業(yè)內(nèi)dataworks和dataphin的實(shí)踐,我們最終選用ANTLR作為SQL解析引擎底座。
二、ANTLR4 簡(jiǎn)介
ANTLR(一種語法解析引擎工具)是一個(gè)功能強(qiáng)大的解析器生成器,用于讀取、處理、執(zhí)行或翻譯結(jié)構(gòu)化文本或二進(jìn)制文件。它廣泛用于構(gòu)建語言、工具和框架。ANTLR可以根據(jù)語法規(guī)則文件生成一個(gè)可以構(gòu)建和遍歷解析樹的解析器。
ANTLR4 特性
ANTLR4 是一個(gè)強(qiáng)大的工具,適合用于語言處理、編譯器構(gòu)建、代碼分析等多種場(chǎng)景。它的易用性、靈活性和強(qiáng)大的特性使得它成為開發(fā)者的熱門選擇。
- 強(qiáng)大的文法定義:ANTLR4 允許用戶使用簡(jiǎn)單且易讀的文法語法來定義語言的結(jié)構(gòu)。這使得創(chuàng)建和維護(hù)語言解析器變得更加直觀,同時(shí)在復(fù)雜文法構(gòu)造上支持左遞歸文法、嵌套結(jié)構(gòu)以及其他復(fù)雜的文法構(gòu)造,使得能夠解析更復(fù)雜的語言結(jié)構(gòu)。
- 抽象語法樹遍歷:ANTLR4 可以生成抽象語法樹,使得在解析源代碼時(shí)能夠更容易地進(jìn)行分析和變換。AST 是編譯器和解釋器的核心組件。同時(shí)提供了簡(jiǎn)單的 API 來遍歷生成的語法樹,使得實(shí)現(xiàn)代碼分析、轉(zhuǎn)換等操作變得簡(jiǎn)單
- 自動(dòng)語法錯(cuò)誤處理:ANTLR4 提供了內(nèi)置的錯(cuò)誤處理機(jī)制,可以在解析過程中自動(dòng)處理語法錯(cuò)誤,并且可以自定義錯(cuò)誤消息和處理邏輯
- 可擴(kuò)展性:ANTLR4 允許用戶擴(kuò)展和自定義生成的解析器的行為。例如,您可以自定義解析器的方法、錯(cuò)誤處理以及其他功能。
- 工具&社區(qū)生態(tài):ANTLR4 提供了豐富的工具支持,包括命令行工具、集成開發(fā)環(huán)境插件和可視化工具,可以幫助您更輕松地開發(fā)和調(diào)試解析器。同時(shí)擁有活躍的社區(qū),提供了大量的文檔、示例和支持。這使得新用戶能夠快速上手,并得到必要的幫助。
ANTLR4 的應(yīng)用場(chǎng)景
Apache Spark: 流行的大數(shù)據(jù)處理框架,使用ANTLR作為其SQL解析器的一部分,支持SQL查詢。
Twitter: Twitter 使用ANTLR來解析和分析用戶的查詢語言,這有助于他們的搜索和分析功能。
IBM: IBM使用ANTLR來支持一些其產(chǎn)品和工具中的DSL(領(lǐng)域特定語言)解析需求,例如,在其企業(yè)集成解決方案中。
ANTLR4入門
ANTLR元語言
為了實(shí)現(xiàn)一門計(jì)算機(jī)編程語言,我們需要構(gòu)建一個(gè)程序來讀取輸入語句,對(duì)其中的詞組和符號(hào)進(jìn)行識(shí)別處理,即我們需要語法解釋器或者翻譯器來識(shí)別出一門特定語言的所有詞組,子詞組,語句。我們將語法分析過程拆分為兩個(gè)獨(dú)立的階段則為詞法分析和語法分析。
圖片
ANTLR語法遵循了一種專門用來描述其他語言的語法,我們稱之為ANTLR元語言(ANTLR’s meta-language)。ANTLR元語句是一個(gè)強(qiáng)大的工具,可以用來定義編程語言的語法。通過定義詞法和語法規(guī)則,可以基于antlr生成解析器和詞法分析器。
1.自頂向下
在語言結(jié)構(gòu)中,整體的辨識(shí)都是從最粗的粒度開始,一直進(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ì)算機(jī)語言常見4種語言模式:序列(sequence)、選擇(choice)、詞法符號(hào)依賴 (token dependency),以及嵌套結(jié)構(gòu)(nested phrase)。以下是ANTLR對(duì)4種模式的語法規(guī)則描述。
圖片
3.語法歧義
在自頂向下的語法和手工編寫的遞歸下降語法分析器中,處理表達(dá)式都是一件相當(dāng)棘手的事情,這首先是因?yàn)榇蠖鄶?shù)語法都存在歧義,其次是因?yàn)榇蠖鄶?shù)語言的規(guī)范使用了一種特殊的遞歸方式,稱為左遞歸。
expr : expr '*' expr
| expr '+' expr
| INT
;
我們舉個(gè)運(yùn)算符優(yōu)先級(jí)帶來的語法歧義問題,同樣的規(guī)則可以匹配多個(gè)輸入字符流。
圖片
在其他語法工具中,通常通過指定額外的標(biāo)記來指定運(yùn)算符優(yōu)先級(jí)。而在ANTLR4中通過備選分支的排序來指定優(yōu)先級(jí),越靠前優(yōu)先級(jí)越高。
代碼自動(dòng)生成
ANTLR可以根據(jù)lexer.g4和parser.g4自動(dòng)生成詞法分析器,語法分析器,監(jiān)聽器,訪問器等。
antlr4ng -Dlanguage=TypeScript -visitor -listener -Xexact-output-dir -o ./src/lib ./src/grammar/*.g
圖片
語法解析與業(yè)務(wù)邏輯解耦
在ANTLR4中語法解析和業(yè)務(wù)邏輯的高度解耦是一個(gè)重要的設(shè)計(jì)理念,優(yōu)點(diǎn)就是同一個(gè) AST 結(jié)構(gòu)能夠在不同的業(yè)務(wù)邏輯實(shí)現(xiàn)之間實(shí)現(xiàn)復(fù)用。不同的業(yè)務(wù)邏輯(如執(zhí)行、轉(zhuǎn)換、優(yōu)化等)可以對(duì)同一個(gè) AST 進(jìn)行不同的處理,而不需要關(guān)心解析過程。核心幾個(gè)設(shè)計(jì)方案如下:
- 訪問者模式:ANTLR4通過訪問者模式支持業(yè)務(wù)代碼可訪問特定“詞法”或“語法”節(jié)點(diǎn)執(zhí)行自定義的操作,通過這個(gè)方式完全解耦A(yù)ST(抽象語法樹)生成和業(yè)務(wù)邏輯,詞法分析器和解釋器專注于AST生成,而業(yè)務(wù)可以通過訪問器的擴(kuò)展支持業(yè)務(wù)定制化訴求。
- 語法和語義的獨(dú)立性:ANTLR4中可以獨(dú)立進(jìn)行語法解析和語義分析,可以在 AST 中進(jìn)行語義檢查和業(yè)務(wù)邏輯處理。這種分離使得開發(fā)者可以更靈活地處理輸入的語法和語義。
- AST生成:ANRL4通過語法解析器生成結(jié)構(gòu)化AST(抽象語法樹),不同業(yè)務(wù)邏輯可以不斷復(fù)用同一個(gè)AST。
- 上下文模式:解析器在處理輸入數(shù)據(jù)時(shí),上下文會(huì)在解析樹中傳遞信息。每當(dāng)進(jìn)入一個(gè)新的語法規(guī)則時(shí),都會(huì)創(chuàng)建一個(gè)新的上下文實(shí)例上下文可以存儲(chǔ)解析過程中需要的臨時(shí)信息,例如變量的值、數(shù)據(jù)類型等。上下文信息主要結(jié)合訪問器模式進(jìn)行使用,同時(shí)也解決了在解析復(fù)雜語句如多層嵌套結(jié)構(gòu)的層級(jí)調(diào)用問題。
三、SparkSQL介紹
Spark SQL 是 Apache Spark 的一個(gè)模塊,專門用于處理結(jié)構(gòu)化數(shù)據(jù),Spark SQL 的特點(diǎn)包括:
- 高效的查詢執(zhí)行:通過 Catalyst 優(yōu)化器和 Tungsten 執(zhí)行引擎,Spark SQL 能夠優(yōu)化查詢執(zhí)行計(jì)劃,提升查詢性能。
- 與 Hive 的兼容性:Spark SQL 支持 HiveQL 語法,使得用戶可以輕松遷移現(xiàn)有的 Hive 查詢。
- 支持多種數(shù)據(jù)源:Spark SQL 可以從多種數(shù)據(jù)源讀取數(shù)據(jù),包括 HDFS、Parquet、ORC、JDBC 等。
四、技術(shù)實(shí)現(xiàn)
語法設(shè)計(jì)
在Aparch Spark源碼中就是使用ANTLR4來解析和處理SQL語句,以下為Apach Spark中基于ANTLR元語言定義的詞法分析器和語法分析器,在語法定義上我們只需要基于這套標(biāo)準(zhǔn)的SparkSQL語法去適配得物自研引擎的能力,做能力對(duì)齊。
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ǔ)全場(chǎng)景為例解析從語法定義,語法解析,語法補(bǔ)全,上下文信息采集各個(gè)流程節(jié)點(diǎn)剖析最后完成的表字段信息精準(zhǔn)推薦。在下列語法場(chǎng)景中,存在多層Select語法嵌套,同時(shí)表du_emr_test.empsalary tableB和表du_emr_test.hujh_type_tk AS tableB設(shè)置了同一別名, 如圖在父子查詢中都使用了同一個(gè)表別名(tableB),當(dāng)用戶在父子查詢中分別輸入tableB.時(shí),這時(shí)候需要結(jié)合當(dāng)前上下文語境,對(duì)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的字段,如上圖。
基于以上場(chǎng)景我們核心要解決2個(gè)問題:
問題1:當(dāng)前光標(biāo)應(yīng)該提示哪些推薦語法類型
目前,開源方案ANTLR-C3引擎就能完美解決我們問題,用戶在編輯器實(shí)時(shí)輸入時(shí),獲取當(dāng)前光標(biāo)位置,實(shí)時(shí)做語法解析,然后基于開源的ANTLR-C3引擎能力結(jié)合ANTLR 生成的AST即可獲取當(dāng)前光標(biāo)位置所需要的語法規(guī)則。
問題2: 獲取當(dāng)前上下文信息以實(shí)現(xiàn)精準(zhǔn)推薦
根據(jù)不同業(yè)務(wù)場(chǎng)景需要采集的上下文信息不同,基于字段推薦的場(chǎng)景,我們需要獲取當(dāng)前光標(biāo)位置處可以推薦的表信息,表別名信息,結(jié)合編輯器能力實(shí)時(shí)獲取表對(duì)應(yīng)的字段信息進(jìn)行字段推薦補(bǔ)全,而上下文信息的采集,我們可以通過ANTLR生成的監(jiān)聽器來實(shí)現(xiàn)。
語法定義
以下我們用ANTLR元語言實(shí)現(xiàn)一段簡(jiǎn)化版的SQL查詢場(chǎng)景的語法規(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"
];
// 包含了所有字面量記號(hào)的名稱
public static readonly literalNames = [
null, "','", "'('", "')'", null, "';'"
];
// 包含為每個(gè)記號(hào)分配的符號(hào)名,這些符號(hào)在生成解析器時(shí)用于標(biāo)識(shí)記號(hào)
public static readonly symbolicNames = [
null, "COMMA", "LEFT_PAREN", "RIGHT_PAREN", "IDENTIFY", "SEMICOLON"
];
// ANTLR 生成的類中的一個(gè)字段,列出了所有定義的規(guī)則
public static readonly ruleNames = [
"COMMA", "LEFT_PAREN", "RIGHT_PAREN", "IDENTIFY", "DIGIT", "LETTER",
"SEMICOLON",
];
2.語法分析器
ANTLR自動(dòng)為每個(gè)規(guī)則生成了一個(gè)解析方法,以下是tableName的 ANTLR 中的解析器方法,具備了處理標(biāo)識(shí)符、可選的別名和錯(cuò)誤處理的能力。
// SQLParse.ts
// ANTLR自動(dòng)生成了一個(gè)解析 SQL 表名的 ANTLR 中的解析器方法,具備了處理標(biāo)識(shí)符、可選的別名和錯(cuò)誤處理的能力
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;
}
自動(dòng)補(bǔ)全
ANTLR4代碼補(bǔ)全核心(antlr4-c3) 是一個(gè)開創(chuàng)性的工具,它為ANTLR4生成的解析器提供了一個(gè)通用的代碼補(bǔ)全解決方案。無論你的項(xiàng)目是處理哪種編程語言或領(lǐng)域特定語言(DSL),只要是基于ANTLR就能夠利用這個(gè)庫實(shí)現(xiàn)精準(zhǔn)的代碼建議和自動(dòng)補(bǔ)全,極大地增強(qiáng)開發(fā)體驗(yàn)。通過antlr4-c3 能力我們通過手動(dòng)配置需要收集的語法規(guī)則,獲取在當(dāng)前光標(biāo)處需要推薦的語法規(guī)則類型。
1.語法規(guī)則
通過ANTLR4工具我們可以自動(dòng)生成Sqllexer.ts詞法解析器,SqlParser.ts語法解析器,SqlParserLister.ts訪問器,SqlParseVisitor.ts監(jiān)聽器,在SqlParser 語法解析器自動(dòng)生成了我們?cè)谡Z法定義中的語法規(guī)則。
preferredRules = new Set([
SqlParser.RULE_tableName,
SqlParser.RULE_columnName,
]);
2.代碼補(bǔ)全
以下我們實(shí)現(xiàn)一套簡(jiǎn)化版的代碼補(bǔ)全能力。
當(dāng)用戶在編輯器實(shí)時(shí)輸入時(shí),調(diào)用getSuggestionAtCaretPosition獲取當(dāng)前語境中需要推薦的信息,包含語法規(guī)則,關(guān)鍵詞,上下文信息,在結(jié)合業(yè)務(wù)層數(shù)據(jù)做自動(dòng)補(bǔ)全,其中包含5個(gè)核心步驟:
- 獲取當(dāng)前語法解析器實(shí)例。
- 獲取當(dāng)前光標(biāo)位置對(duì)應(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()
// 異常場(chǎng)景兼容存在多條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)
}
}
// 獲取對(duì)應(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
};
}
在這里我們簡(jiǎn)化了流程,忽略了很多異常case的處理,自動(dòng)補(bǔ)全的前提是在當(dāng)前語法規(guī)則正確,而在多級(jí)子查詢嵌套場(chǎng)景我們需要考慮到過濾異常QueryStatment, 在當(dāng)前光標(biāo)出最小范圍有效的QueryStatment做補(bǔ)全。這時(shí)候需要配合監(jiān)聽器去做上下文采集做容錯(cuò)性更高的自動(dòng)補(bǔ)全。
上下文分析
圖片
如圖:每個(gè)table都?xì)w屬于一個(gè)QueryStatment表達(dá)式, 查詢中又存在子層級(jí)查詢的嵌套。我們需要通過上下文收集以下信息:
- 每個(gè)查詢語句的信息,包含Position位置信息,記錄當(dāng)前的查詢開始行,結(jié)束行,開始列,結(jié)束列。
- 查詢語句的關(guān)聯(lián)關(guān)系,即記錄當(dāng)前查詢語句父級(jí)查詢語句對(duì)象。
- 表實(shí)體信息包含表名,表位置信息,表別名信息,當(dāng)前表歸屬于那個(gè)查詢語句。
則我們需要監(jiān)聽3個(gè)語法規(guī)則包含QueryStatment, TableName,TableAlias, 采集QueryStatment信息,Table信息同時(shí)將table與當(dāng)前歸屬的QueryStatment做關(guān)聯(lián), 還有與別名信息作配對(duì)關(guān)聯(lián)。這就要求在不同監(jiān)聽器之間的信息需要做共享,上下文信息需要做傳遞和保留。ANTLR常用的3種信息共享方案包含:
- 使用訪問器方法來返回值,
- 使用類成員在事件方法之間共享數(shù)據(jù),
- 在語法定義中使用樹標(biāo)記來存儲(chǔ)信息。
在這里我們使用第二種(在這里我們簡(jiǎn)化了SQL的語法定義,在實(shí)際場(chǎng)景中語法層級(jí)深度和復(fù)雜度遠(yuǎn)比當(dāng)前高,這也使得方案1和3實(shí)際操作起來更麻煩,規(guī)則嵌套層級(jí)深使得方案一和方案三開發(fā)成本和維護(hù)成本更高)
1.監(jiān)聽器(SqlParserLister)
通過ANTLR4工具我們可以自動(dòng)生成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時(shí)采集當(dāng)前Table信息,并關(guān)聯(lián)當(dāng)前QueryStatment。
- 監(jiān)聽退出TableAliasToken時(shí)采集信息,并關(guān)聯(lián)到Table實(shí)體。
- 監(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)入下個(gè)同級(jí)Stmt
pushTableEntity() {} // 采集當(dāng)前表信息,關(guān)聯(lián)當(dāng)前Stmt
pushTableEntity() {} // 采集關(guān)聯(lián)表
enterProgram() {} // 清空重置
getTableEntity() {
return this.TableEntity(ctx)
}
}
在這里我們簡(jiǎn)化了語法定義的規(guī)則便于講解,但在實(shí)際中語法規(guī)則的整體嵌套層級(jí)是很深的,從以下的SparkSql語法定義中我們可以看到右側(cè)聚合的表達(dá)式高達(dá)200+個(gè),單個(gè)表達(dá)式的備選分支最多高達(dá)140+,這也加大了上下文分析采集的復(fù)雜度,即我們無法簡(jiǎn)單的從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()
}
語法校驗(yàn)
ANRLR在生成語法分析器中內(nèi)置了自動(dòng)錯(cuò)誤報(bào)告和恢復(fù)策略,能夠在遇到句法錯(cuò)誤時(shí)自動(dòng)產(chǎn)生錯(cuò)誤消息,為每個(gè)句法錯(cuò)誤產(chǎn)生一條錯(cuò)誤消息。
詞法錯(cuò)誤
常見的詞法錯(cuò)誤包含字符遺漏,詞法錯(cuò)誤。舉個(gè)例子,在spark標(biāo)準(zhǔn)語法定義中 tableName規(guī)則不支持表變量場(chǎng)景(${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
自動(dòng)恢復(fù)機(jī)制
語法分析器不應(yīng)該在遇到非法的成員定義時(shí)結(jié)束,而是應(yīng)盡最大可能匹配到一個(gè)合法的類定義,ANRTL4自動(dòng)錯(cuò)誤恢復(fù)機(jī)制能在語法分析器在發(fā)現(xiàn)語法錯(cuò)誤后還能繼續(xù)進(jìn)行嘗試語法解析和自動(dòng)恢復(fù)。
1.異常捕獲
ANRLT自動(dòng)生成的語法解析器中自動(dòng)為每個(gè)規(guī)則包裹異常捕獲能力,并在catch中嘗試錯(cuò)誤恢復(fù)。
圖片
2.恢復(fù)策略
一般情況下,語法分析器在遇到無法匹配的錯(cuò)誤時(shí)會(huì)嘗試最簡(jiǎn)單的符號(hào)補(bǔ)全和移除來嘗試解析,都不管用時(shí),這時(shí)候就會(huì)用更高階的策略來進(jìn)行恢復(fù)。包括掃描后續(xù)詞法符號(hào)來恢復(fù),從不匹配的詞法符號(hào)中恢復(fù),從子規(guī)則的錯(cuò)誤中恢復(fù),捕獲失敗的語義判定。
雖然ANTLR提供了很多策略來進(jìn)行錯(cuò)誤恢復(fù),但在實(shí)際業(yè)務(wù)場(chǎng)景中,需要結(jié)合考慮語法、語境的復(fù)雜度去權(quán)衡性能與更友好的錯(cuò)誤提示之間的抉擇。在復(fù)雜場(chǎng)景中ANTLR表現(xiàn)并不理想,在一些復(fù)雜語法和語境的情況下解析器在檢測(cè)錯(cuò)誤時(shí)難以做出合理的決策,例如:遞歸和嵌套結(jié)構(gòu)中會(huì)使得錯(cuò)誤恢復(fù)變得很復(fù)雜,導(dǎo)致解析器無法做出合理決策。還有在上下文敏感的語境中,錯(cuò)誤恢復(fù)機(jī)制基本無法提供有效恢復(fù)。
性能
在 ANTLR 4 中,語法復(fù)雜度、語法歧義、語法規(guī)則嵌套深度與預(yù)測(cè)算法的選擇都會(huì)顯著影響解析器的性能和準(zhǔn)確性。Spark SQL語法規(guī)則達(dá)200+,備選分支最高達(dá)140, 嵌套深度達(dá)20+,同時(shí)又存在負(fù)責(zé)循環(huán)嵌套場(chǎng)景, 這也意味著在整個(gè)語法解析,語法錯(cuò)誤的處理過程是很復(fù)雜的,當(dāng)遇到復(fù)雜大SQL量和一片狼籍的語法錯(cuò)誤SQL,會(huì)導(dǎo)致語法解析過程變得緩慢引發(fā)性能問題。目前在性能優(yōu)化上,有以下幾個(gè)方向。
緩存優(yōu)化
在antlr4中詞法解析和語法解析能力和業(yè)務(wù)是完全解耦的,這也意味著底層基于同個(gè)SQL內(nèi)容解析出來的tokens和parserTree都是可以在不同業(yè)務(wù)邏輯應(yīng)用里復(fù)用。我們可以通過緩存tokens,parseTree減少詞法解析和語法解析的損耗。
語法優(yōu)化
通過減少語法樹的層級(jí)和優(yōu)化表達(dá)式減少解析過程中“二義性”的次數(shù),可以加速語法解析的速度,優(yōu)化AST生成性能。合理使用語法定義中用法,例如樹標(biāo)記(用于上下文通信數(shù)據(jù)共享),在語法解析過程中會(huì)為每個(gè)標(biāo)記生成上下文,這也意味著每個(gè)局部結(jié)果都會(huì)保留,會(huì)有更大的內(nèi)存消耗。
預(yù)測(cè)模型選擇
在語法解析中不同預(yù)測(cè)模型的選擇對(duì)解析性能有顯著影響,針對(duì)不同的場(chǎng)景需要評(píng)估時(shí)效性與正確性之間的衡量。
ANTLR4預(yù)測(cè)模型:
https://www.antlr.org/api/Java/org/antlr/v4/runtime/atn/PredictionMode.html
我們可以選擇性價(jià)比更高的SLL預(yù)測(cè)模型作為語法分析策略,結(jié)合定制化的錯(cuò)誤監(jiān)聽器做錯(cuò)誤糾正。
編輯器應(yīng)用
編輯器集成
與MonacoEditor集成流程可查看此文章 https://blog.shizhuang-inc.com/article/MTUzNzY?fromType=personal_blog
輔助編程
1.信息項(xiàng)提示(表,函數(shù),字段)
圖片
圖片
圖片
2.自動(dòng)補(bǔ)全(庫,表,字段,語法)
圖片
圖片
圖片
五、大模型下的SQL編輯器應(yīng)用
隨著大模型的蓬勃發(fā)展,在數(shù)據(jù)產(chǎn)品中的應(yīng)用也逐步得到了驗(yàn)證和落地,目前,Galaxy還沒有接入Copilot, 內(nèi)部暫時(shí)還沒有基于SQL的Copilot。業(yè)界較成熟的是阿里云的Dataworks, DataWorks于2023年推出了Copilot 產(chǎn)品, 核心2個(gè)方向,一個(gè)方向是智能 SQL 編程助手,輔助 SQL 編程,支持 NL2SQL 及 SQL 代碼補(bǔ)全;另一個(gè)方向是 AI Agent,提供 LUI(自然語言用戶界面),以提升產(chǎn)品功能操作的便捷性和用戶體驗(yàn)。
NL2SQL應(yīng)用場(chǎng)景
基于SQL的Copilot一般在以下幾個(gè)應(yīng)用場(chǎng)景比較深入和廣泛的落地效果:簡(jiǎn)單數(shù)據(jù)查詢,SQL 優(yōu)化與轉(zhuǎn)換,SQL 語法查詢與講解, 函數(shù)查詢,功能咨詢,注釋生成,SQL 解釋,SQL 一鍵糾錯(cuò)。
NL2SQL自動(dòng)補(bǔ)全
代碼補(bǔ)全是編程類 Copilot 的主要場(chǎng)景和能力,單市場(chǎng)上主流的編程類 Copilot 對(duì) SQL 支持的好的并不多見。眾所周知,SQL 代碼補(bǔ)全比其他高級(jí)語言的代碼補(bǔ)全更具挑戰(zhàn)性,主要原因有以下幾個(gè)方面:
- 上下文和環(huán)境的依賴性:SQL 代碼不是獨(dú)立存在的,而是依賴于數(shù)據(jù)表的元數(shù)據(jù)信息以及表與表之間的關(guān)聯(lián)關(guān)系。
- SQL 語義多樣性:實(shí)現(xiàn)同一種查詢結(jié)果,可以有多種 SQL 寫法,如何實(shí)現(xiàn)“最佳”寫法存在挑戰(zhàn)。
- 語法簡(jiǎn)潔但高度專業(yè)化:SQL 語法簡(jiǎn)潔但每一個(gè)關(guān)鍵字、函數(shù)或語法都有特定的含義,大模型要準(zhǔn)確理解這些得通過針對(duì)性的訓(xùn)練學(xué)習(xí)。
- 執(zhí)行計(jì)劃和性能考量: 這跟數(shù)據(jù)庫底層的執(zhí)行計(jì)劃有關(guān),需要考慮如何書寫才能使 SQL 的性能最優(yōu)。
- 數(shù)據(jù)庫特異性:市面上不同的數(shù)據(jù)庫往往存在不同的 SQL 方言,存在差異,針對(duì)這種差異性我們要投入大量時(shí)間做 SQL 數(shù)據(jù)集準(zhǔn)備、數(shù)據(jù)標(biāo)注、模型微調(diào)。
- 高度業(yè)務(wù)相關(guān)性:SQL 語句通常與特定業(yè)務(wù)高度相關(guān),比如一個(gè)指標(biāo)存在特定的計(jì)算口徑,這是與公司業(yè)務(wù)相關(guān),通用的大模型也無法提前學(xué)習(xí)。
目前較成熟的代碼補(bǔ)全核心場(chǎng)景主要在有規(guī)律的代碼連續(xù)推薦場(chǎng)景(例如:字段、字段別名推薦,注釋推薦、分區(qū)字段推薦、Group by 字段推薦,上下文自動(dòng)聯(lián)想推薦等)。
六、總結(jié)
通過SQL引擎能力建設(shè)我們?cè)贕alaxy數(shù)據(jù)研發(fā)IDE上支持了個(gè)性化詞法規(guī)則定制能力,包含字段別名支持中文, 表變量等場(chǎng)景, 同時(shí)通過語法解析和監(jiān)聽器能力,支持實(shí)時(shí)識(shí)別各類的語法規(guī)則,包含表,函數(shù),字段等做輔助編程提示和做精準(zhǔn)化的庫,表,字段代碼補(bǔ)全和推薦。
后續(xù)我們?nèi)悦媾R很大的挑戰(zhàn),在非專業(yè)的數(shù)據(jù)開發(fā)背景、復(fù)雜的業(yè)務(wù)定制需求、語言定義的復(fù)雜性和嵌套深度等因素共同導(dǎo)致了解析器的開發(fā)難度。目前,在語法校驗(yàn)自動(dòng)糾錯(cuò)提示上,雖然ANTLR的提供了自動(dòng)錯(cuò)誤恢復(fù)機(jī)制但整體表現(xiàn)并不理想,后續(xù)2個(gè)方向,第一,接入大模型的能力。第二,從基礎(chǔ)語法定義上進(jìn)行重構(gòu),減少語法歧義和層級(jí)優(yōu)化。為了應(yīng)對(duì)這些挑戰(zhàn),我們需要加強(qiáng)對(duì) ANTLR 和 Spark SQL語言,數(shù)據(jù)處理的理解,以便順利使用和擴(kuò)展解析器。
參考資料
- ANTLR
- ANTLR4-C3
- DataWorks Copilot:大模型時(shí)代數(shù)據(jù)開發(fā)的新范式
- ANTLR4權(quán)威指南 - [美] 特恩斯·帕爾 著