我们如何使用 ANTLR4 构建 SQL 自动完成框架

这是关于 SQL 解析器的系列文章:

  1. 我们如何将 SQL 解析器速度提高 70 倍
  2. 我们如何使用 ANTLR4 构建 SQL 自动完成框架(此)

背景

Bytebase 是针对数据库的 DevSecOps 解决方案,支持广泛的数据库系统。它提供了一个基于 Web 的 SQL 编辑器,配备了强大的安全功能,包括权限控制、数据屏蔽和审计日志记录。然而,除了这些安全功能之外,任何有效的 SQL 客户端的核心功能都是自动完成

在研究了市场上主流的 SQL 客户端后,我们发现,虽然它们在简单的查询中表现良好,但在面对更复杂的 SQL 语句时,它们往往会遇到困难。在本文中,我们将分享如何在 Bytebase SQL 编辑器中实现自动完成来解决这些限制。

问题

自动完成(也称为 IntelliSense)是开发人员经常使用的熟悉工具。它解决的问题定义明确:基于以下内容提供相关的完成建议:

  1. 输入文本
  2. 预定义语法规则
  3. 给定的完成位置

从本质上讲,自动完成是一个利用这三个上下文因素的预测问题。

“天真”的方法

天真的方法实际上并不难。我们可以模拟用户的思维过程,建立一系列联想规则来处理需要自动完成的场景。鉴于我的工作和背景,在本文中,我将使用简单的 SQL 作为示例。

<span style="color:#e5e7eb"><span style="background-color:#172136 !important"><code class="language-sql"><span style="color:#66ccff">SELECT</span> <span style="color:#ba8c5e">|</span> <span style="color:#66ccff">FROM</span> t<span style="color:#808080">;</span></code></span></span>

在上面的示例中,该符号用于表示光标位置,该位置对应于前面提到的给定完成位置。根据 SQL 背景知识,我们可以推断光标很可能被放置来写列名,并且由于文本包含 ,我们可以合理地推断该列应该来自表 t。|FROM t

从这个推理中,我们可以总结出一个简单的规则:当尝试完成子句中的字段时,查找后续子句,识别相应的表名,并提供表的列名列表供用户选择。SELECTFROM

我们可以将这样的规则称为经验规则。虽然用文字描述它并不太困难,但实施它确实带来了一些挑战。

第 1 步 - 识别光标位置:我们首先需要确定光标的位置。这可以使用解析器或正则表达式等工具来实现。显然,用于检查光标是否在子句内的工具的复杂性与它们需要处理的场景的复杂性成正比。在更简单的情况下,正则表达式等工具就足够了,但它们可能会因更复杂的 SQL 查询(例如涉及子查询的查询)而失败。在这些情况下,需要更高级的方法和工具。SELECT

第 2 步 - 找到 FROM 子句:接下来,我们需要识别紧跟在后面的子句,并尝试从中解析表名。幸运的是,使用 ANTLR4 构建的解析器自然支持部分解析,这意味着我们可以尝试仅解析 FROM 子句,而无需从头开始解析整个查询。FROM

步骤3 - 检索表结构信息:最后,根据解析后的表名,我们检索表结构信息,并提供列名以进行自动补全。

主要见解:

  1. 通过不断添加这样的经验规则,我们可以逐步改进自动完成处理的场景集,从而增强整体用户体验。
  2. 经验规则严重依赖于特定语法,必须手动添加。
  3. 不同的经验规则可能需要不同的实现,每条规则的复杂性与其旨在涵盖的场景的广度相关。

现实世界的挑战

在这一点上,您可能已经意识到,基于经验规则的朴素实现通常只适用于更简单的场景和语法。对于 Bytebase 来说尤其如此,它必须处理许多大致相似但在细节上不同的 SQL 方言。不可扩展的解决方案将很快导致工程工作的爆炸式增长。

我们如何将 SQL 解析器速度提高 70 倍一文中,我们讨论了 Bytebase 如何基于 ANTLR4 为每种 SQL 方言生成和维护解析器。自然而然地,问题就出现了:我们能否利用 ANTLR4 创建一个跨不同方言实现自动完成的解决方案,既提供可扩展性又具有强大的完成功能?

解析器和 ANTLR4

在这里,我们需要讨论解析器的本质。

从功能的角度来看,解析器本质上是一种工具,它获取一串文本并根据预定义的语法规则将其转换为结构化数据。从实现的角度来看,解析器本质上是一种将预定义的语法规则转换为自动机的机制。然后,该自动机根据输入执行状态转换,最终根据输入的最终状态确定输入是否符合语法规则。

具体来说,对于 ANTLR4,ANTLR4 生成器基于书面语法文件 () 创建一个称为增强过渡网络 (ATN) 的状态机。ATN 是一个自动机,代表语言的语法。.g4

当基于 ANTLR4 的解析器处理输入字符串时,它会根据 ATN 在状态之间转换,直到它达到最终状态或输入耗尽。这种状态机驱动的过程允许 ANTLR4 识别和验证输入是否符合指定的语法。

