SQL 解析與執行流程

KaiwuDB發表於2024-01-08

一、前言

在先前的技術部落格中,我們已經詳細介紹過資料庫的 parser 模組與執行流程:使用者輸入的 SQL 語句透過詞法解析器生成 token,再透過語法分析器生成抽象語法樹(AST),經過 AST 生成對應的 planNode,最後執行 planNode。本期部落格我們將以新增語法為例,重點介紹一條 SQL 語句需要歷經的流程,以及如何自定義 SQL 語句和功能。

二、新增 SQL 語法

KaiwuDB 是透過 goyacc 解析 sql.y 中的程式碼生成 AST,而我們想要新增一個語法,則需要在 sql.y 中新增對應的關鍵字以及對應的語法解析規則,最後在程式碼中新增對應的語法節點以及功能實現。我們以建立時序資料庫的語法 CREATE TS DATABASE xxx 為例,講解如何新增新 SQL 語句,以及該語句的執行過程。

1. 定義新的關鍵字

詞法分析器是透過一個個關鍵字解析整條語句,所以第一步我們需要將整條語句涉及到的所有關鍵字定義完畢。

搜尋 pkg/sql/parser/sql.y 檔案,在檔案中找到 %token(這個代表著定義的關鍵字),我們可以在這裡看到已經有 CREATE 和 DATABASE 關鍵字,所以需要新增一個 TS 關鍵字。

Go
......
%token <str> TS
......

加好關鍵字後,我們還需定義其是保留關鍵字或是非保留關鍵字,非保留關鍵字可以作為識別符號使用並在使用時需要加上雙引號。所以,我們需要將 TS 加到非保留關鍵字裡中。

Go
unreserved_keyword:
......
| TS
......

新增完畢後,詞法分析器即可解析整條語句的所有關鍵字,接下來我們需要定義一個新的語法規則,使得語法解析器可以處理這條新的語句。

2. 新增新的語法規則

新的語法首先要給它定義一個型別。在 sql.y 中,一條 SQL 語句的型別都是 %type <tree.Statement> 。

Go
......
%type <tree.Statement> create_ts_database_stmt
......

然後將該語法放到語法 case 列表中,這條語法屬於 create_ddl_stmt 的一部分,我們將其放在 create_ddl_stmt 下方即可。

Go
create_ddl_stmt:
......
| create_ts_database_stmt  // EXTEND WITH HELP: CREATE TS_DATABASE
......

接下來需要為該語法新增對應的語法規則和幫助資訊:

Go
// %Help: CREATE TS_DATABASE - create a new ts database
// %Category: DDL
// %Text: CREATE TS_DATABASE <name>
create_ts_database_stmt:
  CREATE TS DATABASE database_name{
    ......
  }
| CREATE TS DATABASE error // SHOW HELP: CREATE DATABASE

到這裡,整個 parser 部分就可以識別這條新的語法並提示相應資訊,但現在還沒有新增具體的語法操作,所以整個語法還不能完全執行。

3. 新增執行語法操作

在解析器可以成功解析語法後,我們需要新增對應的語義,來讓整條 SQL 語句執行。而這一步就是生成一個抽象語法樹(AST),將語句資訊從 parser 階段傳到執行階段。

在上文中我們將 create_ts_database_stmt 新增為 tree.Statement 型別,所以我們還需要實現 tree.Statement 型別的介面,其後還需要實現以下幾個方法:

  • fmt.Stringer
  • NodeFormatter
  • StatementType()
  • StatementTag()
  • StatOp()
  • StatTargetType()

為此,所以首先要定義一個結構體,可以作為整條語句解析的返回值,用以實現上述的幾種方法。對於我們想要新增的語句,可以複用原來的 CreateDatabase 結構體並在其中新增一個欄位 EngineType 用來代表是否為時序資料庫。該結構體已經實現了以上幾種方法,但我們新增了新的語法,所以要在 Format 方法中將新的語法 Format 方式新增進去。

Go
// Format implements the NodeFormatter interface.
func (node *CreateDatabase) Format(ctx *FmtCtx) {
  ctx.WriteString("CREATE ")
  if node.EngineType == EngineTypeTimeseries {
   ctx.WriteString("TS ")
  }
  ......
}

接下來我們將 parser 部分補全,讓它返回一個對應的 CreateDatabase 節點。

Go
// %Help: CREATE TS_DATABASE - create a new ts database
// %Category: DDL
// %Text: CREATE TS_DATABASE <name>
create_ts_database_stmt:
  CREATE TS DATABASE database_name{
    $$.val = &tree.CreateDatabase{
      Name: tree.Name($4),
      EngineType: 1,
    }
  }
| CREATE TS DATABASE error // SHOW HELP: CREATE DATABASE

至此,整條語句已經可以成功識別並執行。但由於我們是複用已有的 CreateDatabase 結構,所以執行流程還需要對應的修改。如果是新增一個新的結構體,我們需要在 plan.go 中新加一個 planNode,用於生成執行計劃,

Go
var _ planNode = &createDatabaseNode{}

planNode 也有以下幾個介面需要實現:

  • startExec(params runParams)
  • Next(params runParams)
  • Values()
  • Close(ctx context.Context)

在 buildOpaque 新增一個 case,用於執行時識別 AST 結構,生成對應的 planNode。

Go
......
switch n := stmt.(type) {
  case *tree.CreateDatabase:
    plan, err = p.CreateDatabase(ctx, n)
  ......

目前我們已有對應的 createDatabaseNode ,所以無需再新增。而在 CreateDatabase 中需要我們將 AST 轉成 planNode,並需要做出語義上的檢查與限制。

最後一步就是要定義如何執行整條語句,在 startExec 方法中,透過構建好的 planNode 去實現我們所需的語法功能。

Go
func (n *createDatabaseNode) startExec(params runParams) error {
    ......
}

至此,新增的該語法功能已實現。

來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70027415/viewspace-3003124/,如需轉載,請註明出處,否則將追究法律責任。

相關文章