闲来无事,写个数据库吧(1)

项目地址How Does a Database Work? | Let’s Build a Simple Database (cstack.github.io)

我们要写个像sqlite的轻量级数据库,先得看看它长什么样。(真正的sqlite:呵呵,我13万行代码,你来写呀。本次项目将完成其1%的工程量。他们比我们多了写什么?他们有回滚,有锁,而且类型判断及错误判断比我们多,分层比我们多,甚至他们的shell都有万行以上。)

为什么它这么长呢?(至少对于我来说挺长的,虽然它在业界中是轻量级小型数据库。)我们来看看。

这里我们介绍介绍功能。

     (一)公共接口(Interface)

SQLite库的大部分公共接口由main.c, legacy.c和vdbeapi.c源文件中的函数来实现,这些函数依赖于分散在其他文件中的一些程序,因为在这些文件中它们可以访问有文件作用域的数据结构。

     (二)词法分析器(Tokenizer)

当执行一个包含SQL语句的字符串时,接口程序要把这个字符串传递给tokenizer。Tokenizer的任务是把原有字符串分割成一个个标识符(token),并把这些标识符传递给解析器。Tokenizer是用手工编写的,在C文件tokenize.c中。

     (三)语法分析器(Parser)

语法分析器的工作是在指定的上下文中赋予标识符具体的含义。SQLite的语法分析器使用Lemon LALR(1)分析程序生成器来产生,Lemon做的工作与YACC/BISON相同,但它使用不同的输入句法,这种句法更不易出错。Lemon还产生可重入的并且线程安全的语法分析器。Lemon定义了非终结析构器的概念,当遇到语法错误时它不会泄露内存。驱动Lemon的源文件可在parse.y中找到。

     (四)代码生成器(Code Generator)

语法分析器在把标识符组装成完整的SQL语句后,就调用代码生成器产生虚拟机代码,以执行SQL语句请求的工作。代码生成器包含许多文件:attach.c, auth.c, build.c, delete.c, expr.c, insert.c,pragma.c, select.c, trigger.c, update.c, vacuum.c和where.c。这些文件涵盖了大部分最重要、最有意义的事情。expr.c处理SQL中表达式的代码生成。where.c处理SELECT、UPDATE和DELETE语句中WHERE子句的代码生成。文件attach.c, delete.c, insert.c, select.c, trigger.c, update.c和vacuum.c处理同名SQL语句的代码生成(这些文件在必要时都调用expr.c和where.c中的例程)。所有其他SQL语句的代码由build.c生成。文件auth.c实现sqlite3_set_authorizer()的功能。

     (五)虚拟机(Virtual Machine)

代码生成器生成的代码由虚拟机来执行。总的来说,虚拟机实现一个专为操作数据库文件而设计的抽象计算引擎。它有一个存储中间数据的存储栈,每条指令包含一个操作码和不超过三个额外的操作数。

     (六)B-树(B-Tree)

一个SQLite数据库使用B-树的形式存储在磁盘上,B-树的实现位于源文件btree.c中。数据库中的每个表和索引使用一棵单独的B-树,所有的B-树存放在同一个磁盘文件中。文件格式的细节被记录在btree.c开头的备注里。B-树子系统的接口在头文件btree.h中定义。

     (七)页面高速缓存(Page Cache)

B-树模块以固定大小的数据块形式从磁盘上请求信息,默认的块大小是1024个字节,但是可以在512和65536个字节之间变化。页面高速缓存负责读、写和缓存这些数据块。页面高速缓存还提供回滚和原子提交的抽象,并且管理数据文件的锁定。B-树驱动模块从页面高速缓存中请求特定的页,当它想修改页面、想提交或回滚当前修改时,它也会通知页面高速缓存。页面高速缓存处理所有麻烦的细节,以确保请求能够快速、安全而有效地被处理。
页面高速缓存的代码实现被包含在单一的C源文件pager.c中。页面高速缓存子系统的接口在头文件pager.h中定义。

      (八) OS接口

为了在POSIX和Win32操作系统之间提供移植性,SQLite使用一个抽象层来提供操作系统接口。OS抽象层的接口在os.h中定义,每种支持的操作系统有各自的实现:Unix使用os_unix.c,Windows使用os_win.c,等等。每个特定操作系统的实现通常都有自己的头文件,如os_unix.h, os_win.h等。

     (九)实用工具(Utilities)

