用 Go 構建一個 SQL 解析器
在本文中,小編將向大家簡單介紹如何在 Go 中構造 LL(1) 解析器,並應用於解析SQL查詢。希望大家能用 Go 對簡單的解析器演算法有一個瞭解和簡單應用。
摘要
本文旨在簡單介紹如何在 Go 中構造 LL(1) 解析器,在本例中用於解析SQL查詢。
為了簡單起見,我們將處理子選擇、函式、複雜巢狀表示式和所有 SQL 風格都支援的其他特性。這些特性與我們將要使用的策略緊密相關。
1分鐘理論
一個解析器包含兩個部分:
詞法分析:也就是“Tokeniser”
語法分析:AST 的建立
詞法分析
讓我們用例子來定義一下。“Tokenising”以下查詢:
SELECT id, name FROM 'users.csv'
表示提取構成此查詢的“tokens”。tokeniser 的結果像這樣:
[]string{"SELECT", "id", ",", "name", "FROM", "'users.csv'"}
語法分析
這部分實際上是我們檢視 tokens 的地方,確保它們有意義並解析它們來構造出一些結構體,以一種對將要使用它的應用程式更方便的方式表示查詢(例如,用於執行查詢,用顏色高亮顯示它)。在這一步之後,我們會得到這樣的結果:
query{
Type: "Select",
TableName: "users.csv",
Fields: ["id", "name"],
}
有很多原因可能會導致解析失敗,所以同時執行這兩個步驟可能會比較方便,並在出現錯誤時可以立即停止。
策略
我們將定義一個像這樣的解析器:
type parser struct {
sql string // The query to parse
i int // Where we are in the query
query query.Query // The "query struct" we'll build
step step // What's this? Read on...
}
// Main function that returns the "query struct" or an error
func (p *parser) Parse() (query.Query, error) {}
// A "look-ahead" function that returns the next token to parse
func (p *parser) peek() (string) {}
// same as peek(), but advancing our "i" index
func (p *parser) pop() (string) {}
直觀地說,我們首先要做的是“peek() 第一個 token”。在基礎的SQL語法中,只有幾個有效的初始 token:SELECT、UPDATE、DELETE等;其他的都是錯誤的。程式碼像這樣:
switch strings.ToUpper(parser.peek()) {
case "SELECT":
parser.query.type = "SELECT" // start building the "query struct"
parser.pop()
// TODO continue with SELECT query parsing...
case "UPDATE":
// TODO handle UPDATE
// TODO other cases...
default:
return parser.query, fmt.Errorf("invalid query type")
}
我們基本上可以填寫 TODO 和讓它跑起來!然而,聰明的讀者會發現,解析整個 SELECT 查詢的程式碼很快會變得混亂,而且我們有許多型別的查詢需要解析。所以我們需要一些結構。
有限狀態機
FSMs 是一個非常有趣的話題,但我們來這裡不是為了講這個,所以不會深入介紹。讓我們只關注我們需要什麼。
在我們的解析過程中,在任何給定的點(與其說“點”,不如稱其稱為“節點”),只有少數 token 是有效的,在找到這些 token 之後,我們將進入新的節點,其中不同的 token 是有效的,以此類推,直到完成對查詢的解析。我們可以將這些節點關係視覺化為有向圖:
點轉換可以用一個更簡單的表來定義,但是:
我們可以直接將這個錶轉換成一個非常大的 switch 語句。我們將使用那個我們之前定義過的 parser.step 屬性:
func (p *parser) Parse() (query.Query, error) {
parser.step = stepType // initial step
for parser.i < len(parser.sql) {
nextToken := parser.peek()
switch parser.step {
case stepType:
switch nextToken {
case UPDATE:
parser.query.type = "UPDATE"
parser.step = stepUpdateTable
// TODO cases of other query types
}
case stepUpdateSet:
// ...
case stepUpdateField:
// ...
case stepUpdateComma:
// ...
}
parser.pop()
}
return parser.query, nil
}
好了!注意,有些步驟可能會有條件地迴圈回以前的步驟,比如 SELECT 欄位定義上的逗號。這種策略對於基本的解析器非常適用。然而,隨著語法變得複雜,狀態的數量將急劇增加,因此編寫起來可能會變得單調乏味。我建議在編寫程式碼時進行測試;更多資訊請見下文。
Peek() 實現
記住,我們需要同時實現 peek() 和 pop() 。因為它們幾乎是一樣的,所以我們用一個輔助函式來保持程式碼整潔。此外,pop() 應該進一步推進索引,以避免取到空格。
func (p *parser) peek() string {
peeked, _ := p.peekWithLength()
return peeked
}
func (p *parser) pop() string {
peeked, len := p.peekWithLength()
p.i += len
p.popWhitespace()
return peeked
}
func (p *parser) popWhitespace() {
for ; p.i < len(p.sql) && p.sql[p.i] == ' '; p.i++ {
}
}
下面是我們可能想要得到的令牌列表:
var reservedWords = []string{
"(", ")", ">=", "<=", "!=", ",", "=", ">", "<",
"SELECT", "INSERT INTO", "VALUES", "UPDATE",
"DELETE FROM", "WHERE", "FROM", "SET",
}
除此之外,我們可能會遇到帶引號的字串或純識別符號(例如欄位名)。下面是一個完整的 peekWithLength() 實現:
func (p *parser) peekWithLength() (string, int) {
if p.i >= len(p.sql) {
return "", 0
}
for _, rWord := range reservedWords {
token := p.sql[p.i:min(len(p.sql), p.i+len(rWord))]
upToken := strings.ToUpper(token)
if upToken == rWord {
return upToken, len(upToken)
}
}
if p.sql[p.i] == '\'' { // Quoted string
return p.peekQuotedStringWithLength()
}
return p.peekIdentifierWithLength()
}
其餘的函式都很簡單,留給讀者作為練習。如果您感興趣,可以檢視 github 的連結,其中包含完整的原始碼實現。
最終驗證
解析器可能會在得到完整的查詢定義之前找到字串的末尾。實現一個 parser.validate() 函式可能是一個好主意,該函式檢視生成的“query”結構,如果它不完整或錯誤,則返回一個錯誤。
測試Go的表格驅動測試模式非常適合我們的情況:
type testCase struct {
Name string // description of the test
SQL string // input sql e.g. "SELECT a FROM 'b'"
Expected query.Query // expected resulting "query" struct
Err error // expected error result
}
測試例項:
ts := []testCase{
{
Name: "empty query fails",
SQL: "",
Expected: query.Query{},
Err: fmt.Errorf("query type cannot be empty"),
},
{
Name: "SELECT without FROM fails",
SQL: "SELECT",
Expected: query.Query{Type: query.Select},
Err: fmt.Errorf("table name cannot be empty"),
},
...
像這樣測試測試用例:
for _, tc := range ts {
t.Run(tc.Name, func(t *testing.T) {
actual, err := Parse(tc.SQL)
if tc.Err != nil && err == nil {
t.Errorf("Error should have been %v", tc.Err)
}
if tc.Err == nil && err != nil {
t.Errorf("Error should have been nil but was %v", err)
}
if tc.Err != nil && err != nil {
require.Equal(t, tc.Err, err, "Unexpected error")
}
if len(actual) > 0 {
require.Equal(t, tc.Expected, actual[0],
"Query didn't match expectation")
}
})
}
我使用 verify 是因為當查詢結構不匹配時,它提供了一個 diff 輸出。
深入理解
這個實驗非常適合:
學習 LL(1) 解析器演算法
自定義解析無依賴關係的簡單語法
然而,這種方法可能會變得單調乏味,而且有一定的侷限性。考慮一下如何解析任意複雜的複合表示式(例如 sqrt(a) =(1 *(2 + 3)))。
要獲得更強大的解析模型,請檢視解析器組合符。goyacc 是一個流行的Go實現。
下面是完整的解析器地址:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555491/viewspace-2650041/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 用 Go 構建一個區塊鏈 -- Part 7: 網路Go區塊鏈
- 使用golang+antlr4構建一個自己的語言解析器(二)Golang
- 從零到一:用Go語言構建你的第一個Web服務GoWeb
- 如何構建一個WEB同構應用Web
- 用 GIN 構建一個 WEB 服務Web
- 構建一個Flowable命令列應用命令列
- 使用golang+antlr4構建一個自己的語言解析器(完結篇)Golang
- 使用SlashDB,Go和Vue構建一個簡單的時間表應用程式GoVue
- 使用Go語言構建一個解釋型語言Go
- 如何用Go構建GoGo
- 構建一個 @synchronizedsynchronized
- 1. 構建您的第一個應用
- 使⽤用Requests庫構建⼀一個HTTP請求HTTP
- 一個可以自動構建CURD控制器的go-apiGoAPI
- 手寫一個解析器
- Go通過cobra快速構建命令列應用Go命令列
- 用Go構建區塊鏈——6.交易2Go區塊鏈
- 構建一個即時訊息應用(二):OAuthOAuth
- React Native 學習指南(一) - 構建第一個應用React Native
- go 基於gin-vue 構建一套mvc開發應用GoVueMVC
- 用多層架構構建一個簡易留言本 (轉)架構
- Odin —— 用於構建命令列應用的 Go 開發包命令列Go
- 如何構建一個系統?
- [譯] 用javascript實現一門程式語言-寫一個解析器JavaScript
- 構建 Go 應用 docker 映象的十八種姿勢GoDocker
- 從零構建一個基於Docker的Laravel應用DockerLaravel
- 用 OpenStack Designate 構建一個 DNS 即服務(DNSaaS)DNS
- Electron構建一個檔案瀏覽器應用(二)瀏覽器
- “Hello,Jetpack”:構建您的第一個Jetpack應用程式Jetpack
- 用Java構建一個簡單的WebSocket聊天室JavaWeb
- 用 Uno Platform 構建一個 Kanban-style Todo AppPlatformAPP
- FISCO BCOS | 構建第一個區塊鏈應用程式區塊鏈
- 用 Python 構建一個極小的區塊鏈Python區塊鏈
- SQL解析器詳解SQL
- c# 怎樣能寫個sql的解析器C#SQL
- 一個使用Go語言和現代Web技術構建跨平臺桌面應用程式開源專案GoWeb
- 構建你的第一個Flutter視訊通話應用Flutter
- 全棧工程師如何快速構建一個Web應用全棧工程師Web