精读《手写 SQL 编译器 - 词法分析》

1 引言

因为工作关系,需要开发支持众多方言的 SQL 编辑器,所以复习了一下编译原理相关知识。

相比编译原理专家,我们只需要了解部分编译原理即可实现 SQL 编辑器,所以这是一篇写给前端的编译原理文章。

解析 SQL 可以分为如下四步:

  1. 词法分析,将 SQL 字符串拆分成包含关键词识别的字符段(Tokens)。
  2. 语法分析,利用自顶向下或自底向上的算法,将 Tokens 解析为 AST,可以手动,也可以自动。
  3. 错误检测、恢复、提示推断,都需要利用语法分析产生的 AST。
  4. 语义分析,做完这一步就可以执行 SQL 语句了,不过对前端而言,不需要深入到这一步,可以跳过。

2 精读

词法分析就像刀削面的过程,拿着一段字符串(面条)一端不断下刀,当面条被切完也就完成了词法分析,所以词法分析是 字符串 -> 一堆字符段 的过程。

流程很简单,难点就在下刀的分寸了,每次砍几厘米呢?

回到词法分析,为了准备切分,我们需要定义 SQL 的 Token 有哪些类型,即 Token 分类。

Token 分类

SQL 的 Token 可以分为如下几类:

  • 注释。
  • 关键字(SELECTCREATE)。
  • 操作符(+->=)。
  • 开闭合标志((CASE)。
  • 占位符(?)。
  • 空格。
  • 引号包裹的文本、数字、字段。
  • 方言语法(${variable})。

可以看到,在词法分析阶段,我们的 Tokens 不需要关心关键词是什么,只要识别是不是关键词即可,因为关键词的辨认会留到语法分析时处理。涉及到语意处理就要考虑上下文,而这都不是词法分析阶段要考虑的。

同样,操作符、空格、文本、占位符等构成了 SQL 语句的其他部分,最后通过开闭合标志比如左括号和右括号,让 SQL 支持子语句。

再强调一次,虽然 SQL 支持子语句,但并不是放在任何位置都是合理的,其他类型 Token 同理,但是词法分析不需要考虑 Token 是否合理,只要切分即可。

用正则逐段分词

像大多数语言一样,SQL 为了方便人类阅读,采用从左到右的书写方式,因此分词方向也从左到右

我们为每个 Token 类型写一个函数,比如匹配空格的匹配函数:

function getTokenWhitespace(restStr: string) {
  const matches = restStr.match(/^(\s+)/);

  if (matches) {
    return { type, value: matches[1] };
  }
}

restStr 表示掐去头部剩下的 SQL 字符串,所有匹配函数都拿 restStr 进行匹配,已经匹配的不需要再处理。

通过正则 /^(\s+)/ 匹配到第一个以空格开头的空格(读起来有点别扭),匹配时必须保证以你要匹配的内容开头,而且只匹配一次,这样才不会在切词时发生遗漏。

同理匹配 /**/ 类型注释时,也能通过正则轻而易举的实现:

function getTokenBlockComment(restStr: string) {
  const matches = restStr.match(/^(\/\*[^]*?(?:\*\/|$))/);

  if (matches) {
    return { type, value: matches[1] };
  }
}

其中 (?:\*/\) 表示匹配到以 */ 结尾处,而 (?:\*\/|$) 后面的 |$ 表示或者直接匹配到结尾(如果一直没有遇到 */ 那后面全部当作注释)。

所以只要 Token 分类得当,并且能为每一个分类写一个头匹配正则,分词功能就实现了 90%。

方言拓展

为了支持某些方言,需要从分词时就开始做考虑。比如 ${variable} 作为一种变量用法时,我们需要在普通字段的正则匹配中,加入一项 \$\{[a-zA-Z0-9]+\} 匹配。

如果要支持纯中文作为字段,可以再补充 |\u4e00-\u9fa5

分词主流程

有了一个个分词函数,再补充一个不断匹配、切割字符串、再匹配的主函数即可,这一步更简单:

while (sqlStr) {
  token =
    getTokenWhitespace(sqlStr, token) | getTokenBlockComment(sqlStr, token);

  sqlStr = sqlStr.substring(token.value.length);

  tokens.push(token);
}

上面的函数每取一次 Token,都将取到的 Token 长度丢掉,继续匹配剩下的字符串,直到字符串被切分完为止。

有些特殊情况需要拿到上次的 Token 才能判断下一个 Token 该如何切割,所以将 Token 传给每一个下一步 Match 函数。

最后,执行这个主函数,分词就完成了!

3 总结

分词比较简单,到这里就全部结束了。后面即将进入深水区语法分析,敬请期待。

4 更多讨论

讨论地址是:精读《手写 SQL 编译器 - 词法分析》 · Issue #93 · dt-fe/weekly

如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。

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

推荐阅读更多精彩内容