SQL 解析 — 如何轻松实现新增语句
KaiwuDB 支持多种不同类型的 SQL 语句,例如 create、insert 等。本文将介绍在 KaiwuDB SQL Parser(下文统称解析器)中添加新语句的过程及其实现。我们将了解如何使用 goyacc 工具更新解析器,以及执行器和查询计划器(planner)如何协同执行这条语句。
一、语法和关键字
添加新的 SQL 语句需要从向 SQL 解析器添加必要的语法开始。解析器是通过 goyacc 生成的,它是流行的 yacc 编译器的 Go 版本。语法定义位于 pkg/sql/parser/sql.y 文件中。解析器的输出是一个抽象语法树(AST),其中的节点类型(node)在 pkg/sql/sem/tree 目录下的各个文件中定义。
向 SQL 解析器添加新的语句主要由三个组成部分:添加新的关键字、向语句解析器添加语法,以及添加新的语法节点类型。
二、FROBNICATE STATEMENT
本文以在 KaiwuDB 新增一个新的语句为例:FROBNICATE。这个语句将会随机修改数据库的设置。它将有三个选项:FROBNICATE CLUSTER,用于操作集群设置;FROBNICATE SESSION,用于操作会话设置;FROBNICATE ALL,用于同时处理两者。
让我们从检查所有关键字是否已定义开始。打开 pkg/sql/parser/sql.y 文件并搜索"Ordinary key words"。你会看到一系列按字母顺序排列的标记定义。由于其他语法已经定义了 SESSION、CLUSTER 和 ALL 关键字,我们不需要添加它们,但我们需要为 FROBNICATE 创建一个关键字。它应该如下所示:
这告诉词法分析器识别关键字,但我们仍然需要将它添加到其中一个类别列表中。如果关键字可以出现在标识符位置,它必须是保留的(reserved_keyword, 这要求对其的其他用途,例如作为列名,必须使用引号引用)。由于我们的新关键字作为一条 SQL 语句的开始,它不能被误认为是标识符,所以我们可以安全地将它添加到非保留关键字列表中。在 pkg/sql/parser/sql.y 文件中搜索 unreserved_keyword:,并按照以下方式添加 | FROBNICATE:
既然词法分析器已经知道了我们所有的关键字,我们需要教会解析器如何处理我们的新语句。有三个地方我们需要添加引用:语句类型列表、语句情况列表和解析子句。
在语法文件(pkg/sql/parser/sql.y)中搜索<tree.Statement>,你将找到类型列表。添加一行关于我们新语句类型的内容,类似于:
这样我们就为新语句类型"frobnicateStmt"添加了一个类型声明。请注意,"frobnicateStmt"只是一个示例名称,你可以根据实际情况自定义。
接下来,我们需要将新语句类型添加到语句情况列表中。继续搜索语法文件,找到以"stmt"开头的规则(如 stmt_select、stmt_insert 等)。在这些规则中添加以下情况:
最后,我们需要为我们的语句添加一个产生式规则。在 pkg/sql/parser/sql.y 文件中添加以下规则:
这里列出了我们允许的三种表达形式,用竖线字符分隔。每个产生式还有一个用大括号括起来的实现(暂时报错并显示“未实现”的错误信息)。
最后为我们的语句添加 help 文档。在我们刚刚添加的产生式规则上方,添加以下注释:
现在我们的解析器将能够识别新的语句类型,并且生成一些新语法相关的注释以为用户提供帮助。重新编译代码后,尝试执行这条语句,得到以下结果:
这代表我们新加的语法成功解析了,但是由于尚未实现,无法执行任何操作。
三、添加抽象语法树
语法层添加完成后,现在我们需要为新的语句赋予适当的语义。我们需要一个 AST 来从解析器向运行时传递语句的结构。上文说过我们的语句是 %type <tree.Statement>,这意味着它需要实现 tree.Statement 接口,可以在 pkg/sql/sem/tree/stmt.go 中找到。
我们需要编写四个函数:三个用于 Statement 接口本身(StatementReturnType、StatementType 和 StatementTag),一个用于 NodeFormatter(Format),还有标准的 fmt.Stringer。
请为我们的语句类型创建一个新文件:pkg/sql/sem/tree/frobnicate.go。在其中,放入我们 AST 节点的格式和定义。
要添加我们 AST 树的语句和字符串表示,打开 pkg/sql/sem/tree/stmt.go 文件并搜索 // StatementReturnType implements the Statement interface。现在你可以看到不同类型 AST 的实现列表。按照字母顺序将以下内容插入其中:
接下来,按字母顺序添加以下内容:
现在我们需要更新解析器,在遇到我们的语法时返回一个带有适当模式类型 FROBNICATE 节点(AST)。返回到 pkg/sql/parser/sql.y 文件,搜索 // %Help: FROBNICATE,并将语句替换为以下内容:
特殊符号 $$.val 表示此规则生成的节点值。还有一些其他的 $ 符号,可以在 yacc 中使用。其中一个更有用的形式是引用子产生式的节点值(例如,在这三个语句中,$1 将是标记 FROBNICATE)。
接下来重新编译 KaiwuDB,重新输入新语法,得到以下结果:
现在我们看到了一个不同于之前的错误。这个错误来自于 SQL 计划器(planner),在遇到新的语句类型时不知道该怎么处理。我们需要教给它新的语句的含义。尽管我们的语句不会在任何查询计划中起作用,但我们将通过向规划器添加一个方法来实现它。这是集中化语句派发的地方,因此在那里添加语义。
找到我们当前看到的错误的源代码,会发现它在 /pkg/sql/opaque.go 文件中一长串类型选择语句的末尾。让我们在其中添加一个 case:
同样,在同一文件 /pkg/sql/opaque.go 的 init() 函数下面添加以下内容:
这将调用计划器自身上的一个方法(尚未实现)。让我们在 pkg/sql/frobnicate.go 文件中实现该方法。
此时重新编译 KaiwuDB,再次执行该语句:
至此为止,我们已经能够让错误传递到 SQL 客户端了。我们只需要上述接口添加功能能代码,让语句生效即可。
评论