创建一个简单的数据库:介绍并设置REPL(read-excute-print loop)

这里,只有一个搬运工。如果翻译或是理解有问题,欢迎指正。

 

作为一个web开发者,在每天的日常工作中,都在使用关系型数据库,但是这些数据库对于使用者来说是一个黑盒子。

为了解决这些问题,才有了从头开始写一个数据库的目标。这里选择以sqlite为模板,相较于mysql或者postgresql,sqlit要更小而具有部分它们的部分功能,这样可以更好的理解数据库。整个数据库存储在一个文件中!

 

sqlite

在他们的网站上,有很多sqlite内部结构文档,此外还有一份《SQLite Database System: Design and Implementation》(sqlite 数据库系统:设计及实现)

sqlite官方文档网站:https://www.sqlite.org/arch.html

 

 

一个查询通过一系列的组件来获取或修改数据。前端组成部分:

  • 标记器
  • 解析器
  • 代码生成器

 

在前端输入一个sql查询。输出的是sqlite虚拟机字节码(可在数据库运行的已编译程序)。

 

后端组成:

  • 虚拟机
  • B树
  • pager(寻呼机)
  • OS接口

虚拟机把前端生成的字节码当做指令。它可以对一个或者多个表,或索引执行操作,而它们分别存储在一个名为B树的数据结构里。

虚拟机本质上是对字节码指令的一个转换。

 

每一个B树是由许多的节点组成。每个节点是一页的长度。B树可以通过向pager发送命令来从硬盘上获取一页或是将一页写回硬盘。

 

pager收到命令,写或是读数据页。它负责在数据库文件里读取或写入合适的偏移量。同时,它还将近期访问的页缓存在内存中,并决定这些页什么时候写回硬盘。

 

OS接口,随着操作系统不同而不同,它取决于sqlite为哪种操作系统编译。在这个教程中,并不会支持跨平台。

 

千里之行,始于足下。现在我们从简单的开始:REPL

制作一个简单的REPL

当从命令行启动sqlite时,sqlite会开始一个read-execute-print循环:

~ sqlite3
SQLite version 3.16.0 2016-11-04 19:09:39
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> create table users (id int, username varchar(255), email varchar(255));
sqlite> .tables
users
sqlite> .exit
~

 

要实现这个,主函数需要有一个无限循环:打印提示,获取输入行,并对输入行内容进行处理:

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) {
      close_input_buffer(input_buffer);
      exit(EXIT_SUCCESS);
    } else {
      printf("Unrecognized command '%s'.\n", input_buffer->buffer);
    }
  }
}

 

这里需要定义InputBuffer作为一个小容器,用于存放和getline()交互时,用到的声明(状态)。(更多稍后说明)

typedef struct {
  char* buffer;
  size_t buffer_length;
  ssize_t input_length;
} InputBuffer;

InputBuffer* new_input_buffer() {
  InputBuffer* input_buffer = (InputBuffer*)malloc(sizeof(InputBuffer));
  input_buffer->buffer = NULL;
  input_buffer->buffer_length = 0;
  input_buffer->input_length = 0;

  return input_buffer;
}

 

接着,print_prompt()打印一个提示给用户。这个实在读取输入前执行。

void print_prompt() { printf("db > "); }

 

为了读取输入,使用getline()

ssize_t getline(char **lineptr, size_t *n, FILE *stream);

lineptr:这是一个指针,这个指针指向包含读取内容的buffer。如果它被设置为NULL,那么就由getline对其进行分配,这样也需要由用户来释放,即使命令失败。

n: 指针,指向分配buffer的大小

stream: 被读取的输入流。从标准输入读取。

返回值

  读取的字节数,可能比buffer的size要小。

 

通过getline,实现:将读取内容存放到input_buffer->buffer,同时将buffer的长度存放到input_buffer->buffer_length。而返回值会被存放到input_buffer->input_length。

 

buffer一开始是NULL,所以geline分配足够的内存来存放输入,并让buffer指向它。

void read_input(InputBuffer* input_buffer) {
  ssize_t bytes_read =
      getline(&(input_buffer->buffer), &(input_buffer->buffer_length), stdin);

  if (bytes_read <= 0) {
    printf("Error reading input\n");
    exit(EXIT_FAILURE);
  }

  // Ignore trailing newline
  input_buffer->input_length = bytes_read - 1;
  input_buffer->buffer[bytes_read - 1] = 0;
}

 

至此,可以定义一个函数用于释放Inputbuffer *实例占用的内存,以及各个结构的缓存元素(geline通过read_input, 分配input_buffer->buffer内存).

void close_input_buffer(InputBuffer* input_buffer) {
    free(input_buffer->buffer);
    free(input_buffer);
}

 

最后,需要解析并执行命令。目前只有一个可识别的命令:.exit, 用于结束程序。此外需要打一个错误信息以及让循环继续。

if (strcmp(input_buffer->buffer, ".exit") == 0) {
  close_input_buffer(input_buffer);
  exit(EXIT_SUCCESS);
} else {
  printf("Unrecognized command '%s'.\n", input_buffer->buffer);
}

 

现在可以进行测试!

~ ./db
db > .tables
Unrecognized command '.tables'.
db > .exit
~

 

好的,现在已经得到了一个可执行的REPL。下一节,开始开发命令。同时,下面是这个小节的全部代码:

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
  char* buffer;
  size_t buffer_length;
  ssize_t input_length;
} InputBuffer;

InputBuffer* new_input_buffer() {
  InputBuffer* input_buffer = malloc(sizeof(InputBuffer));
  input_buffer->buffer = NULL;
  input_buffer->buffer_length = 0;
  input_buffer->input_length = 0;

  return input_buffer;
}

void print_prompt() { printf("db > "); }

void read_input(InputBuffer* input_buffer) {
  ssize_t bytes_read =
      getline(&(input_buffer->buffer), &(input_buffer->buffer_length), stdin);

  if (bytes_read <= 0) {
    printf("Error reading input\n");
    exit(EXIT_FAILURE);
  }

  // Ignore trailing newline
  input_buffer->input_length = bytes_read - 1;
  input_buffer->buffer[bytes_read - 1] = 0;
}

void close_input_buffer(InputBuffer* input_buffer) {
    free(input_buffer->buffer);
    free(input_buffer);
}

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) {
      close_input_buffer(input_buffer);
      exit(EXIT_SUCCESS);
    } else {
      printf("Unrecognized command '%s'.\n", input_buffer->buffer);
    }
  }
}

 

posted @ 2020-07-24 16:24  N_zero  阅读(350)  评论(0)    收藏  举报