精读《手写 SQL 编译器 - 毛病提醒》

1 弁言

《精读《手写 SQL 编译器 - 毛病提醒》》

编译器除了天生语法树以外,还要在输入涌现毛病时给出适当的提醒。

比方当用户输入 select (name,这是个未完成的 SQL 语句,我们的目的是提醒出这个语句未完成,并给出后续的发起: ) - + % / * . (

2 精读

剖析一个 SQL 语句,现将 query 字符串转成 Token 数组,再组织文法树剖析,那末能够涌现毛病的状况有两种:

  1. 语句毛病。
  2. 文法未完成。

给出毛病提醒的第一步是推断毛病发作。

《精读《手写 SQL 编译器 - 毛病提醒》》

经由历程这张 Token 婚配历程图能够发明,当深度优先遍历文法节点时,婚配胜利后才会返回父元素继承往下走。而当走到父元素没有根节点了才算婚配胜利;当尝试 Chance 时没有机会了,就是毛病发作的机遇。

所以我们只需找到末了一个婚配胜利的节点,再依据末了胜利与否,以及搜刮出下一个能够节点,就可以晓得毛病范例以及给出发起了。

function onMatchNode(matchNode, store) {
  const matchResult = matchNode.run(store.scanner);

  if (!matchResult.match) {
    tryChances(matchNode, store);
  } else {
    const restTokenCount = store.scanner.getRestTokenCount();
    if (matchNode.matching.type !== "loose") {
      if (!lastMatch) {
        lastMatch = {
          matchNode,
          token: matchResult.token,
          restTokenCount
        };
      }
    }

    callParentNode(matchNode, store, matchResult.token);
  }
}

所以在运转语法剖析器时,在碰到婚配节点(MatchNode)时,假如婚配胜利,就记录下这个节点,如许我们最终会找到末了一个婚配胜利的节点:lastMatch

以后经由历程 findNextMatchNodes 函数找到下一个能够的引荐节点列表,作为毛病恢复的发起。

findNextMatchNodes 函数会依据某个节点,找出下一节点一切能够 Tokens 列表,这个函数背面文章再特地引见,或许你也能够先浏览
源码.

语句毛病

也就是任何一个 Token 婚配失利。比方:

select * from table_name as table1 error_string;

这里 error_string 就是冗余的语句。

经由历程语法剖析器剖析,能够获得实行失利的效果,然后经由历程 findNextMatchNodes 函数,我们能够获得下面剖析效果:

《精读《手写 SQL 编译器 - 毛病提醒》》

能够看到,顺序推断出了 error_string 这个 Token 属于毛病范例,同时给出发起,能够将 error_string 替换成这 14 个发起字符串中恣意一个,都能使语句准确。

之所以失利范例推断为毛病范例,是由于查找了这个准确 Token table1 背面另有一个没有被运用的 error_string,所以毛病归类是 wrong

注重,这里给出的是下一个 Token 发起,而不是悉数 Token 发起,因而引荐了 where 示意 “或许背面跟一个完全的 where 语句”。

文法未完成

和语句毛病差别,这类毛病一切输入的单词都是准确的,但却没有写完。比方:

select *

经由历程语法剖析器剖析,能够获得实行失利的效果,然后经由历程 findNextMatchNodes 函数,我们能够获得下面剖析效果:

《精读《手写 SQL 编译器 - 毛病提醒》》

能够看到,顺序推断出了 * 这个 Token 属于未完成的毛病范例,发起在背面补全这 14 个发起字符串中恣意一个。比较轻易联想到的是 where,但也能够是恣意子文法的未完成状况,比方背面补充 , 继承填写字段,或许直接跟一个单词示意别号,或许先输入 as 再跟别号。

之所以失利范例推断为未完成,是由于末了一个准确 Token * 以后没有 Token 了,但语句剖析失利,那只有一个缘由,就是语句为写完,因而毛病归类是 inComplete

找到最易读的毛病范例

在一最先有提到,我们只需找到末了一个婚配胜利的节点,就可以够顺藤摸瓜找到毛病缘由以及提醒,但末了一个胜利的节点能够和我们人类直觉相违犯。举下面这个例子:

select a from b where a = '1' ~ -- 这里手滑了

一般状况,我们都认为毛病点在 ~,而末了一个准确输入是 '1'。但词法剖析器可不这么想,在我第一版代码里,推断出毛病是如许的:

《精读《手写 SQL 编译器 - 毛病提醒》》

提醒是 where 错了,而且提醒是 .,有点摸不着头脑。

读者能够已想到了,这个题目与文法构造有关,我们看 fromClause 的文法形貌:

const fromClause = () =>
  chain(
    "from",
    tableSources,
    optional(whereStatement),
    optional(groupByStatement),
    optional(havingStatement)
  )();

虽然现实传入的 where 语句多了一个 ~ 标记,但由于文法认为全部 whereStatement 是可选的,因而失足后会跳出,跳到 b 的位置继承婚配,而 明显 groupByStatementhavingStatement 都不能婚配到 where,因而编译器认为 “不会从 b where a = '1' ~” 最先就有题目吧?因而继承往回追溯,从 tableName 最先婚配:

const tableName = () =>
  chain([matchWord, chain(matchWord, ".", matchWord)()])();

此时第一次走的 b where a = '1' ~ 线路对应 matchWord,因而尝试第二条线路,所以认为 where 应当换成 .

要处理这个题目,首先要 认可这个推断是对的,由于这是一种 毛病提早的状况,只是人类明白时每每只能看到末了几步,所以我们默许用户想要的毛病信息,是 准确婚配链路最长的那条,并对 onMatchNode 作出下面优化:

lastMatch 对象改成 lastMatchUnderShortestRestToken:

if (
  !lastMatchUnderShortestRestToken ||
  (lastMatchUnderShortestRestToken &&
    lastMatchUnderShortestRestToken.restTokenCount > restTokenCount)
) {
  lastMatchUnderShortestRestToken = {
    matchNode,
    token: matchResult.token,
    restTokenCount
  };
}

也就是每次婚配到准确字符,都猎取盈余 Token 数目,只保存末了一婚配准确 且盈余 Token 起码的谁人

3 总结

做语法剖析器毛病提醒功用时,再次革新了笔者三观,本来我们认为的必定,在编译器里对应着那末多 “能够”。

当我们碰到一个毛病 SQL 时,毛病缘由每每不止一个,你能够随意截取一段,说是从这一步最先就错了。语法剖析器为了让报错相符人们的第一直觉,对毛病信息做了 过滤,只保存盈余 Token 数最短的那条毛病信息。

4 更多议论

议论地点是:
精读《手写 SQL 编译器 – 毛病提醒》 · Issue #101 · dt-fe/weekly

假如你想介入议论,请点击这里,每周都有新的主题,周末或周一宣布。

    原文作者:黄子毅
    原文地址: https://segmentfault.com/a/1190000016244312
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