手写SQL编译器 - Chapter 1

本文介绍了如何逐步搭建一个SQL编译器的框架,从基础的REPL(Read-Eval-Print Loop)开始。文章选择了SQLite作为起点,解释了SQL语句从输入到执行的流程,涉及词法解析器、语法解析器、代码生成器和虚拟机等概念。还实现了简单的REPL,允许用户输入SQL查询,并展示了如何逐步升级这个REPL以处理更复杂的输入。
Seed-Coder-8B-Base

Seed-Coder-8B-Base

文本生成
Seed-Coder

Seed-Coder是一个功能强大、透明、参数高效的 8B 级开源代码模型系列,包括基础变体、指导变体和推理变体,由字节团队开源

数据库是怎么工作的?

当你打开这篇文章的时候你一定也好奇:

  • SQL语句为什么能对数据库操作
  • 事务回滚是怎么滚的?
  • 数据是怎么被保存到硬盘里的?

接下来我们将一步步搭建一个框架并逐渐完善,并尝试着把这些问题讲明白。

我选择基于 SQLite ,因为 SQLite 体积小又功能齐全,整个数据库就存储在一个文件中。
我认为可以以此为起点以小见大,启发大家对数据库有更多的思考。

一个 SQL 语句需要经过一连串的过程才能检索或修改数据。

  1. 前端包括:
    • tokenizer 词法解析器
    • parser 语法解析器
    • code generator 代码生成器

前端输入一个 SQL 查询,输出为 SQLite 虚拟机字节码。字节码本质上就是可以在数据库上运行的程序。

  1. 后端包括:
    • Virtual Machine 虚拟机
    • B-Tree B 树
    • Pager 页缓存管理器
    • OS Interface 操作系统接口
      虚拟机将前端生成的字节码作为指令,可以对一个或多个索引执行操作,每个表或索引都存储在 B 树上(B 树:一种树状数据结构)。其实虚拟机本质上就是一堆判断字节码指令的 switch 语句。

B 树由许多结点组成。每个结点的长度为一页。B 树可以通过向 pager 发出指令来执行 从磁盘检索页面或保存回磁盘的操作。

pager通过接收指令读取或写入页,它负责以适当的偏移量在数据库文件中进行读取或写入。它还在内存中保留了最近访问页面的缓存,并决定何时将这些页面写回磁盘。

操作系统接口与上面提到的不在同一层。具体什么样的接口取决于为哪个操作系统的 SQLite 编译。在这个系列中,暂不支持跨平台。


本系列环境:
【操作系统】:Ubuntu 20.04 LTS
【语言】:C 语言
【编译器】: GCC 9.3.0

前置知识要求:
1. 会C语言,不要求精通,懂基本的 顺序结构、分支结构、循环结构 和 指针即可。
2. 对数据结构略有耳闻
3. 会 SQL 或 了解 SQLite

先整点好玩的~~

REPL

REPL,全称 Read Eval Print Loop交互式解释器

例如 Python 安装以后就自带 REPL ,在命令行下输入 Python 就可以启动

$ python
Python 3.8.5 (default, Aug 19 2020, 17:31:43) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 
>>> print("Hello World!")
Hello World!
>>> 

动手实现

现在我们也自己实现一个

// MyREPL.c 

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

int main() {
    char command[100];
    
    while (true) {
    	printf("BoiiDB > ");    // 打印提示符
        scanf("%s", command);   // 接收输入
        
        if(strcmp(command, ".q") == 0) {    // 判断退出条件
        	printf("Bye~ \n");
            exit(EXIT_SUCCESS);     // 退出程序
        } else {
        	printf("You typed the command [ %s ].\n", command);
        }
    }
}

编译后执行的效果如下:

$ gcc MyREPL.c -o go
$ ./go
BoiiDB > Hello
You typed the command [ Hello ].
BoiiDB > REPL 
You typed the command [ REPL ].
BoiiDB > .q
Bye~
$

