databend中nom的應(yīng)用

databend中sql解析器是基于nom來完成的,接下來結(jié)合一個樣例:explain語句

let explain = map_res( // 需要完成sql輸入解析的Parser以及將解析完成后的輸出提供給閉包函數(shù)
        rule! {
            EXPLAIN ~ ( AST | SYNTAX | PIPELINE | JOIN | GRAPH | FRAGMENTS | RAW | OPTIMIZED | MEMO )? ~ #statement
        }, // 解析器
        |(_, opt_kind, statement)| { // 用于使用nom的parser解Input后的結(jié)果當(dāng)成輸入?yún)?shù),提供給當(dāng)前的閉包函數(shù)
            Ok(Statement::Explain {
                kind: match opt_kind.map(|token| token.kind) {
                    Some(TokenKind::AST) => {
                        let formatted_stmt = format_statement(statement.stmt.clone())
                            .map_err(|_| ErrorKind::Other("invalid statement"))?;
                        ExplainKind::Ast(formatted_stmt)
                    }
                    Some(TokenKind::SYNTAX) => {
                        let pretty_stmt = pretty_statement(statement.stmt.clone(), 10)
                            .map_err(|_| ErrorKind::Other("invalid statement"))?;
                        ExplainKind::Syntax(pretty_stmt)
                    }
                    Some(TokenKind::PIPELINE) => ExplainKind::Pipeline,
                    Some(TokenKind::JOIN) => ExplainKind::JOIN,
                    Some(TokenKind::GRAPH) => ExplainKind::Graph,
                    Some(TokenKind::FRAGMENTS) => ExplainKind::Fragments,
                    Some(TokenKind::RAW) => ExplainKind::Raw,
                    Some(TokenKind::OPTIMIZED) => ExplainKind::Optimized,
                    Some(TokenKind::MEMO) => ExplainKind::Memo("".to_string()),
                    None => ExplainKind::Plan,
                    _ => unreachable!(),
                },
                query: Box::new(statement.stmt),
            })
        },
    );

這里先介紹下nom實現(xiàn)解析文本的

關(guān)于nom實現(xiàn)解析文本: 一種是基于macro來完成的,另一種是function來完成的(目前最新的nom版本比較支持的)

直接引入官方的一個demo

use nom::character::complete::digit1;
use nom::combinator::map_res;

// 這里的組合器包括digit1用來判斷內(nèi)容是否為數(shù)字,當(dāng)滿足是數(shù)字字符串時,則繼續(xù)執(zhí)行后續(xù)的閉包進行類型轉(zhuǎn)換
let mut parse = map_res(digit1, |s: &str| s.parse::<u8>());

// the parser will convert the result of digit1 to a number
assert_eq!(parse("123"), Ok(("", 123)));

// this will fail if digit1 fails
assert_eq!(parse("abc"), Err(Err::Error(("abc", ErrorKind::Digit))));

// this will fail if the mapped function fails (a `u8` is too small to hold `123456`)
assert_eq!(parse("123456"), Err(Err::Error(("123456", ErrorKind::MapRes))));

關(guān)于nom中map_res的定義

pub fn map_res<I: Clone, O1, O2, E: FromExternalError<I, E2>, E2, F, G>(
    parser: F,   // 解析器
    f: G         // 針對解析處理后的內(nèi)容進行后續(xù)處理
) -> impl FnMut(I) -> IResult<I, O2, E>
where
    F: Parser<I, O1, E>,
    G: FnMut(O1) -> Result<O2, E2>,

不過在databend中map_res被改寫了:去掉了FromExternalError,其中被改寫的還有:separated_list1、separated_list0(將原有的函數(shù)聲明中的error定義剔除)
比如在databend中map_res的定義

