控制台查詢編輯器中的多個查詢
Rockset Console 的查詢編輯器允許用戶在集合上鍵入和運行查詢。然而,直到現在,在編輯器中輸入的任何內容都作為單個查詢運行和解析。這意味着,對於用戶來說,在我們的編輯器中切換多個查詢並不容易。他們將不得不注釋掉他們不想運行的查詢,或者將所有查詢保存在一個單獨的文本文件中,並一次復制他們想要運行的查詢。
為了更容易地在多個查詢之間切換,我們決定在我們的編輯器中允許多個查詢,用分號分隔,一個 SQL 語句終止符。為了實現這一點,我們的編輯器必須了解查詢的開始和結束位置。我們天真的最初的方法是用分號(或者分號 + 換行)分割編輯器的整個文本的字符串,並將由此分割產生的每個字符串理解為一個單獨的查詢。當然,這種幼稚的方法並不能完全證明,因為分號可能存在於字符串中或 SQL 查詢中的注釋中。我們不想在這樣的分號上拆分。
對分號的幼稚拆分還不夠好。我們的編輯器必須了解 SQL 注釋和字符串,才能知道其中一個分號何時出現在其中,而不是用來表示語句的結束。那時,為了實現對編輯器文本的更透徹的理解,我們決定在前端使用 ANTLR。
ANTLR - SQL 查詢的基本理解
ANTLR 是一個強大的語言解析工具。根據指定的語法(一組規則),ANTLR 生成詞法分析器和解析器,它們一起可以根據輸入(在我們的示例中為 SQL 字符串)構建一棵樹,以及一個可以在訪問該樹時執行邏輯的偵聽器。事實上,我們已經在后端使用ANTLR來解析SQL語句,我們從中產生的樹是用來理解和執行用戶SQL語句的!
語法
讓我們從頭開始。下載ANTLR后,我們需要制作語法文件。例如,查看Presto 的 SQL ANTLR 語法。Rockset 的后端查詢解析語法其實就是這個 Presto 語法的修改版。但是,對於前端的查詢拆分,我們最感興趣的是注釋和字符串的語法規則:
STRING : '\'' ( ~'\'' | '\'\'' )* '\'' ; COMMENT : SIMPLE_COMMENT | BRACKETED_COMMENT ; fragment SIMPLE_COMMENT : '--' ~[\r\n]* '\r'? '\n'? ; fragment BRACKETED_COMMENT : '/*' .*? '*/' ;
這里發生的事情的關鍵是指定哪些字符串字符構成規則的序列,即字符串或注釋。這些語法規則完全從我們的后端語法文件中復制而來,因此我們可以確保前端的查詢拆分語法以與后端系統完全相同的方式理解注釋和字符串。看看我們其余的語法規則:
queriesText
: statement* EOF
;
statement
: ';'* (CHAR | STRING | COMMENT)+ ';'* ; CHAR : ~';' ;
我們可以看到整個查詢文本只是被定義為一系列 0 或更多語句,並且這些語句被定義為字符(在我們的例子中,除了分號之外的任何東西)、注釋和位於分號之間的字符串的混合。這里的關鍵是分號可以放在注釋和字符串中,並作為語句的一部分。
這是包含上述所有規則的完整語法文件。
建造樹
ANTLR 的強大功能是從這個語法生成文件,我們可以用它來做我們的邏輯。
antlr4 -Dlanguage=JavaScript <GrammarFileName>.g4
是用於指定我們的目標語言是 JavaScript 的命令,運行它會生成大量文件供我們在 JavaScript 中使用,包括詞法分析器、解析器和偵聽器。有關更多詳細信息,請參閱此文檔。讓我們看看我們最終使用所有這些文件編寫的函數,以了解它們在做什么。
從'antlr4'導入*作為 antlr4 ; | |
從'./QuerySeparationGrammarLexer'導入{ QuerySeparationGrammarLexer } ; | |
從'./QuerySeparationGrammarParser'導入{ QuerySeparationGrammarParser } ; | |
從'./QuerySeparationGrammarListener'導入{ QuerySeparationGrammarListener } ; | |
從'./CustomListener'導入{ CustomListener } ; | |
導出 const SplitQueries = (輸入) => { | |
const chars = 新的 antlr4 。的InputStream (輸入); | |
const lexer = new QuerySeparationGrammarLexer (字符); | |
const 令牌 = 新的 antlr4 。CommonTokenStream (詞法分析器); | |
const parser = new QuerySeparationGrammarParser ( tokens ) ; | |
解析器。buildParseTrees = true ; | |
const 樹 = 解析器。查詢文本( ) ; // 我要走的那棵樹 | |
常量 結果 = [ ] ; | |
const listener = new CustomListener ( result ) ; // 自定義偵聽器建立在空結果數組上 | |
螞蟻金服4 。樹。解析樹行者。默認。走(聽者, 樹); | |
返回 監聽器。結果; // 此函數返回開始和停止索引。 | |
} ; |
在這里,我們可以看到詞法分析器和解析器幫助我們使用我們的語法從字符串輸入構建一棵樹。以‘[statement1][statement2][statement3]’
字符串輸入為例,其中每一個[statement]
都是字符串中匹配語句的語法規則的一部分,例如'abc/*comment;;*/def;'
. 包含三個語句的輸入字符串將被解析為如下所示的樹:
樹的每個節點都基於輸入字符串中出現的語法規則,並且該節點的子節點是該節點包含的組件(基於其他語法規則)。根節點基於 queryText 語法規則,它作為整個字符串存在於輸入字符串中。所以根節點代表整個輸入字符串。它的孩子是組成這個節點的組件。我們可以從querysText 語法規則中看出,一個querysText 由一系列語句組成。所以在樹中,querysText 根節點的子節點是每個語句的節點。同樣,每個語句節點的子節點是該語句的組件,即該語句中的特定字符、字符串和注釋。
這里要注意的重要一點是,根據我們的語法,輸入樹中存在的語句被成功解析為樹的單獨節點。輸入字符串中語句的分離是我們最初的目標。
聆聽者(走在樹上)
現在我們有了一個可行的樹,是時候對其進行邏輯處理了。聽眾幫助我們做到這一點。回到 SplitQueries 函數,我們可以看到我們通過使用 CustomListener 遍歷樹來“使用”樹。
從'antlr4'導入*作為 antlr4 ; | |
從'./QuerySeparationGrammarLexer'導入{ QuerySeparationGrammarLexer } ; | |
從'./QuerySeparationGrammarParser'導入{ QuerySeparationGrammarParser } ; | |
從'./QuerySeparationGrammarListener'導入{ QuerySeparationGrammarListener } ; | |
從'./CustomListener'導入{ CustomListener } ; | |
導出 const SplitQueries = (輸入) => { | |
const chars = 新的 antlr4 。的InputStream (輸入); | |
const lexer = new QuerySeparationGrammarLexer (字符); | |
const 令牌 = 新的 antlr4 。CommonTokenStream (詞法分析器); | |
const parser = new QuerySeparationGrammarParser ( tokens ) ; | |
解析器。buildParseTrees = true ; | |
const 樹 = 解析器。查詢文本( ) ; // 我要走的那棵樹 | |
常量 結果 = [ ] ; | |
const listener = new CustomListener ( result ) ; // 自定義偵聽器建立在空結果數組上 | |
螞蟻金服4 。樹。解析樹行者。默認。走(聽者, 樹); | |
返回 監聽器。結果; // 此函數返回開始和停止索引。 | |
} ; |
CustomListener 是我們編寫的文件,繼承自生成的偵聽器文件。生成的基本偵聽器為位於樹中的每個解析節點提供一個enter
和exit
函數。正是這些函數在樹的遍歷中執行。它是這樣工作的:
在樹的遍歷過程中,進入/退出每個節點會調用該節點對應的進入/退出函數,在我們的例子中是 CustomListener(我們用來遍歷樹的監聽器)的監聽器。生成的監聽器基類中的進入/退出函數是空的,不做任何事情,所以如果我們想在行走過程中進入或退出節點時執行邏輯,我們需要在CustomListener中覆蓋這些函數,而我們的自定義函數將被執行。
我們需要在 CustomListener 中覆蓋的唯一函數是exitStatement
函數,該函數在退出樹中的語句節點時調用。請記住,由於我們的語法,樹中的語句節點准確地代表了我們輸入文本中的語句。退出語句節點后,我們希望獲取該語句在輸入文本字符串中的位置。幸運的是,偵聽器函數將ctx
節點的參數作為參數,其中包含很多信息,但對我們特別有用,ctx 包含輸入字符串中該節點的開始和停止索引。
因此,該exitStatement
函數的實現變得非常簡單:在退出時記錄並存儲每個語句的開始和停止索引,以便在遍歷結束時,我們擁有所有開始和停止索引的集合,告訴我們在哪里每個查詢語句都以輸入字符串開始和結束。
這是我們的 CustomListener,具有以下exitStatement
功能:
var QuerySeparationGrammarListener = require ( './QuerySeparationGrammarListener' ) 。QuerySeparationGrammarListener ; | |
var CustomListener = 函數(結果) { | |
這個。結果 = 結果; | |
QuerySeparationGrammarListener 。調用(這個); // 繼承默認監聽器 | |
返回 這個; | |
} ; | |
// 繼續繼承默認監聽器 | |
自定義監聽器。原型 = 對象。創建(QuerySeparationGrammarListener 。原型); | |
自定義監聽器。原型。構造函數 = 自定義 監聽器; | |
// 覆蓋默認監聽器行為 | |
自定義監聽器。原型。退出語句 = 函數(ctx ) { | |
這個。結果。推([ ctx 。開始。開始, ctx 。停止。停止] ); // 存儲每條語句的開始和停止索引 | |
} ; | |
出口。自定義監聽器 = 自定義 監聽器; |
SplitQueries 函數
結束我們對 ANTLR 的使用,讓我們最后看一下SplitQueries
函數。首先,我們構建定義簡單 SQL 語句和其中的一些基本組件的語法。然后,根據這個語法,我們生成詞法分析器、解析器和偵聽器。在我們的SplitQueries
函數中,我們接受一個字符串輸入,並使用詞法分析器和解析器使用語法規則從中構建一棵樹。仍然在SplitQueries
函數內部,然后我們使用 customListener 遍歷樹,它記錄每個語句的開始和停止索引。SplitQueries
返回這組索引。
從我們的輸入文本中,我們得到了我們單獨的 SQL 語句的索引位置的輸出,這些 SQL 語句以分號結尾,但可以將它們包含在它們的注釋和字符串中,如 ANTLR 語法所定義的那樣。
使用 SplitQueries 和光標位置選擇用戶查詢
將 ANTLR 語法和解析功能打包到該SplitQueries
函數中后,是時候通過使用此功能從控制台查詢編輯器中的多個查詢的文本中選擇單個用戶查詢來結束該項目了。我們為此所做的是:
- 對用戶的輸入文本使用 SplitQueries 以獲取該文本中每個查詢的開始和停止索引。
- 選擇開始和停止索引包含用戶光標位置的查詢,這意味着我們選擇用戶單擊的查詢。(在某些特殊情況下,比如選擇一個空行,我們會選擇一個相鄰的查詢。)
通過這樣做,我們能夠選擇用分號分隔的用戶查詢,即使該查詢在其字符串或注釋中有分號,這都是因為SplitQueries
在計算查詢邊界時我們的 ANTLR 語法規則會通知它。
結論
我們很高興我們在前端使用 ANTLR 進行嚴格的語法分析。嘗試在Rockset Console 中輸入多個查詢以查看該功能的工作原理!隨着 ANTLR 現在融入我們的前端,我們也期待在控制台中進行不同類型的解析。
JavaScript 中的 ANTLR 資源:
這是Rockset Console 中使用的 ANTLR/js 代碼的副本。這包括語法、生成的文件、CustomListener 和 SplitQueries 函數。隨意修改語法並使用以下方法重新生成文件:
antlr4 -Dlanguage=JavaScript QuerySeparationGrammar.g4
您可以在自己的字符串輸入上運行 SplitQueries 腳本!