db_tutorial_cpp

Writing a sqlite clone from scratch in C++

View the Project on GitHub KCNyu/db_tutorial_cpp

如何用C++实现一个简易数据库(二)

1. 如何修改我们的单元测试?

由于我们的情况增多,我们可能会对之前的测试情况进行修改,所以各个章节内的单元测试可能相同,但实际期望的返回值会有所不同,希望注意。此外,头部的run_script函数是相同的,至此不再赘述。

  it 'test exit and unrecognized command and sql sentence' do
    result = run_script([
      "hello world",
      ".HELLO WORLD",
      ".exit",
    ])
    expect(result).to match_array([
      "db > Unrecognized keyword at start of 'hello world'.",
      "db > Unrecognized command: .HELLO WORLD",
      "db > Bye!",
    ])
  end

  it 'test insert and select' do
    result = run_script([
      "insert 1 user1",
      "select",
      ".exit",
    ])
    expect(result).to match_array([
      "db > Executing insert statement",
      "db > Executing select statement",
      "db > Bye!",
    ])
  end

注意到我们现在所期望的返回值已经是不同的了,我们对输入内容做出了不同的解析,现在就让我们来看一下如何实现这个解析前端。

2. SQL的解析前端是什么?

它将传统输入的string字符串,解析成可被机器识别的字节码内部表现形式,并传递给虚拟机进一步执行。

3. 怎么实现一个SQL的解析前端?

先从我们上一章所解析的command来看起。我们将以.开头的非sql语句称作元命令 (meta command) 所以我们在一开始就检查是否以其开头,并单独封装一个do_meta_command函数来处理它。

bool DB::parse_meta_command(std::string command)
{
    if (command[0] == '.')
    {
        switch (do_meta_command(command))
        {
        case META_COMMAND_SUCCESS:
            return true;
        case META_COMMAND_UNRECOGNIZED_COMMAND:
            std::cout << "Unrecognized command: " << command << std::endl;
            return true;
        }
    }
    return false;
}
MetaCommandResult DB::do_meta_command(std::string command)
{
    if (command == ".exit")
    {
        std::cout << "Bye!" << std::endl;
        exit(EXIT_SUCCESS);
    }
    else
    {
        return META_COMMAND_UNRECOGNIZED_COMMAND;
    }
}

同时我们观察到,我们现在引入了一些枚举类的解析结果,例如我们现在所使用用于解析元命令的 ` enum MetaCommandResult { META_COMMAND_SUCCESS, META_COMMAND_UNRECOGNIZED_COMMAND }; `

因此接下来,我将一次性定义另外两个解析结果用于解析sql语句。 `
enum PrepareResult { PREPARE_SUCCESS, PREPARE_UNRECOGNIZED_STATEMENT }; ` 这个枚举结果用于返回我们所传递的sql语句是否是符合标准的状态。这个时候就能将传统的以selectinsert开头的sql语句转化成对应的statement状态字节码。

PrepareResult DB::prepare_statement(std::string &input_line, Statement &statement)
{
    if (!input_line.compare(0, 6, "insert"))
    {
        statement.type = STATEMENT_INSERT;
        return PREPARE_SUCCESS;
    }
    else if (!input_line.compare(0, 6, "select"))
    {
        statement.type = STATEMENT_SELECT;
        return PREPARE_SUCCESS;
    }
    else
    {
        return PREPARE_UNRECOGNIZED_STATEMENT;
    }
}

我们为sql语句目前仅定义了如下简单的两种状态字节码 ` enum StatementType { STATEMENT_INSERT, STATEMENT_SELECT }; ` 同时再将上一步成功转化后得到的statement交给虚拟机进行解析。

bool DB::parse_statement(std::string &input_line, Statement &statement)
{
    switch (prepare_statement(input_line, statement))
    {
    case PREPARE_SUCCESS:
        return false;
    case PREPARE_UNRECOGNIZED_STATEMENT:
        std::cout << "Unrecognized keyword at start of '" << input_line << "'." << std::endl;
        return true;
    }
    return false;
}

至此我们初步完成了解析前端的工作,根据command或者sql得到了我们所需要的statement

4. 怎么实现一个虚拟机?

我们先根据得到的statement让虚拟机伪执行一下对应sql语句的操作效果。

void DB::excute_statement(Statement &statement)
{
    switch (statement.type)
    {
    case STATEMENT_INSERT:
        std::cout << "Executing insert statement" << std::endl;
        break;
    case STATEMENT_SELECT:
        std::cout << "Executing select statement" << std::endl;
        break;
    }
}
void DB::start()
{
    while (true)
    {
        print_prompt();

        std::string input_line;
        std::getline(std::cin, input_line);

        if (parse_meta_command(input_line))
        {
            continue;
        }

        Statement statement;

        if (parse_statement(input_line, statement))
        {
            continue;
        }

        execute_statement(statement);
    }
}

再次对我们的结果进行测试检验

..

Finished in 0.00775 seconds (files took 0.07753 seconds to load)
2 examples, 0 failures

恭喜大家又一次通过了所创建的测试。

5. 总结

作为教程的第二章,我们引入了解析前端虚拟机的概念,同时将各种状态映射到对应的枚举类型。至此,我们数据库的基本框架已经搭建完成,下一章就要一起来实现数据存储功能了。