自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

用Go構(gòu)建一個SQL解析器

開發(fā) 開發(fā)工具
在本文中,小編將向大家簡單介紹如何在 Go 中構(gòu)造 LL(1) 解析器,并應(yīng)用于解析SQL查詢。希望大家能用 Go 對簡單的解析器算法有一個了解和簡單應(yīng)用

摘要

本文旨在簡單介紹如何在 Go 中構(gòu)造 LL(1) 解析器,在本例中用于解析SQL查詢。

為了簡單起見,我們將處理子選擇、函數(shù)、復(fù)雜嵌套表達式和所有 SQL 風格都支持的其他特性。這些特性與我們將要使用的策略緊密相關(guān)。

1分鐘理論

一個解析器包含兩個部分:

  • 詞法分析:也就是“Tokeniser”
  • 語法分析:AST 的創(chuàng)建

詞法分析

讓我們用例子來定義一下。“Tokenising”以下查詢:

  1. SELECT id, name FROM 'users.csv' 

表示提取構(gòu)成此查詢的“tokens”。tokeniser 的結(jié)果像這樣:

  1. []string{"SELECT", "id", ",", "name", "FROM", "'users.csv'"} 

語法分析

這部分實際上是我們查看 tokens 的地方,確保它們有意義并解析它們來構(gòu)造出一些結(jié)構(gòu)體,以一種對將要使用它的應(yīng)用程序更方便的方式表示查詢(例如,用于執(zhí)行查詢,用顏色高亮顯示它)。在這一步之后,我們會得到這樣的結(jié)果:

  1. query{ 
  2.     Type: "Select", 
  3.     TableName: "users.csv", 
  4.     Fields: ["id", "name"], 

有很多原因可能會導(dǎo)致解析失敗,所以同時執(zhí)行這兩個步驟可能會比較方便,并在出現(xiàn)錯誤時可以立即停止。

策略

我們將定義一個像這樣的解析器:

  1. type parser struct { 
  2.   sql             string        // The query to parse 
  3.   i               int           // Where we are in the query 
  4.   query           query.Query   // The "query struct" we'll build 
  5.   step            step          // What's this? Read on... 
  6.  
  7. // Main function that returns the "query struct" or an error 
  8. func (p *parser) Parse() (query.Query, error) {} 
  9.  
  10. // A "look-ahead" function that returns the next token to parse 
  11. func (p *parser) peek() (string) {} 
  12.  
  13. // same as peek(), but advancing our "i" index 
  14. func (p *parser) pop() (string) {} 

直觀地說,我們首先要做的是“peek() ***個 token”。在基礎(chǔ)的SQL語法中,只有幾個有效的初始 token:SELECT、UPDATE、DELETE等;其他的都是錯誤的。代碼像這樣:

  1. switch strings.ToUpper(parser.peek()) { 
  2.  
  3. case "SELECT": 
  4.   parser.query.type = "SELECT" // start building the "query struct" 
  5.   parser.pop() 
  6.   // TODO continue with SELECT query parsing... 
  7.  
  8. case "UPDATE": 
  9.   // TODO handle UPDATE 
  10.  
  11. // TODO other cases... 
  12.  
  13. default: 
  14.   return parser.query, fmt.Errorf("invalid query type") 
  15.  

我們基本上可以填寫 TODO 和讓它跑起來!然而,聰明的讀者會發(fā)現(xiàn),解析整個 SELECT 查詢的代碼很快會變得混亂,而且我們有許多類型的查詢需要解析。所以我們需要一些結(jié)構(gòu)。

有限狀態(tài)機

FSMs 是一個非常有趣的話題,但我們來這里不是為了講這個,所以不會深入介紹。讓我們只關(guān)注我們需要什么。

在我們的解析過程中,在任何給定的點(與其說“點”,不如稱其稱為“節(jié)點”),只有少數(shù) token 是有效的,在找到這些 token 之后,我們將進入新的節(jié)點,其中不同的 token 是有效的,以此類推,直到完成對查詢的解析。我們可以將這些節(jié)點關(guān)系可視化為有向圖:

點轉(zhuǎn)換可以用一個更簡單的表來定義,但是:

我們可以直接將這個表轉(zhuǎn)換成一個非常大的 switch 語句。我們將使用那個我們之前定義過的 parser.step 屬性:

  1. func (p *parser) Parse() (query.Query, error) { 
  2.   parser.step = stepType // initial step 
  3.  
  4.   for parser.i < len(parser.sql) { 
  5.     nextToken :parser.peek() 
  6.  
  7.     switch parser.step { 
  8.     case stepType: 
  9.       switch nextToken { 
  10.       case UPDATE: 
  11.         parser.query.type = "UPDATE" 
  12.         parser.step = stepUpdateTable 
  13.  
  14.       // TODO cases of other query types 
  15.       } 
  16.     case stepUpdateSet: 
  17.       // ... 
  18.     case stepUpdateField: 
  19.       // ... 
  20.     case stepUpdateComma: 
  21.       // ... 
  22.     } 
  23.  
  24.     parser.pop() 
  25.   } 
  26.  
  27.   return parser.query, nil 

好了!注意,有些步驟可能會有條件地循環(huán)回以前的步驟,比如 SELECT 字段定義上的逗號。這種策略對于基本的解析器非常適用。然而,隨著語法變得復(fù)雜,狀態(tài)的數(shù)量將急劇增加,因此編寫起來可能會變得單調(diào)乏味。我建議在編寫代碼時進行測試;更多信息請見下文。

Peek() 實現(xiàn)

記住,我們需要同時實現(xiàn) peek() 和 pop() 。因為它們幾乎是一樣的,所以我們用一個輔助函數(shù)來保持代碼整潔。此外,pop() 應(yīng)該進一步推進索引,以避免取到空格。

  1. func (p *parser) peek() string { 
  2.   peeked, _ :p.peekWithLength() 
  3.   return peeked 
  4.  
  5. func (p *parser) pop() string { 
  6.   peeked, len :p.peekWithLength() 
  7.   p.i += len 
  8.   p.popWhitespace() 
  9.   return peeked 
  10.  
  11. func (p *parser) popWhitespace() { 
  12.   for ; p.i < len(p.sql) && p.sql[p.i] == ' '; p.i++ { 
  13.   } 

下面是我們可能想要得到的令牌列表:

  1. var reservedWords = []string{ 
  2.   "(", ")", ">=", "<=", "!=", ",", "=", ">", "<", 
  3.   "SELECT", "INSERT INTO", "VALUES", "UPDATE", 
  4.   "DELETE FROM", "WHERE", "FROM", "SET", 

除此之外,我們可能會遇到帶引號的字符串或純標識符(例如字段名)。下面是一個完整的 peekWithLength() 實現(xiàn):

  1. func (p *parser) peekWithLength() (string, int) { 
  2.   if p.i >= len(p.sql) { 
  3.     return "", 0 
  4.   } 
  5.   for _, rWord :range reservedWords { 
  6.     token :p.sql[p.i:min(len(p.sql), p.i+len(rWord))] 
  7.     upToken :strings.ToUpper(token) 
  8.     if upToken == rWord { 
  9.       return upToken, len(upToken) 
  10.     } 
  11.   } 
  12.   if p.sql[p.i] == '\'' { // Quoted string 
  13.     return p.peekQuotedStringWithLength() 
  14.   } 
  15.   return p.peekIdentifierWithLength() 

其余的函數(shù)都很簡單,留給讀者作為練習。如果您感興趣,可以查看 github 的鏈接,其中包含完整的源代碼實現(xiàn)。

最終驗證

解析器可能會在得到完整的查詢定義之前找到字符串的末尾。實現(xiàn)一個 parser.validate() 函數(shù)可能是一個好主意,該函數(shù)查看生成的“query”結(jié)構(gòu),如果它不完整或錯誤,則返回一個錯誤。

測試Go的表格驅(qū)動測試模式非常適合我們的情況:

  1. type testCase struct { 
  2.   Name     string         // description of the test 
  3.   SQL      string         // input sql e.g. "SELECT a FROM 'b'" 
  4.   Expected query.Query    // expected resulting "query" struct 
  5.   Err      error          // expected error result 

測試實例:

  1. ts := []testCase{ 
  2.     { 
  3.       Name:     "empty query fails", 
  4.       SQL:      "", 
  5.       Expected: query.Query{}, 
  6.       Err:      fmt.Errorf("query type cannot be empty"), 
  7.     }, 
  8.     { 
  9.       Name:     "SELECT without FROM fails", 
  10.       SQL:      "SELECT", 
  11.       Expected: query.Query{Type: query.Select}, 
  12.       Err:      fmt.Errorf("table name cannot be empty"), 
  13.     }, 
  14.     ... 

像這樣測試測試用例:

  1. for _, tc :range ts { 
  2.     t.Run(tc.Name, func(t *testing.T) { 
  3.       actual, err :Parse(tc.SQL) 
  4.       if tc.Err != nil && err == nil { 
  5.         t.Errorf("Error should have been %v", tc.Err) 
  6.       } 
  7.       if tc.Err == nil && err != nil { 
  8.         t.Errorf("Error should have been nil but was %v", err) 
  9.       } 
  10.       if tc.Err != nil && err != nil { 
  11.         require.Equal(t, tc.Err, err, "Unexpected error") 
  12.       } 
  13.       if len(actual) > 0 { 
  14.         require.Equal(t, tc.Expected, actual[0], 
  15.           "Query didn't match expectation") 
  16.       } 
  17.     }) 
  18.   } 

我使用 verify 是因為當查詢結(jié)構(gòu)不匹配時,它提供了一個 diff 輸出。

深入理解

這個實驗非常適合:

  • 學(xué)習 LL(1) 解析器算法
  • 自定義解析無依賴關(guān)系的簡單語法

然而,這種方法可能會變得單調(diào)乏味,而且有一定的局限性??紤]一下如何解析任意復(fù)雜的復(fù)合表達式(例如 sqrt(a) =(1 *(2 + 3)))。

要獲得更強大的解析模型,請查看解析器組合符。goyacc 是一個流行的Go實現(xiàn)。

下面是完整的解析器地址(或點擊閱讀原文查看):http://github.com/marianogappa/sqlparser

 

責任編輯:趙寧寧 來源: 51CTO專欄
相關(guān)推薦

2022-10-20 11:00:52

SQL解析器

2017-02-14 10:20:43

Java Class解析器

2014-05-15 09:45:58

Python解析器

2023-12-30 13:33:36

Python解析器JSON

2022-06-28 08:17:10

JSON性能反射

2011-11-28 15:40:52

wiresharkRDP解析器

2015-02-10 14:32:37

XSS漏洞XSS

2023-07-25 14:24:33

元素JSX解析器

2023-05-10 08:05:41

GoWeb應(yīng)用

2024-01-08 08:36:29

HTTPGo代理服務(wù)器

2014-10-15 11:01:02

Web應(yīng)用測試應(yīng)用

2022-09-20 08:43:37

Go編程語言Web

2009-03-19 09:26:05

RSS解析器MagpieRSS

2009-06-19 11:42:09

Scala計算器解析

2011-04-01 16:16:27

JavaScript

2021-08-27 12:16:34

fastjarJAR文件Java

2018-03-19 17:40:10

Python區(qū)塊鏈

2020-12-02 10:13:45

JacksonJDK解析器

2021-04-15 08:55:51

Go struc代碼

2021-04-25 08:58:00

Go拍照云盤
點贊
收藏

51CTO技術(shù)棧公眾號