关于Databend源码-token解析

一、 databend自定义token实现

举个例子: 在databend中将sql进行token化生成最终的AST

// 使用logos进行lexer
let tokens = tokenize_sql(case).unwrap();
let backtrace = Backtrace::new();
// 生成sql的AST
let stmts = parse_sql(&tokens, &backtrace).unwrap();
// 
for stmt in stmts {
      writeln!(file, "---------- Output ---------").unwrap();
      writeln!(file, "{}", stmt).unwrap();
      writeln!(file, "---------- AST ------------").unwrap();
      writeln!(file, "{:#?}", stmt).unwrap();
      writeln!(file, "\n").unwrap();
}

在databend中将一个sql进行token化少不了的struct Tokenizer,主要是结合databend中定义token类型:enum TokenKind,底层使用logos来完成最终的词法解析。

pub struct Tokenizer<'a> {
    // 要被token化的原始sql
    source: &'a str,  
    // 用于token化的lexer:使用logos来进行词法解析              
    lexer: Lexer<'a, TokenKind>,
   // 
    eoi: bool,
}

看一下databend自身结合logos定义的一些token类型:TokenKind详情
主要是通过#[derive(Logos)] 使用logos;

#[derive(Logos, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TokenKind {
    // 省略代码
    // 主要涉及:空白类、标识符、块类型、转义类、符号类、关键字类等
}

而logos进行词法解析的入口TokenKind::lexer(sql), logos是使用过程宏(proc_macro_derive)的方式,来为不同的自定义token化提供了lexer操作;进而得到每个token:kind(类型)/source(原始内容)/span(在原始内容串的范围),具体代码如下:

impl<'a> Iterator for Tokenizer<'a> {
    // 得到databend里面定义Token
    type Item = Result<Token<'a>>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.lexer.next() { // 在logos中Lexer实现了Iterator
            Some(kind) if kind == TokenKind::Error => { // 遍历token类型
                let rest_span = Token { // 不满意定义token规则的错误:看TokenKind中Error
                    source: self.source,
                    kind: TokenKind::Error,
                    span: self.lexer.span().start..self.source.len(),
                };
                Some(Err(ErrorCode::SyntaxException(rest_span.display_error( // 语法解析错误
                    "unable to recognize the rest tokens".to_string(),
                ))))
            }
            Some(kind) => Some(Ok(Token { // token解析正常
                source: self.source,
                kind,
                span: self.lexer.span(),
            })),
            None if !self.eoi => { // ??? 解析结束标识
                self.eoi = true;
                Some(Ok(Token {
                    source: self.source,
                    kind: TokenKind::EOI,
                    span: (self.lexer.span().end)..(self.lexer.span().end),
                }))
            }
            None => None,  // 没有可遍历的内容
        }
    }
}

再看看databend中定义的Token:

#[derive(Clone, PartialEq)]
pub struct Token<'a> {
    pub source: &'a str,      // token在被解析字符串的原始内容
    pub kind: TokenKind,   // token类型
    pub span: Span,          // token在解析字符串中的范围
}

完成token化: 主要是完成databend中自定义的token化

 // Tokenizer本身也实现Iterator trait,可以使用collect完成转为Result<Vec<Token>>
 Tokenizer::new(sql).collect::<Result<Vec<_>>>()

接下来就是将已经token化的Token生成Statement:使用nom文本解析器来完成该部分的;

/// Parse a SQL string into `Statement`s.
pub fn parse_sql<'a>(
    sql_tokens: &'a [Token<'a>],
    backtrace: &'a Backtrace<'a>,
) -> Result<Vec<Statement<'a>>> {
    match statements(Input(sql_tokens, backtrace)) {
        Ok((rest, stmts)) if rest[0].kind == TokenKind::EOI => Ok(stmts), // 结束标识
        Ok((rest, _)) => Err(ErrorCode::SyntaxException( // 语法解析异常
            rest[0].display_error("unable to parse rest of the sql".to_string()),
        )),
        Err(nom::Err::Error(err) | nom::Err::Failure(err)) => { // 解析异常
            Err(ErrorCode::SyntaxException(err.display_error(())))
        }
        Err(nom::Err::Incomplete(_)) => unreachable!(),
    }
}

最终输出databend本身的sql ast:

---------- Input ----------
show tables
---------- Output ---------
SHOW TABLES
---------- AST ------------
ShowTables {
    database: None,
    full: false,
    limit: None,
}

二、关于logos部分

  • 准备: cargo.toml中引入logos
[dependencies]
logos = "0.12.0"
  • 使用:自定义Token
#[derive(Logos, Debug, PartialEq)]
enum Token {
    #[token("fast")]
    Fast,

    #[token(".")]
    Period,

    #[regex("[a-zA-Z]+")]
    Text,

    #[error]
    #[regex(r"[ \t\n\f]+", logos::skip)]
    Error,

    #[regex("[0-9]+", |lex| lex.slice().parse())]
    #[regex("[0-9]+k", kilo)]
    #[regex("[0-9]+m", mega)]
    Number(u64),
}
  • 测试
// 解析简单的文本
#[test]
fn test_lexer_token_demo () {
    let mut tokens = Token::lexer("Create ridiculously fast Lexers.");
    assert_eq!(tokens.next(), Some(Token::Text));
    assert_eq!(tokens.span(), 0..6);
    assert_eq!(tokens.slice(), "Create");

    assert_eq!(tokens.next(), Some(Token::Text));
    assert_eq!(tokens.span(), 7..19);
    assert_eq!(tokens.slice(), "ridiculously");

    assert_eq!(tokens.next(), Some(Token::Fast));
    assert_eq!(tokens.span(), 20..24);
    assert_eq!(tokens.slice(), "fast");

    assert_eq!(tokens.next(), Some(Token::Text));
    assert_eq!(tokens.span(), 25..31);
    assert_eq!(tokens.slice(), "Lexers");

    assert_eq!(tokens.next(), Some(Token::Period));
    assert_eq!(tokens.span(), 31..32);
    assert_eq!(tokens.slice(), ".");

    assert_eq!(tokens.next(), None);
}

在自定义token中,定义回调函数:

// 使用自定义回调函数
#[regex("[0-9]+", |lex| lex.slice().parse())]
#[regex("[0-9]+k", kilo)]   
#[regex("[0-9]+m", mega)]
Number(u64),

回调函数如下:

fn kilo(lex: &mut Lexer<Token>) -> Option<u64> {
    eprintln!("==execute kilo==");
    let slice = lex.slice();
    let n: u64 = slice[..slice.len() - 1].parse().ok()?;
    Some(n * 1_000)
}

fn mega(lex: &mut Lexer<Token>) -> Option<u64> {
    eprintln!("==execute mega==");
    let slice = lex.slice();
    let n: u64 = slice[..slice.len() - 1].parse().ok()?;
    Some(n * 1_000_000)
}

用例:

#[test]
fn test_callback_lexer_demo() {
    let mut lexer = Token::lexer("5 42k 75m");
    assert_eq!(lexer.next(), Some(Token::Number(5)));
    assert_eq!(lexer.span(), 0..1);
    assert_eq!(lexer.slice(), "5");

    assert_eq!(lexer.next(), Some(Token::Number(42_000)));
    assert_eq!(lexer.span(), 2..5);
    assert_eq!(lexer.slice(), "42k");

    assert_eq!(lexer.next(), Some(Token::Number(75_000_000)));
    assert_eq!(lexer.span(), 6..9);
    assert_eq!(lexer.slice(), "75m");

    assert_eq!(lexer.next(), None);
}

三、引用

logos
nom

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容