SQL compiler和Virtual Machine介绍
让我们继续SQLite的实现。下图是SQLite官方网站上对SQLite整体架构的描述图,图中右上部分的SQL compiler作为sqlite的前端负责解析字符串,并将其转化为内部的表达式——bytecode(字节码)后输出。字节码在后续的流程中由virtual machine(虚拟机)负责执行。
像这样把事情分成两步有几个好处:
- 降低每个部分的复杂度(如:虚拟机无需考虑语法检查的问题)
- 常见的查询语句在经过一次编译后可以缓存其字节码,从而提高性能
重构main函数
根据上述思路,我们开始重构main函数来支持两个新关键字的处理:
int main(int argc, char* argv[]) {
InputBuffer* input_buffer = new_input_buffer();
while (true) {
print_prompt();
read_input(input_buffer);
if (strcmp(input_buffer->buffer, ".exit") == 0) {
if (input_buffer->buffer[0] == '.') {
switch (do_mate_command(input_buffer)) {
case (MATE_COMMAND_SUCCESS):
continue;
case (MATE_COMMAND_UNRECOGNIZED_COMMAND):
printf("Unrecognized command '%s'.\n", input_buffer->buffer);
continue;
}
}
}
Statement statement;
switch (prepare_statement(input_buffer, &statement)) {
case (PREPARE_SUCCESS):
break;
case (PREPARE_UNRECOGNIZED_STATEMENT):
printf("Unrecognized keyword at start of '%s'.\n", input_buffer->buffer);
continue;
}
execute_statement(&statement);
printf("Executed.\n");
}
}
我们把类似.exit这样的非SQL语句叫做"元命令"(mate-commands)。它们都以“.”开头,所以在识别出这种命令后,我们使用单独的函数来处理它们。
在这之后,我们将命令行输入转换为内部语句表示形式,这就是简易版的sqlite前端。
最后,我们把生成好的语句传给execute_statement函数,这个函数最终会成为我们的虚拟机。
注意,下面的枚举类型是我们新函数的返回值,用来表示处理结果是成功还是失败。
typedef enum {
MATE_COMMAND_SUCCESS,
MATE_COMMAND_UNRECOGNIZED_COMMAND
} MateCommandResult;
typedef enum {
PREPARE_SUCCESS,
PREPARE_UNRECOGNIZED_STATEMENT
} PrepareResult;
“未识别的语句”听起来像是某种异常情况,但通常我并不喜欢使用异常处理(C语言也不支持),因此使用枚举类型来表示异常。当我的switch语句不能处理枚举类型成员时,C语言编译器会报错,所以我们有自信能处理函数的每种结果。预计之后会添加更多枚举变量成员。
do_mate_command在包装现有功能的基础上,也为后续的命令留出了空间:
MateCommandResult do_mate_command(InputBuffer* input_buffer) {
if (strcmp(input_buffer->buffer, ".exit") == 0) {
exit(EXIT_SUCCESS);
} else {
return MATE_COMMAND_UNRECOGNIZED_COMMAND;
}
}
当前,“预处理语句”的枚举类型只包含两种可能值。随着我们语句的扩充将会有更多的值添加到枚举类型中。
typedef enum {
STATEMENT_INSERT,
STATEMENT_SELECT
} StatementType;
typedef struct {
StatementType type;
} Statement;
prepare_statement(我们的“SQL编译器”)现在还无法理解SQL。目前它只能理解两个单词:“insert”和“select”。
PrepareResult prepare_statement(InputBuffer* input_buffer, Statement* statement) {
if (strncmp(input_buffer->buffer, "insert", 6) == 0) {
statement->type = STATEMENT_INSERT;
return PREPARE_SUCCESS;
}
if (strcmp(input_buffer->buffer, "select") == 0) {
statement->type = STATEMENT_SELECT;
return PREPARE_SUCCESS;
}
return PREPARE_UNRECOGNIZED_STATEMENT;
}
注意我们在比较“insert”的时候使用了strncmp,这是因为“insert”关键字后面通常跟着数据(如:insert 1 user foo@bar.com)
最后,我们只剩下execute_statement没有介绍了:
void execute_statement(Statement* statement) {
switch (statement->type) {
case (STATEMENT_INSERT):
printf("This is where we would do an insert.\n");
break;
case (STATEMENT_SELECT):
printf("This is where we would do a select.\n");
break;
}
}
注意,execute_statement不会返回任何错误代码,是因为到这一步时已经不存在出现任何错误的可能了。
通过这次重构我们的程序现在能识别两个新的关键字了!
db > insert foo bar
This is where we would do an insert.
Executed.
db > delete foo
Unrecognized keyword at start of 'delete foo'.
db > select
This is where we would do a select.
Executed.
db > .tables
Unrecognized command '.tables'
db > .exit
原文链接:Let’s Build a Simple Database:Part 2 - World’s Simplest SQL Compiler and Virtual Machine