# 第 1 部分 - 介绍和设置 REPL

> 原文： [https://cstack.github.io/db_tutorial/parts/part1.html](https://cstack.github.io/db_tutorial/parts/part1.html)

作为一名网络开发人员，我每天都会在工作中使用关系数据库，但对我来说，这是一个黑盒子。我有一些问题：

*   保存的数据格式是什么？ （在内存和磁盘上）
*   它什么时候从内存移动到磁盘？
*   为什么每个表只能有一个主键？
*   如何回滚事务工作？
*   索引是如何格式化的？
*   何时以及如何进行全表扫描？
*   准备好的语句保存为什么格式？

换句话说，数据库**如何工作**？

为了解决问题，我正在从头开始编写数据库。它模仿了 sqlite，因为它设计得比 MySQL 或 PostgreSQL 的功能更少，所以我更希望理解它。整个数据库存储在一个文件中！

# 源码

在他们的网站上有很多[关于 sqlite 内部的文档](https://www.sqlite.org/arch.html)，另外我还有[ SQLite 数据库系统：设计与实现](https://play.google.com/store/books/details?id=9Z6IQQnX1JEC)的副本。

![sqlite architecture (https://www.sqlite.org/zipvfs/doc/trunk/www/howitworks.wiki)](img/7194375dc0ca5695801310fd384e4f44.jpg)

sqlite 架构（https://www.sqlite.org/zipvfs/doc/trunk/www/howitworks.wiki）

查询通过一系列组件来检索或修改数据。 **前端**包括：

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

前端的输入是 SQL 查询。输出是 sqlite 虚拟机字节码（本质上是一个可以在数据库上运行的编译程序）。

**后端**包括：

*   虚拟机
*   B 树
*   分页器
*   os 接口

**虚拟机**将前端生成的字节码作为指令。然后，它可以对一个或多个表或索引执行操作，每个表或索引都存储在称为 B 树的数据结构中。 VM 本质上是字节码指令类型的一个大的 switch 语句。

每个 **B 树**由许多节点组成。每个节点的长度为一页。 B 树可以从磁盘检索页面或通过向分页器发出命令将其保存回磁盘。

**分页器**接收读取或写入数据页面的命令。它负责在数据库文件中的适当偏移量处进行读/写。它还在内存中保留最近访问过的页面的缓存，并确定何时需要将这些页面写回磁盘。

**os 接口**是不同的层，取决于 sqlite 在哪个操作系统上编译。在本教程中，我不打算支持多个平台。

[千里之行，始于足下](https://en.wiktionary.org/wiki/a_journey_of_a_thousand_miles_begins_with_a_single_step)，所以让我们从一些更直接的东西开始：REPL。

## 编写一个简单的 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
~ 
```

为此，我们的`main`函数将有一个无限循环，它打印提示，获取一行输入，然后处理该输入行：

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

我们将`InputBuffer`定义为状态的小包装器，我们需要存储它来与[`getline()`](http://man7.org/linux/man-pages/man3/getline.3.html)交互（稍后会详细介绍）。

```c
struct InputBuffer_t {
  char* buffer;
  size_t buffer_length;
  ssize_t input_length;
};
typedef struct InputBuffer_t 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;
} 
```

接下来，`print_prompt()`向用户输出提示。我们在读取每行输入之前这样做。

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

要读取一行输入，请使用[`getline()`](http://man7.org/linux/man-pages/man3/getline.3.html)：

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

`lineptr`：指针的指针，我们用来指向包含读取行的缓冲区。

`n`：我们用来保存已分配缓冲区大小的变量的指针。

`stream`：要读取的输入流。我们将从标准输入中读取。

`return value`：读取的字节数，可能小于缓冲区的大小。

我们告诉`getline`，在`input_buffer->buffer`中存储读取的行，和在`input_buffer->buffer_length`中储存分配的缓冲区的大小。我们将返回值存储在`input_buffer->input_length`中。

`buffer`从`null`开始，因此`getline`分配足够的内存来保存输入行并使`buffer`指向它。

```c
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;
} 
```

最后，我们解析并执行命令。现在只有一个已识别的命令：`.exit`，它终止程序。否则，我们打印错误消息并继续循环。

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

我们来试试吧！

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

好吧，我们有一个能用的 REPL。在下一部分中，我们将开始开发我们的命令语言。同时，这是这一部分的整个程序：

```c
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> 
struct InputBuffer_t {
  char* buffer;
  size_t buffer_length;
  ssize_t input_length;
};
typedef struct InputBuffer_t 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;
}

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