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

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

本文是《如何用C++实现一个简易数据库》的第三个单元

源码位于tutorial03-实现insert和select

1. 怎么初步完成我们的数据库?

现在我们先从最简单的开始,我们即将完成的数据库功能非常简单。

  • 仅支持insertselect两项
  • 将一切数据储存到内存中,即退出程序后一切数据丢失。
  • 仅支持单个硬编码表

2. 如何修改我们的测试用例?

为了支持上述情况,我们需要修改我们的insert和select测试用例如下。

it 'inserts and retrieves a row' do
    result = run_script([
      "insert 1 user1 person1@example.com",
      "insert 2 user2",
      "select",
      ".exit",
    ])
    expect(result).to match_array([
      "db > Executed.",
      "db > Syntax error. Could not parse statement.",
      "db > (1, user1, person1@example.com)",
      "Executed.",
      "db > Bye!",
    ])
  end

3. 怎么设计我们的储存结构?

我们目前规定所储存的类型结构如下

类型
id整型 (integer)
username可变字符串 (varchar 32)
email可变字符串 (varchar 255)

注意到我们这个时候没有使用 std::string 而使用的是 char[] 以限制长度。

#define COLUMN_USERNAME_SIZE 32
#define COLUMN_EMAIL_SIZE 255
class Row
{
public:

    uint32_t id;
    char username[COLUMN_USERNAME_SIZE];
    char email[COLUMN_EMAIL_SIZE];

}; 

现在我们已经有了一行行的数据了,现在让我们将它合理的储存起来。 sqlite 使用 B-Tree 进行储存,这种优越的数据结构可以快速的进行查找,插入,删除。但是显然我们应该从最简单的开始,我们选择将它分组到 页面 (Page) 当中去,然后将这些页面以数组的形式排列。此外,我们需要在一页中,尽可能的将其紧密排列,意味着数据应该一个挨着一个。

大小 (bytes)偏移量 (offset)
id40
username324
email25536
总计291

我们通过实现序列化 (serialize) 以及反序列化 (serialize) 来达成我们的目的。同时注意到我们这里写了一个(char *)的强制转化类型,是为了让编译器明白,偏移量 (offset) 是以单个字节 (bytes) 为单位的。

#define size_of_attribute(Struct, Attribute) sizeof(((Struct *)0)->Attribute)

const uint32_t ID_SIZE = size_of_attribute(Row, id);
const uint32_t USERNAME_SIZE = size_of_attribute(Row, username);
const uint32_t EMAIL_SIZE = size_of_attribute(Row, email);
const uint32_t ID_OFFSET = 0;
const uint32_t USERNAME_OFFSET = ID_OFFSET + ID_SIZE;
const uint32_t EMAIL_OFFSET = USERNAME_OFFSET + USERNAME_SIZE;
const uint32_t ROW_SIZE = ID_SIZE + USERNAME_SIZE + EMAIL_SIZE;

void serialize_row(Row &source, void *destination)
{
    memcpy((char *)destination + ID_OFFSET, &(source.id), ID_SIZE);
    memcpy((char *)destination + USERNAME_OFFSET, &(source.username), USERNAME_SIZE);
    memcpy((char *)destination + EMAIL_OFFSET, &(source.email), EMAIL_SIZE);
}

void deserialize_row(void *source, Row &destination)
{
    memcpy(&(destination.id), (char *)source + ID_OFFSET, ID_SIZE);
    memcpy(&(destination.username), (char *)source + USERNAME_OFFSET, USERNAME_SIZE);
    memcpy(&(destination.email), (char *)source + EMAIL_OFFSET, EMAIL_SIZE);
}

接下来,让我们创建我们的Table来储存这些分页。同时我们与大多数计算机系统一样,将其设置为4k大小。

#define TABLE_MAX_PAGES 100
const uint32_t PAGE_SIZE = 4096;
const uint32_t ROWS_PER_PAGE = PAGE_SIZE / ROW_SIZE;
const uint32_t TABLE_MAX_ROWS = ROWS_PER_PAGE * TABLE_MAX_PAGES;
class Table
{
public:
    uint32_t num_rows;
    void *pages[TABLE_MAX_PAGES];
    Table()
    {
        num_rows = 0;
        for (uint32_t i = 0; i < TABLE_MAX_PAGES; i++)
        {
            pages[i] = NULL;
        }
    }
    ~Table()
    {
        for (int i = 0; pages[i]; i++)
        {
            free(pages[i]);
        }
    }
};

注意到,由于C++的构造和析构特性,使得我们在一定程度上减少了很多手动初始化和释放的代码工作量。

