C语言从零实现SQLite数据库(一)

前言

最近学习了一篇国外大神用C语言仿写SQLite数据库的文章,受益匪浅,因此萌生了翻译该文章的念头。原文作者选择SQLite作为仿写对象是因为它是一个轻量级的数据库,在设计和功能上都相对精简。本文共分为十三个章节介绍如何实现SQLite,从一个简单的REPL开始,逐步增加功能,到最终实现数据库的基本功能。本文适合有一定C语言基础且对数据库感兴趣的程序员,他们可以从中学到数据库的基础知识、SQLite的设计思想以及一些复杂数据结构在工程中的实践应用方法等。

在正式开始之前,有几个问题想让大家思考一下:

  • 数据库中的数据在计算机上是以什么形式存储的?(在内存和硬盘上)
  • 数据库什么时候需要把数据从内存搬到硬盘?
  • 为什么数据库中每一个表(table)有且只有一个主键(primary key)?
  • 数据库的回滚事务(rollback transaction)是怎样工作的?
  • 数据库如何格式化索引?
  • 什么时候会发生全表扫描?如何进行?
  • 内置的预定义语句是以什么样的格式保存的?

能回答出上述问题说明你已经对数据库有了比较全面的理解,如果有答不上来的问题,可以带着这些问题继续阅读后文,看看能不能从中找到答案。

SQLite简介

关于SQLite在他们的官网上有很多介绍文档,下图是对SQLite架构的抽象表示。
sqlite architecture
为了实现一次数据查询或者修改,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

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您可以使用C语言中的SQLite API来实现读取SQLite3数据库中的所有数据。下面是一个简单的示例代码: ```c #include <stdio.h> #include <sqlite3.h> int callback(void* data, int argc, char** argv, char** azColName) { for (int i = 0; i < argc; i++) { printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL"); } printf("\n"); return 0; } int main() { sqlite3* db; char* errMsg = 0; // 打开数据库连接 int rc = sqlite3_open("your_database.db", &db); if (rc) { fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db)); return 1; } // 执行查询语句 char* sql = "SELECT * FROM your_table;"; rc = sqlite3_exec(db, sql, callback, 0, &errMsg); if (rc != SQLITE_OK) { fprintf(stderr, "SQL查询错误: %s\n", errMsg); sqlite3_free(errMsg); return 1; } // 关闭数据库连接 sqlite3_close(db); return 0; } ``` 请注意,上述代码假设您已经安装了SQLite C语言接口库,并且数据库文件名为 "your_database.db",表名为 "your_table"。您可以根据实际情况进行修改。 该示例中的回调函数 `callback` 用于处理查询结果集中的每一行数据。在这里,我们只是简单地打印出每个列的名称和对应的值,您可以根据需要对数据进行进一步处理。 在代码中,我使用了SQLite的函数 `sqlite3_open` 打开数据库连接,`sqlite3_exec` 执行查询语句,并通过 `sqlite3_close` 关闭数据库连接。如果在打开数据库或执行查询语句时出现错误,相应的错误信息将被打印到标准错误流中。 希望这能帮助到您!如果您有任何问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值