【C语言搭建数据库】Part01 - 什么是SQLite & 制作一个简单的REPL

这部分内容主要介绍了sqlite数据库的构成,并和你一起从头构建一个REPL工具。文章原地址: https://cstack.github.io/db_tutorial/parts/part1.html

Sqlite

在sqlite的网站上有很多关于sqlite内核的文档 ,此外这里还有一本SQLite数据库:设计与实现.

【C语言搭建数据库】Part01 - 什么是SQLite & 制作一个简单的REPL_第1张图片

一条查询语句通过一系列组件来实现数据的检索或修改。前端包括以下部分:

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

前端的输入是SQL查询语句,输出是sqlite虚拟机字节码(本质上是一段可在数据库操作的编译程序)。

后端则由以下部分组成:

  • 虚拟机
  • B树
  • 寻址器
  • 操作系统接口

虚拟机使用前端生成的字节码作为指令。并且可以对一个表或多个表或索引进行操作,每个表或索引都被存储在一种叫B树的数据结构中。虚拟机的本质上是一种关于字节码指令类型的大开关语句。

每一棵B树包括了许多节点。每一个节点的长度为一页。B树可以从磁盘中检索数据页,或向寻址器发送命令将其保存至硬盘。

寻址器接收用于读写数据页的命令。它负责在存储数据的文件中的合适位置读取或写入数据。它还在内存中存有最近访问过页面的地址,并决定这些页面何时会被存入硬盘。

操作系统接口是因编译sqlite的操作系统不同而异的层。在本教程中,我不打算支持多个平台。

千里之行,始于足下,所以让我们从更直接的东西开始: REPL.

Making a simple REPL

当你从命令行启动sqlite时,它会启动一个读取->执行->打印的循环。

~ 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 : 指向我们用来存储输入命令的缓冲区。如果它被设置为 NULl 则会被 getline 分配内存,因此即便命令为空或执行失败,用户也应该释放这部分空间。

n : 一个指向用于记录分配缓存区大小的变量的指针。

stream : 从中读取输入流。我们将从标准输入中读取。

return value : 读取的字节数,可能小于缓冲区的大小。

我们把 getline 读取的输入流存入 input_buffer->buffer ,分配缓冲区的大小存入 input_buffer->buffer_length,函数的返回值存入input_buffer->input_length.

buffer 初始时指空,所以 getline 会分配足够的内存空间来存储一整行命令,并使 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 * 实例对象和 buffer 所占用的内存了。(getlineread_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 
#include 
#include 
#include 

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

你可能感兴趣的:(C语言搭建数据库,数据库,sqlite,c语言)