内存分配和字符串比较函数位于util.c中。语法分析器使用的符号表用Hash表来维护,其实现位于hash.c中。源文件utf.c包含Unicode转换子程序。SQLite有自己的printf()实现(带一些扩展功能),在printf.c中,还有自己的随机数生成器,在random.c中。

      (十)测试代码(Test Code)

如果你计算回归测试脚本,超过一半的SQLite代码将被测试。主要代码文件中有许多assert()语句。另外,源文件test1.c通过test5.c和md5.c实现只用于测试目的的一些扩展。os_test.c后端接口用来模拟断电,以验证页面高速缓存的崩溃恢复机制。

SQLite 由以下几个主要模块组成:SQL 编译器、内核、后端。

  • 接口:接口由SQLite C API组成,程序可以通过API与SQLite数据库进行交互。
  • 编译器:分词器(Tokenizer)和分析器(Parser)对执行的SQL语句进行语法检查,然后生成语法树方便VDBE代码生成器使用,然后把语法树传给VDBE代码生成器(Code Generator)处理。代码生成器根据语法树生成VDBE的类汇编语言操作码。
  • 后端:由B-树(B-tree),页缓存管理(pager)和操作系统接口构成。B-tree和Pager共同对数据进行管理。B-tree用与对数据建立索引,它维护着各个页面之间的复杂的关系,便于快速找到所需数据。Pager的主要作用就是通过OS接口在B-tree和硬盘之间传递页面。

以下我们将体验写数据库的感觉,虽然和真的sqlite差很多,但能实现增删改查功能。

首先,在我们输入之前,它会打印sqlite> ,然后接受输入进行判断,在输入不为.exit时程序会一直进行下去。所以程序应该有个while(1),它先输出sqlite>,然后接受输入,并对输入进行判断。如果是.exit就退出,如果不是就输出未知命令并继续。所以我们建立InputBuffer结构体,记录长度,指针,并进行初始化。

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

然后编写主函数

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

完善函数并做一些字符读取的判断

void print_prompt() { printf("sqlite>"); }
size_t getline(char **lineptr, size_t *n, FILE *stream);

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

  // 忽略换行符
  input_buffer->input_length = bytes_read - 1;
  input_buffer->buffer[bytes_read - 1] = 0;
}

lineptr:指向我们用来指向包含读取行的缓冲区的变量的指针。如果将其设置为“被 mallocatted”,因此应由用户释放,即使命令失败也是如此。

n:指向我们用于保存已分配缓冲区大小的变量的指针。

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

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

前面malloc了,就得有释放

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

再在主函数加上close_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);
    }
  }
}

好了,最简单的没有实装任何功能的数据库就完成了。我们运行一下。

以下是全部源代码

#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("sqlite>"); }

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
社区闲置共享系统需要设计一个数据库来保存用户信息、商品信息、订单信息等数据。以下是一个简单的数据库设计方案: 1. 用户表(User):保存用户信息,包括用户ID、用户名、密码、邮箱、电话等。 | 字段名称 | 数据类型 | 描述 | | -------- | -------- | ---- | | user_id | int | 用户ID,主键 | | username | varchar | 用户名 | | password | varchar | 密码 | | email | varchar | 邮箱 | | phone | varchar | 电话 | 2. 商品表(Product):保存商品信息,包括商品ID、商品名称、商品描述、价格、发布者ID等。 | 字段名称 | 数据类型 | 描述 | | ----------- | -------- | ---- | | product_id | int | 商品ID,主键 | | product_name| varchar | 商品名称 | | description | varchar | 商品描述 | | price | float | 商品价格 | | user_id | int | 发布者ID,外键,关联用户表的user_id字段 | 3. 订单表(Order):保存订单信息,包括订单ID、商品ID、买家ID、卖家ID、订单状态等。 | 字段名称 | 数据类型 | 描述 | | --------- | -------- | ---- | | order_id | int | 订单ID,主键 | | product_id| int | 商品ID,外键,关联商品表的product_id字段 | | buyer_id | int | 买家ID,外键,关联用户表的user_id字段 | | seller_id | int | 卖家ID,外键,关联用户表的user_id字段 | | status | int | 订单状态,0表示待支付,1表示已支付,2表示已完成 | 以上是一个简单的数据库设计方案,实际上还需要根据具体需求进行调整和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_51275728

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值