对于解析器,如果输入字符串有效,它将构造并返回一个解析树(或抽象语法树,AST),它根据语法表示输入的语法结构。但是,如果输入字符串无效,解析器将抛出错误并且不会继续。此行为可确保解析器仅处理有效输入,严格遵守定义的语法规则。

<span style="color:#e5e7eb"><span style="background-color:#172136 !important"><code class="language-sql"><span style="color:#66ccff">SELECT</span> a <span style="color:#66ccff">FROM</span> t<span style="color:#808080">;</span></code></span></span>

树

在上面的示例中,我使用省略号来表示语法规则,而圆角矩形表示终端(标记)。

但是,对于前面提到的自动完成示例:

<span style="color:#e5e7eb"><span style="background-color:#172136 !important"><code class="language-sql"><span style="color:#66ccff">SELECT</span> <span style="color:#ba8c5e">|</span> <span style="color:#66ccff">FROM</span> t<span style="color:#808080">;</span></code></span></span>

解析器将无法解析,从而停止解析过程。SELECT items

自动化? 自动机器!

那么,这与自动完成有什么关系呢?早些时候,在讨论朴素方法时,我们提到自动完成本质上是一个预测问题——我们想预测用户的预期输入。重要的是,这种预测不是武断的;它必须遵守预定义的语法规则。

现在我们知道语法规则被 ANTLR4 转换为自动机,我们可以应用基于状态机的方法来自动完成!

我们可以尝试在自动机中转换状态。当我们到达光标位置(触发自动完成的点)时,有效的下一个状态集变得有限。这些有效的 next 状态表示该位置可能的自动完成建议。从本质上讲,自动机指导我们根据语法规则预测下一个有效输入可能是什么,并且这些预测可以用作自动完成选项。

所以,自动完成需要做的是:

  1. 接收输入字符串并停在光标位置。
  2. 检查当前状态和可能的有效状态转换。
  3. 将可接受的过渡转换为文本,并将其作为自动完成建议返回。

如您所见,这种新方法与朴素方法没有本质区别。这两种方法都旨在预测下一个有效输入。主要区别在于,朴素方法依赖于特定的、依赖于语法的经验规则,这使得其实现与特定的语法结构固有地联系在一起。相比之下,基于自动机的方法基于适用于 SQL 方言的通用机制。这使得它至少在过程的前两个步骤(接收输入和检查状态转换)期间与语法无关。

相同的底层核心支持跨语法可扩展性。这是 ANTLR4 带来的卓越功能,也是它提供的关键优势之一——能够为跨各种 SQL 方言的自动完成提供可扩展的解决方案。

ANTLR4 上的跨 SQL 方言自动完成

现在我们已经确定了核心方法,我们需要回到手头的现实世界问题。Bytebase 旨在跨不同的 SQL 方言提供准确有效的 SQL 自动完成功能。

在实践中,我们发现用户通常希望 SQL 自动完成功能能够处理几种常见场景,例如:

场景 1:SQL 中的关键字可能很长或难以记住,因此用户只需输入几个字符即可获得自动完成建议。

解决方案:SQL 关键字的完成本质上是自动机状态转换中过渡到终端符号的问题。这些关键字定义是词法语法规则的一部分,因此可以以与语法无关的方式进行处理。

场景 2:在适当的位置提供表名、列名、函数名等的补全,类似于编程语言中的变量名补全。

解决方案:在 SQL 中,这些名称可以分组为标识符,由特定的语法规则定义。我们不需要为每种情况找到相应的终端符号;相反,我们可以专注于识别相关的语法规则,并通过附加信息(如数据库模式)提供适当的表名、列名、函数等。在大多数情况下,这也可以以与语法无关的方式完成。

方案 3:建议不太常用的 SQL 语法,例如如何编写 ALTER TABLE 语句的下一部分,或其他类似的代码片段补全。

解决方案:这类似于代码片段补全,例如在 C 等语言中如何完成其他代码结构。它涉及预测常见模式和 SQL 语法结构,可以使用特定的规则或模板来处理。FOR LOOP

在这里,我们将重点介绍场景 1 和 2。

整个自动补全架构实际上非常简单,由两部分组成:

  1. 基于 ANTLR4 的与语法无关的代码补全核心。它使用自动机和输入来识别终端符号(关键字)和感兴趣的语法规则(标识符)。我们将后者称为候选人规则。

  2. 特定于语法的后处理。它主要将候选规则转换为相应的字符串,并将其返回给用户。

ANTLR4 上的自动完成核心

感谢 ANTLR4!ANTLR4 运行时提供了使用和作 ATN 的接口,这使我们可以轻松地使用 ATN 执行所需的作。

这里的总体思路是首先将 ATN 遍历到光标位置,然后从当前状态中找到可能的后续状态。这些可能的后续状态称为 .然后我们检查它是否包含我们感兴趣的任何语法规则或终端。之后,我们收集这些候选人并将它们传递到下一阶段。Follow SetFollow Set

这是一个小的优化:很明显,只依赖于 ATN,因此我们可以预先计算并缓存每个特定语法的所有 。这样,我们就不需要在每次执行自动完成时都重新计算它们。Follow SetFollow Sets