pub fn map_res<'a, O1, O2, F, G>(
    mut parser: F,  // 使用的parser,一般是nom中的parser
    mut f: G,       // 閉包函數(shù)
) -> impl FnMut(Input<'a>) -> IResult<'a, O2>
where
    F: nom::Parser<Input<'a>, O1, Error<'a>>,
    G: FnMut(O1) -> Result<O2, ErrorKind>,

在databend中如何進行sql解析

map_res( // 需要完成sql輸入解析的Parser以及將解析完成后的輸出提供給閉包函數(shù)
    rule! {
            EXPLAIN ~ ( AST | SYNTAX | PIPELINE | JOIN | GRAPH | FRAGMENTS | RAW | OPTIMIZED | MEMO )? ~ #statement
    }, // 解析器
   | (_, opt_kind, statement)| {
      // 省略代碼
   },
)

map_res來完成sql解析,主要有兩部分:

  • rule!(解析器集) 通過解析使用 BNF 來定義 SQL 的語法規(guī)則,
  • |(_, opt_kind, statement)|{} 閉包 用于對解析器處理后的內(nèi)容進行處理,獲取當(dāng)前statement的kind
sql解析

通過將用戶輸入的sql字符串提供給詞法解析器生成token序列,接著將token序列提供給語法解析器,并結(jié)合指定的sql方言/語義最終生成statement;
在語法解析過程會進行sql語法檢查,若是整個sql沒有問題就會輸出抽象語法樹AST.

// 樣例sql
select a + 1 from t

// 通過語法解析器輸出抽象語法樹(AST)
SelectStatement {
    projection: Expr::BinaryOp {
        op: Op::Plus,
        args: [
            Expr::ColumnRef("a"),
            Expr::Constant(Scalar::Int(1))
        ]
    }
    from: "t",
}

sql解析分成兩個階段:Tokenize(分詞) 和 Parsing(解析)階段

  • Tokenize分詞token化
    主要是將用戶輸入的sql字符串切分成具有獨立語義的單元,稱為token; 一般是通過正則表達式從字符串中識別相鄰有意義的部分,比如關(guān)鍵字、變量名
    、常量、操作符
    類似如下定義
Ident = [_a-zA-Z][_$a-zA-Z0-9]*
Number = [0-9]+
Plus = +
SELECT = SELECT
FROM = FROM

按照從上到下順序嘗試匹配正則表達式,并一直重復(fù)這個步驟最終得到token序列
比如 select a+1 from t,token化序列如下

[
    Keyword(SELECT),
    Ident(a),
    BinOp(+),
    Number(1),
    Keyword(FROM),
    Ident(t),
]

接下來就是parsing階段,首先先來了解下BNF來定義sql語法規(guī)則

關(guān)于BNF定義sql語法規(guī)則

主要來描述了有意義的Token如何進行組合

<select_statement> ::= SELECT <expr> FROM <ident>
<expr> ::= <number>
         | <ident>
         | <expr> <op> <expr>
<op> ::= + | - | * | /
  • Parsing解析statement
    在databend中使用BNF來定義sql語法規(guī)則,將上面生成的token序列輸入到解析器,進而生成AST
SelectStatement {
    projection: Expr::BinaryOp {
        op: Op::Plus,
        args: [
            Expr::ColumnRef("a"),
            Expr::Constant(Scalar::Int(1))
        ]
    }
    from: "t",
}

自此sql statement就形成了。

databend的sql token和parsing階段的實現(xiàn)

在databend中使用logos和nom來分別完成token化和解析:
1、使用logos庫來完成sql字符串token化, 詳情見databend sql字符串token化
在databend中定義了TokenKind的enum枚舉類型,每個TokenKind對應(yīng)一個特定的Token: 例如關(guān)鍵字、變量名、常量和操作符。并且每個TokenKind都會有
一個單獨的正則表達式進行匹配,以確保準確地從輸入字符串中提取token,將TokenKind和Logos庫整合,最終編譯一個高效的狀態(tài)機和跳表來達到超過手寫Tokenizer
的極快的分詞性能

#[allow(non_camel_case_types)]
#[derive(Logos)]
pub enum TokenKind {
    #[regex(r"[ \t\r\n\f]+", logos::skip)]
    Whitespace,

    #[regex(r#"[_a-zA-Z][_$a-zA-Z0-9]*"#)]
    Ident,
    #[regex(r"[0-9]+")]
    Number,

    #[token("+")]
    Plus,

    #[token("SELECT", ignore(ascii_case))]
    SELECT,
    #[token("FROM", ignore(ascii_case))]
    FROM,
}

將“select a+1 from t”進行token化,如下

[
    Token { kind: TokenKind::Select, text: "SELECT", span: 0..6 },
    Token { kind: TokenKind::Ident, text: "a", span: 7..8 },
    Token { kind: TokenKind::Plus, text: "+", span: 9..10 },
    Token { kind: TokenKind::Number, text: "1", span: 11..12 },
    Token { kind: TokenKind::From, text: "from", span: 13..17 },
    Token { kind: TokenKind::Ident, text: "t", span: 18..19 },
]

除了 TokenKind 的定義,Token 本身還包括了一個重要的屬性——span。span 記錄了 Token 在原始字符串中的起始和結(jié)束位置。

2、使用nom實現(xiàn)遞歸下降sql解析器,同時nom自身語法規(guī)則構(gòu)建過程相對繁瑣,所以定義了rule!()過程宏,并使用BNF來定義語法規(guī)則, 自動生成nom解析器

// databend中rule過程宏
rule! {
    EXPLAIN ~ ( AST | SYNTAX | PIPELINE | JOIN | GRAPH | FRAGMENTS | RAW | OPTIMIZED | MEMO )? ~ #statement
}

// 底層使用nom中的nom_rule::rule!來完成的
#[macro_export]
macro_rules! rule {
    ($($tt:tt)*) => { nom_rule::rule!(
        $crate::match_text,
        $crate::match_token,
        $($tt)*)
    }
}

在databend中使用nom來實現(xiàn)遞歸下降sql解析器,其中有兩個比較主要的parser組成
1、Terminal Parser:是最基本的解析器,用于匹配特定的TokenKind的Token。在databend中定義了match_token()和match_text()這兩個Terminal Parser

fn match_text(text: &str) -> impl FnMut(&[Token]) -> IResult<&[Token], Token> {
    satisfy(|token: &Token| token.text == text)(i)
}

fn match_token(kind: TokenKind) -> impl FnMut(&[Token]) -> IResult<&[Token], Token> {
    satisfy(|token: &Token| token.kind == kind)(i)
}

2、 Combinator parser:允許將多個小的解析器組合成較大的解析器。也是構(gòu)建復(fù)雜邏輯的基礎(chǔ)。以下是一些常見的組合器:

 -* tuple(a, b, c):會讓多個 parser 按順序排列(例如先是 a,然后是 b,接著是 c)。它們需要按照預(yù)期的順序逐個成功才算作最后的成功。
 -* alt(a, b, c):嘗試多個 parser 分支(例如 a,b 或 c),直到滿足第一個成功條件。如果全部失敗,解析器將報告錯誤。
 -* many0(a):貪婪地循環(huán)調(diào)用相同的 parser。如果無法繼續(xù),這個解析器將成功返回已匹配的 Token 序列??赡転榭?。
 -* many1(a):貪婪地循環(huán)調(diào)用相同的 parser,但至少需要匹配成功一次。如果至少匹配成功一次,解析器會返回 Token 序列。
 -* opt(a):允許 parser 失敗。當(dāng) parser 失敗時,將返回 `None`。成功時將返回成功匹配的結(jié)果。

舉個例子:

 <select_statement> ::= SELECT <expr>+ FROM <ident>

接下來使用nom來組裝語法樹: 使用match_token識別關(guān)鍵字;使用many1來實現(xiàn)循環(huán)多個 <expr>+。

tuple((
    match_token(SELECT),
    many1(expr),
    match_token(FROM),
    match_token(Ident),
))

將‘select a+1 from t’進行token化和解析,結(jié)果如下:

Query {
    span: Some(0..17,),
    with: None,
    body: Select(
            SelectStmt {
                span: Some(0..17,),
                hints: None,
                distinct: false,
                select_list: [
                    AliasedExpr {
                        expr: BinaryOp {
                                span: Some(8..9,),
                                op: Plus,
                                left: ColumnRef {
                                    span: Some(7..8,),
                                    database: None,
                                    table: None,
                                    column: Name(
                                        Identifier {
                                            name: \"a\",
                                            quote: None,
                                            span: Some(7..8,),
                                        },
                                    ),
                                },
                                right: Literal {
                                      span: Some(9..10,),
                                      lit: UInt64(1,),
                                },
                        },
                        alias: None,
                    },
                ],
                from: [
                    Table {
                        span: Some(16..17,),
                        catalog: None,
                        database: None,
                        table: Identifier {
                            name: \"t\",
                            quote: None,
                            span: Some(16..17, ),
                        },
                        alias: None,
                        travel_point: None,
                        pivot: None,
                        unpivot: None,
                    },
                ],
                selection: None,
                group_by: None,
                having: None,
                window_list: None,
            },
    ),
    order_by: [],
    limit: [],
    offset: None,
    ignore_result: false,
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容