当前位置: 首页 > news >正文

第三章 仅支持追加的单表内存数据库

第三章 仅支持追加的单表内存数据库

我们将从小处着手,对数据库施加很多限制。目前,它有如下限制:

  • 支持两种操作:插入一行和打印所有行

  • 仅驻留在内存中(不需要持久化到磁盘)

  • 支持单个硬编码表

我们的硬编码用户表结构如下所示:

列名类型
idinteger
usernamevarchar(32)
emailvarchar(255)

这是一个简单的架构,但它要求我们能够支持多种数据类型和多种大小的文本数据类型。

insert 语句现在需按照如下格式编写:

insert 1 cstack foo@bar.com

这意味着我们需要升级我们的 prepare_statement 函数来解析参数.

if (strncmp(input_buffer->buffer, "insert", 6) == 0) {statement->type = STATEMENT_INSERT;
+    int args_assigned = sscanf(
+        input_buffer->buffer, "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;}

我们将这些解析的参数存储到语句对象内的新 Row 数据结构中:

+#define COLUMN_USERNAME_SIZE 32
+#define COLUMN_EMAIL_SIZE 255
+typedef struct {
+  uint32_t id;
+  char username[COLUMN_USERNAME_SIZE];
+  char email[COLUMN_EMAIL_SIZE];
+} Row;
+typedef struct {StatementType type;
+  Row row_to_insert;  // only used by insert statement} Statement;

现在我们需要将该数据复制到表示表的某个数据结构中。SQLite使用B树进行快速查找,插入和删除。我们将从更简单的东西开始。像B树一样,它会将行分组到页面中,但不是将这些页面排列为树,而是将它们排列为一个数组。

以下是实现细节:

  • 将行存储在称为页的内存块中

  • 每个页面存储尽可能多的行

  • 行被序列化为每页的紧凑表示形式

  • 按需分配页面

  • 保留指向页面的固定大小的指针数组

我们先定义行的序列化表示(我们将行序列化到内存的某个地址里):

#define size_of_attribute(Struct, Attribute) sizeof(((Struct*)0)->Attribute)#define ID_SIZE  size_of_attribute(Row, id)
#define USERNAME_SIZE  size_of_attribute(Row, username)
#define EMAIL_SIZE  size_of_attribute(Row, email)
#define ID_OFFSET (uint32_t)0
#define USERNAME_OFFSET  (ID_OFFSET + ID_SIZE)
#define EMAIL_OFFSET  (USERNAME_OFFSET + USERNAME_SIZE)
#define ROW_SIZE  (ID_SIZE + USERNAME_SIZE + EMAIL_SIZE)#define PAGE_SIZE  4096
#define TABLE_MAX_PAGES 100
#define ROWS_PER_PAGE  (PAGE_SIZE / ROW_SIZE)
#define TABLE_MAX_ROWS  (ROWS_PER_PAGE * TABLE_MAX_PAGES)

序列化后的行结构将如下所示:

列名类型offset
idinteger0
usernamevarchar(32)4
emailvarchar(255)36
total291

我们还需要代码来进行序列化和反序列化转换。

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

接下来,一个 Table 指向行页并跟踪行数的结构:

+const uint32_t PAGE_SIZE = 4096;
+#define TABLE_MAX_PAGES 100
+const uint32_t ROWS_PER_PAGE = PAGE_SIZE / ROW_SIZE;
+const uint32_t TABLE_MAX_ROWS = ROWS_PER_PAGE * TABLE_MAX_PAGES;
+
+typedef struct {
+  uint32_t num_rows;
+  void* pages[TABLE_MAX_PAGES];
+} Table;

我将页面大小设为 4 KB,因为它与大多数计算机体系结构的虚拟内存系统中使用的页面大小相同。这意味着我们数据库中的一页对应于操作系统使用的一个页面。操作系统会将页面作为整个单元移入和移出内存,而不是分解它们。

我添加了一个随意的限制,即我们最多分配 100 页。当我们切换到树结构时,数据库的最大大小将仅受文件最大大小的限制。(尽管我们仍然会限制一次在内存中保留的页面数)。

由于页面在内存中可能不会彼此相邻存在,为了使读取/写入行变得更加容易,我们假设行不应跨越页面边界。

以下是我们如何确定特定行在内存中读取/写入的位置:

+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 page + byte_offset;
+}

现在我们可以根据表结构使用 execute_statement进行读/写操作:

-void execute_statement(Statement* statement) {
+ExecuteResult execute_insert(Statement* statement, Table* table) {
+  if (table->num_rows >= TABLE_MAX_ROWS) {
+    return EXECUTE_TABLE_FULL;
+  }
+
+  Row* row_to_insert = &(statement->row_to_insert);
+
+  serialize_row(row_to_insert, row_slot(table, table->num_rows));
+  table->num_rows += 1;
+
+  return EXECUTE_SUCCESS;
+}
+
+ExecuteResult execute_select(Statement* statement, Table* table) {
+  Row row;
+  for (uint32_t i = 0; i < table->num_rows; i++) {
+    deserialize_row(row_slot(table, i), &row);
+    print_row(&row);
+  }
+  return EXECUTE_SUCCESS;
+}
+
+ExecuteResult execute_statement(Statement* statement, Table* table) {switch (statement->type) {case (STATEMENT_INSERT):
-      printf("This is where we would do an insert.\n");
-      break;
+      return execute_insert(statement, table);case (STATEMENT_SELECT):
-      printf("This is where we would do a select.\n");
-      break;
+      return execute_select(statement, table);}}

最后,我们需要初始化表,创建相应的内存释放函数并处理更多错误情况:

+ Table* new_table() {
+  Table* table = (Table*)malloc(sizeof(Table));
+  table->num_rows = 0;
+  for (uint32_t i = 0; i < TABLE_MAX_PAGES; i++) {
+     table->pages[i] = NULL;
+  }
+  return table;
+}
+
+void free_table(Table* table) {
+    for (int i = 0; table->pages[i]; i++) {
+	free(table->pages[i]);
+    }
+    free(table);
+}
 int main(int argc, char* argv[]) {
+  Table* table = new_table();InputBuffer* input_buffer = new_input_buffer();while (true) {print_prompt();
@@ -105,13 +203,22 @@ int main(int argc, char* argv[]) {switch (prepare_statement(input_buffer, &statement)) {case (PREPARE_SUCCESS):break;
+      case (PREPARE_SYNTAX_ERROR):
+        printf("Syntax error. Could not parse statement.\n");
+        continue;case (PREPARE_UNRECOGNIZED_STATEMENT):printf("Unrecognized keyword at start of '%s'.\n",input_buffer->buffer);continue;}-    execute_statement(&statement);
-    printf("Executed.\n");
+    switch (execute_statement(&statement, table)) {
+      case (EXECUTE_SUCCESS):
+        printf("Executed.\n");
+        break;
+      case (EXECUTE_TABLE_FULL):
+        printf("Error: Table full.\n");
+        break;
+    }}}

通过这些更改,我们实际上可以将数据保存在数据库中!

PS D:\code\db021\code> make  
gcc -g -O0 main.c -o db
PS D:\code\db021\code> .\db.exe
db > insert 1 cstack foo@bar.com
Executed.
db > insert 2 bob bob@example.com
Executed.
db > select
(1, cstack, foo@bar.com)
(2, bob, bob@example.com)
Executed.
db > insert foo bar 1
Syntax error. Could not parse statement.
db > .exit
PS D:\code\db021\code>

现在我们可以基于当前代码编写一些测试用例,原因如下:

  • 我们计划大幅改变存储表的数据结构,测试将捕获回归。

  • 有几个边缘情况我们没有手动测试(例如填满表格)

我们将在下一部分中解决这些问题。

http://www.lryc.cn/news/94077.html

相关文章:

  • 抖音seo矩阵系统源码解析
  • 6个ChatGPT4的最佳用途
  • go系列-读取文件
  • 10 编码转换问题
  • Spring MVC获取参数和自定义参数类型转换器及编码过滤器
  • 理想的实验
  • nginx配置开机启动(Windows环境)
  • MySQL 基础面试题02(事务索引)
  • 主从架构lua脚本-Redis(四)
  • maven与idea版本适配问题
  • ChatGPT扫盲知识库
  • chatgpt赋能python:Python轨迹可视化:用数据讲故事
  • K-means
  • 归并排序(基础+提升)
  • MATLAB应用
  • LeetCode --- 1784. Check if Binary String Has at Most One Segment of Ones 解题报告
  • js:javascript中的事件体系:常见事件、事件监听、事件移除、事件冒泡、事件捕获、事件委托、阻止事件
  • 【数据结构】特殊矩阵的压缩存储
  • 在layui中使用vue,使用vue进行页面数据部分数据更新
  • Vue中如何进行数据导入与Excel导入
  • git 的基本操作
  • 搭建Vue项目以及项目的常见知识
  • TypeScript ~ TS Webpack构建工具 ⑦
  • Rust 自建HTTP Server支持图片响应
  • [游戏开发][Unity]UnityWebRequest使用大全
  • 如何使用Fiddler对手机进行弱网测试?(干货教程)
  • 专业科普:什么是单片机?
  • 深度学习-第T11周——优化器对比实验
  • 基于Dlib的疲劳检测系统
  • three.js通过CubeTexture加载环境贴图,和RGBELoader加载器加载hdr环境贴图