此外我们还应该知道,我们的表页该从何处开始读写。

void *row_slot(Table &table, uint32_t row_num)
{
    uint32_t page_num = row_num / ROWS_PER_PAGE;
    void *page = table.pages[page_num];
    if (page == NULL)
    {
        // Allocate memory only when we try to access page
        page = table.pages[page_num] = malloc(PAGE_SIZE);
    }
    uint32_t row_offset = row_num % ROWS_PER_PAGE;
    uint32_t byte_offset = row_offset * ROW_SIZE;
    return (char *)page + byte_offset;
}

4. 怎么实现执行储存操作?

现在我们已经设计好了储存结构,让我们来看一下怎么执行储存操作。首先,我们为statement添加我们的row属性。

class Statement
{
public:
    StatementType type;
    Row row_to_insert;
};

之后我们需要解析insert这个操作。为此,我们对添加PrepareResult新增一个解析句法错误状态PREPARE_SYNTAX_ERROR。我们使用sscanf来进行格式化读取输入,并将它储存到我们的statement当中去。

PrepareResult DB::prepare_statement(std::string &input_line, Statement &statement)
{
    if (!input_line.compare(0, 6, "insert"))
    {
        statement.type = STATEMENT_INSERT;
        int args_assigned = std::sscanf(
            input_line.c_str(), "insert %d %s %s", &(statement.row_to_insert.id),
            statement.row_to_insert.username, statement.row_to_insert.email);
        if (args_assigned < 3)
        {
            return PREPARE_SYNTAX_ERROR;
        }
        return PREPARE_SUCCESS;
    }
    else if (!input_line.compare(0, 6, "select"))
    {
        statement.type = STATEMENT_SELECT;
        return PREPARE_SUCCESS;
    }
    else
    {
        return PREPARE_UNRECOGNIZED_STATEMENT;
    }
}

现在让我们来真正设计执行insertselect操作。

对于insert操作,我们首先判断它是否超出储存限制,针对操作执行结果,我们同样添加了与我们之前类似的枚举类状态码
enum ExecuteResult { EXECUTE_SUCCESS, EXECUTE_TABLE_FULL };
之后,若满足对应条件,我们寻找到合适的内存插入位置,将我们输入的行以serialize_row的方式填充到内存page当中。

ExecuteResult DB::execute_insert(Statement &statement, Table &table)
{
    if (table.num_rows >= TABLE_MAX_ROWS)
    {
        std::cout << "Error: Table full." << std::endl;
        return EXECUTE_TABLE_FULL;
    }

    void *page = row_slot(table, table.num_rows);
    serialize_row(statement.row_to_insert, page);
    table.num_rows++;

    return EXECUTE_SUCCESS;
}

类似的对于select操作,我们仅需从page中对应位置通过deserialize_row的方式获取到即可。

ExecuteResult DB::execute_select(Statement &statement, Table &table)
{
    for (uint32_t i = 0; i < table.num_rows; i++)
    {
        Row row;
        void *page = row_slot(table, i);
        deserialize_row(page, row);
        std::cout << "(" << row.id << ", " << row.username << ", " << row.email << ")" << std::endl;
    }

    return EXECUTE_SUCCESS;
}

最后将我们所设计好的操作交给虚拟机来执行即可。

void DB::execute_statement(Statement &statement, Table &table)
{
    ExecuteResult result;
    switch (statement.type)
    {
    case STATEMENT_INSERT:
        result = execute_insert(statement, table);
        break;
    case STATEMENT_SELECT:
        result = execute_select(statement, table);
        break;
    }

    switch (result)
    {
    case EXECUTE_SUCCESS:
        std::cout << "Executed." << std::endl;
        break;
    case EXECUTE_TABLE_FULL:
        std::cout << "Error: Table full." << std::endl;
        break;
    }
}

void DB::start()
{
    Table table;

    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, table);
    }
}

让我们来看看能否通过测试吧。

..

Finished in 0.00768 seconds (files took 0.07764 seconds to load)
2 examples, 0 failures

结果很不错,看上去我们的程序运行得很好,但是实际上我们还是需要更多的测试。

5.总结

我们已经初步实现了一个具有储存,查询固定表功能的一个数据库了。作为数据库,我们不可避免的涉及到了大量对内存指针的操作,这其中需要大家认真去理解每一个常量的设计原理以及考量。在下一章节,我们将对我们所写的数据库进行更加极端的边界测试,并且来一起修正其中的Bug。同时如果对本教程有所疑问或笔者存在错误,都欢迎在评论区或者issue中提出。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值