由于工做关系,须要开发支持众多方言的 SQL 编辑器,因此复习了一下编译原理相关知识。前端
相比编译原理专家,咱们只须要了解部分编译原理便可实现 SQL 编辑器,因此这是一篇写给前端的编译原理文章。git
解析 SQL 能够分为以下四步:github
词法分析就像刀削面的过程,拿着一段字符串(面条)一端不断下刀,当面条被切完也就完成了词法分析,因此词法分析是 字符串 -> 一堆字符段 的过程。算法
流程很简单,难点就在下刀的分寸了,每次砍几厘米呢?sql
回到词法分析,为了准备切分,咱们须要定义 SQL 的 Token 有哪些类型,即 Token 分类。typescript
SQL 的 Token 能够分为以下几类:编辑器
SELECT
、CREATE
)。+
、-
、>=
)。(
、CASE
)。?
)。${variable}
)。能够看到,在词法分析阶段,咱们的 Tokens 不须要关心关键词是什么,只要识别是否是关键词便可,由于关键词的辨认会留到语法分析时处理。涉及到语意处理就要考虑上下文,而这都不是词法分析阶段要考虑的。函数
一样,操做符、空格、文本、占位符等构成了 SQL 语句的其余部分,最后经过开闭合标志好比左括号和右括号,让 SQL 支持子语句。spa
再强调一次,虽然 SQL 支持子语句,但并非放在任何位置都是合理的,其余类型 Token 同理,可是词法分析不须要考虑 Token 是否合理,只要切分便可。rest
像大多数语言同样,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 函数。
最后,执行这个主函数,分词就完成了!
分词比较简单,到这里就所有结束了。后面即将进入深水区语法分析,敬请期待。
讨论地址是: 精读《手写 SQL 编译器 - 词法分析》 · Issue #93 · dt-fe/weekly
若是你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。