前言
最近学习了一篇国外大神用C语言仿写SQLite数据库的文章,受益匪浅,因此萌生了翻译该文章的念头。原文作者选择SQLite作为仿写对象是因为它是一个轻量级的数据库,在设计和功能上都相对精简。本文共分为十三个章节介绍如何实现SQLite,从一个简单的REPL开始,逐步增加功能,到最终实现数据库的基本功能。本文适合有一定C语言基础且对数据库感兴趣的程序员,他们可以从中学到数据库的基础知识、SQLite的设计思想以及一些复杂数据结构在工程中的实践应用方法等。
在正式开始之前,有几个问题想让大家思考一下:
- 数据库中的数据在计算机上是以什么形式存储的?(在内存和硬盘上)
- 数据库什么时候需要把数据从内存搬到硬盘?
- 为什么数据库中每一个表(table)有且只有一个主键(primary key)?
- 数据库的回滚事务(rollback transaction)是怎样工作的?
- 数据库如何格式化索引?
- 什么时候会发生全表扫描?如何进行?
- 内置的预定义语句是以什么样的格式保存的?
能回答出上述问题说明你已经对数据库有了比较全面的理解,如果有答不上来的问题,可以带着这些问题继续阅读后文,看看能不能从中找到答案。
SQLite简介
关于SQLite在他们的官网上有很多介绍文档,下图是对SQLite架构的抽象表示。
为了实现一次数据查询或者修改,SQLite需要通过一系列组件实现。如果把数据库的功能分为前端和后端,那么前端的组件包括:
- tokenizer
- parser
- code generator
从前端输入的是SQL查询语句,输出的是SQLite虚拟机字节码。前端组件可以看做在数据库上运行的程序,将SQL语句翻译成数据库的操作指令。
后端组件包括
- virtual machine
- B-tree
- pager
- os interface
虚拟机(virtual machine)把前端生成的字节码作为指令。它可以对一个或多个表(table)或者索引(index)执行操作,这些表和索引都存储在叫做B-tree的数据结构上。虚拟机的本质是一个基于字节码指令的大型switch语句。
B-tree包含许多节点(node),每个节点大小相当于内存中的一页(page)。B-tree既能从硬盘上取出page,又能通过给pager下达指令的方式将page写回硬盘。
pager接收指令后执行读/写page数据的操作。它会先在数据库文件中找到正确的偏移位置后再进行读/写。pager在内存中维护了一个cache,用于缓存最近访问的page,并且决定哪些page需要写回硬盘中。
操作系统接口(os interface)根据SQLite编译的目标平台不同而有所区别。在本文中我们不会支持多个平台。
千里之行始于足下,让我们从一个简单的REPL开始吧!
REPL(Read Eval Print Loop,交互式解释器)是一个简单的交互式的编程环境,类似于Linux Terminal,可以在终端中输入命令,并接收系统的响应。
- Read(读取):读取用户输入
- Eval(执行):执行输入的指令
- Print(打印):打印执行结果
- Loop(循环):循环上述步骤
创建一个简单的REPL
当你在命令行中运行SQLite时,SQLite会启动一个REPL。
~ 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
~
为了实现REPL,我们的主函数(main)将循环打印提示符,获取命令行输入和执行输入指令。
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:输入流,我们会从标准输入中读取数据
- return value:getline()读取到的字节数
我们使用getline()将命令行数据保存在 input_buffer->buffer ,将buffer size保存在 input_buffer->buffer_length ,将返回值保存在 input_buffer->input_length
buffer初始时为NULL,所以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 元素的内存。(getline() 在 **read_input()**函数中为 input_buffer->buffer 分配内存)
void close_input_buffer(InputBuffer* input_buffer) {
free(input_buffer->buffer);
free(input_buffer);
}
最后,我们需要解析并执行指令。当前程序只能识别一个指令:.exit ,用来终止程序。如果输入其他指令,当前程序会打印错误消息并继续REPL的循环。
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);
}
}
}
原文链接:Let’s Build a Simple Database:Part 1 - Introduction and Setting up the REPL