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