非常简单,就是做一个无限循环,不断接收输入。
在接受输入之前会先打印提示符 BoiiDB >
然后通过判断输入的是不是 .q 来决定是否退出。

这就是一个最最简单的 REPL 了。

这只是一个雏形,让你感受一下 REPL , 给你一点信心。接下来我们继续完善。

升级版

首先我们不能像上面一样使用一个固定 100 个字符的字符数组来保存用户输入的内容了。
REPL 有时候可能用户会输入很长很长的语句,或者压根没输入。
我们需要专门定义一个结构体来保存用户输入的内容。

typedef struct {
    char *buffer;
    size_t buffer_length;
    ssize_t input_length;
}InputBuffer;

这里我们定义了一个 InputBuffer 结构体,结构体中有三个属性:一个字符指针、一个 size_t 型的字符长度、一个 ssize_t 的输入长度


[ ========== TIPS ========== ]

size_t 是无符号超长整型,一般被用来存放长度,ssize_t 是超长整型,也是被用来存放长度。

它们的定义长这样:

typedef unsigned long long size_t
typedef long long ssize_t

也就是说:

size_tunsigned long long 的别名

ssize_tlong long 的别名


在使用这种结构体之前我们需要将其先初始化,在退出之前也要将占用的空间释放:

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

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

接着我们的提示符和输入也要规范起来。我们把它们封装成两个函数

void print_prompt() {
	printf("BoiiDB > ");
}

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

[ ========== TIPS ========== ]

注意:在windows 下使用C语言,一般使用不了这个 getline() 函数,因为没有定义。

getline() 的函数签名为:

ssize_t getline(char** lineprt, size_t* n, FILE* stream)

它的作用是:
读取一行内容到 *lineptr 所指定的 buffer 中(所以我们才要定义 InputBuffer 结构体),当遇到换行符或 EOF 时读取结束。

*n*lineprt 所指定的 buffer 的大小,如果 *n 小于读入的内容长度,getline 会自动扩展 buffer 的长度,并更新 *lineprt*n 的值。

读取成功时返回读入字符的个数,失败时返回 -1.

Tip:getline 读取的内容中会包括行末的 ‘\n’ 字符。


最后修改一下 main 函数:

int main() {
    InputBuffer* input_buffer = new_input_buffer();
    while (true) {
    	print_prompt();             // 打印提示符
        read_input(input_buffer);   // 读取输入
        
        // 如果没有输入只是回车,则跳过
        if (input_buffer->input_length == 0) 
            continue;
        
        if (strcmp(input_buffer->buffer, ".q") == 0) {  // 如果输入退出指令
            close_input_buffer(input_buffer);
            exit(EXIT_SUCCESS);     // 退出程序
        } else {
        	printf("Unrecognized command [ %s ].\n", input_buffer->buffer);
        }  
    }
}

OK!这样我们就实现了一个比较正式的 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("BoiiDB > "); }

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

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

int main()
{
    InputBuffer* input_buffer = new_input_buffer();
    while (true) {
        print_prompt();
        read_input(input_buffer);

        if (input_buffer->input_length == 0) 
            continue;

        if (strcmp(input_buffer->buffer, ".q") == 0) {
            close_input_buffer(input_buffer);
            exit(EXIT_SUCCESS);
        } else {
            printf("Unrecognized command [ %s ].\n", input_buffer->buffer);
        }
    }
}

后续敬请期待,等我整理完再发出来。

您可能感兴趣的与本文相关的镜像

Seed-Coder-8B-Base

Seed-Coder-8B-Base

文本生成
Seed-Coder

Seed-Coder是一个功能强大、透明、参数高效的 8B 级开源代码模型系列,包括基础变体、指导变体和推理变体,由字节团队开源

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

TCP404

老板大方~

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

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

打赏作者

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

抵扣说明:

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

余额充值