这里的方法相当简单,但技术实现细节相当广泛。由于篇幅所限,这里我们就不作进一步的解释了。让我们把它留到下一篇文章!

语法

收到候选人后,我们需要对其进行分类和处理。

对于终端,不需要太多处理;我们只需找到终端符号的相应字符串并返回它。

对于候选规则,我们需要以分类的方式处理它们。这里,我们将以表名和列名为例。

让我们首先假设一个简化的 SQL 语法:

<span style="color:#e5e7eb"><span style="background-color:#172136 !important"><code class="language-sql">SelectStatement:
	SelectClause FromClause SEMICOLON<span style="color:#808080">;</span>

SelectClause:
	<span style="color:#66ccff">SELECT</span> SelectItems<span style="color:#808080">;</span>

FromClause:
	<span style="color:#66ccff">FROM</span> FromItems<span style="color:#808080">;</span>

SelectItems:
	ColumnName <span style="color:#808080">(</span>COMMA ColumnName<span style="color:#808080">)</span><span style="color:#ba8c5e">*</span><span style="color:#808080">;</span>

ColumnName:
	<span style="color:#808080">(</span>TableName DOT<span style="color:#808080">)</span>? Identifier<span style="color:#808080">;</span> <span style="color:#6a7895">// Lower performance but more readable.</span>

FromItems:
	TableName <span style="color:#808080">(</span>COMMA TableName<span style="color:#808080">)</span><span style="color:#ba8c5e">*</span><span style="color:#808080">;</span>

TableName:
	Identifier<span style="color:#808080">;</span></code></span></span>

对于上面提到的语法,我们通常感兴趣的规则是 和 。现在让我们看下面的例子:TableNameColumnName

<span style="color:#e5e7eb"><span style="background-color:#172136 !important"><code class="language-sql"><span style="color:#66ccff">SELECT</span> <span style="color:#ba8c5e">*</span> <span style="color:#66ccff">FROM</span> |</code></span></span>

使用自动完成核心,我们可以确定候选规则包含一个 .从那里,我们只需要查找架构信息中的所有条目并将它们返回给用户。TableNameTableName

列名的示例稍微复杂一些:

<span style="color:#e5e7eb"><span style="background-color:#172136 !important"><code class="language-sql"><span style="color:#66ccff">SELECT</span> <span style="color:#ba8c5e">|</span> <span style="color:#66ccff">FROM</span> t<span style="color:#808080">;</span></code></span></span>

同样,通过自动补全核心,我们可以识别候选规则包含一个 .最简单的方法是从架构中返回所有列名。但是,这里的一个明确观察是,该子句将列名限制为属于 table 的列名。ColumnNameFROM tt

由于自动完成核心仅解析到光标位置,因此它没有有关子句的信息。因此,我们需要做一些额外的工作:FROM

  1. 我们可以使用 ANTLR4 的词法分析器来定位下面的关键字。FROM
  2. 借助 ANTLR4 的部分解析功能,我们尝试解析为 .FROM tFromClause
  3. 一旦我们有了解析树,我们就可以提取表名。这需要使用 ANTLR4 的模式。tVisitor/Listener
  4. 一旦我们从子句中获得了表名,我们就可以只将属于表的列返回给用户。FROMt

后记

本文主要介绍了 Bytebase 如何构建具有跨 SQL 方言能力的自动完成框架背后的思考过程。这里分享的旅程和想法旨在为读者提供一些见解和启发。

为了便于理解,提供的示例经过高度简化,但实际场景要复杂得多。这包括但不限于子查询、嵌套语句、CTE(公共表表达式)等。如果您有兴趣,请继续关注未来的文章,我们将深入探讨完整的实现,包括如何使用基于 ANTLR4 的自动完成核心、特定于语法的处理,以及如何与 LSP(语言服务器协议)集成以提供完整且强大的 SQL 自动完成。

显然,仍需要完成一些特定于方言的工作,以确保足够好的自动完成体验。不可否认,这种方法大大降低了开发和维护成本,允许一个小团队维护多种方言的自动完成功能。目前,Bytebase 已经基于该框架实现并维护了五种不同方言的自动完成模块。他们是 全部开源:

由于 Bytebase 的技术堆栈选择,我们在 Golang 中实现了基于 ANTLR4 的自动完成内核,并且我们还将其开源

在实施过程中,我们还确定了 ANTLR4 Golang 运行时的几个需要改进的地方。我们正在积极验证并为上游改进做出贡献。让我们共同努力,让 ANTLR4 变得更好!

你不需要重新发明轮子

我们已经在 SQL 编辑器自动完成方面工作了大约两年,并且仍在完善它。你不需要再经历这个,直接采用 Bytebase SQL Editor 就可以了:

  • 让您的开发人员访问 Bytebase 控制台并从 SQL 编辑器与数据库进行交互。
  • 将 SQL 编辑器嵌入到您自己的内部 Web 门户中。

您可以在 https://sql-editor.com 试用演示(无需注册)。

引用

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值