一、前言
数据库DBMS是当前互联网开发者最熟悉的基础设施之一,很多后端开发者的入门项目就是一个简单的基于MySQL的数据管理系统。笔者一直想自己编写一个简单的SQL数据库,正好最近正在学习RocksDB和Zig语言的内容,就想利用这个机会作为学习成果的检验,于是就有了这个小项目。
话不多说,先来看看这个小项目成品的效果:

当然这个小小的数据库只支持创建表、插入和简单查询的能力,并不包含复杂SQL、索引、多租户、事务等高级功能,有兴趣的同学可以自行扩展。
二、什么是RocksDB
RocksDB是由Facebook开发的一款高效的嵌入式键值存储引擎,基于Google的LevelDB进行了多项优化。它主要用于快速存储和高并发读写场景,特别适合在闪存等快速存储介质上运行。
RocksDB是C++开发的,不过它提供了一套C语言API,为不会C++的开发者提供了便利。
三、什么是Zig语言
Zig语言是一种新兴的系统编程语言,由Andrew Kelley于2015年开始开发。其设计目标是改进C语言,并借鉴Rust等其他语言的优点。Zig强调强健性、最佳性和可维护性,并致力于提供高效的手动内存管理和编译时特性。
Zig语言对开发者来说最好的一点就是简单(特别相比Rust来说,Rust笔者已经学了好几次都没能入门),如果你熟悉Rust或者C语言,那么上手Zig只需要2~3天,就算完全没有C家族语言的经验,2周也足够学这门语言。
Zig语言有这些值得关注的特点:
- 并没有提供类似Rust的生命周期管理,所以需要手动管理内存和C一样;
- Zig支持编译时执行代码(comptime),允许开发者在编译期间进行类型操作和函数调用,从而提高性能和灵活性(Go语言开发者默泪);
- 可以无缝衔接C语言的API,继承C生态的库非常简单;
- 强大的编译工具ZigCC,ZigCC 是一个强大的工具,旨在简化 C/C++ 项目的编译过程,同时提供现代编程语言的优势。它不仅提高了编译效率,还通过增量编译和高效的二进制生成,帮助开发者更好地管理和维护他们的代码库。
四、项目结构
为了实现这个简单SQL数据库,我们可以按照如下的架构来对这个项目做功能分层:

接下来,我们就分模块详解介绍。
五、实现解析
RocksDB Layer
我们第一步先完成对RocksDB库的桥接,RocksDB虽然提供了C的API,但是并没有过多的文档说明,好在RocksDB提供了一些使用的例子,我们可以根据这些例子知道这些API的用法。
Zig集成C语言的三方库非常容易:
- 首先,我们把RocksDB的API声明加入项目的include目录

- 第二步,我们添加一个RocksDB.zig文件,调用header中的API,由于这个项目比较简单,所以我们只需要set/get/iteration这几个简单的API:
const std = @import("std");
const rdb = @cImport(@cInclude("rocksdb.h"));
pub const Iter = @import("iter.zig").Iter;
pub fn init(allocator: std.mem.Allocator, dir: []const u8) !Self {
const options: ?*rdb.rocksdb_options_t = rdb.rocksdb_options_create();
rdb.rocksdb_options_set_create_if_missing(options, 1);
var err: ?[*:0]u8 = null;
const db = rdb.rocksdb_open(options, dir.ptr, &err);
const r = Self{
.db = db.?,
.allocator = allocator,
.dir = dir,
};
if (err) |errStr| {
std.log.err("Failed to open RocksDB: {s}.\n", .{errStr});
return RocksdbErrors.RocksDBOpenError;
}
return r;
}
pub fn deinit(self: Self) void {
rdb.rocksdb_close(self.db);
}
pub fn set(self: Self, key: []const u8, value: []const u8) !void {
const writeOptions = rdb.rocksdb_writeoptions_create();
var err: ?[*:0]u8 = null;
rdb.rocksdb_put(
self.db,
writeOptions,
key.ptr,
key.len,
value.ptr,
value.len,
&err,
);
if (err) |errStr| {
std.log.err("Failed to write RocksDB: {s}.\n", .{errStr});
return RocksdbErrors.RocksDBWriteError;
}
}
pub fn get(self: Self, key: []const u8, buf: *std.ArrayList(u8)) !void {
const readOptions = rdb.rocksdb_readoptions_create();
var value_length: usize = 0;
var err: ?[*:0]u8 = null;
const v = rdb.rocksdb_get(
self.db,
readOptions,
key.ptr,
key.len,
&value_length,
&err,
);
if (v == null) {
return;
}
if (err) |errStr| {
std.log.err("Failed to read RocksDB: {s}.\n", .{errStr});
return RocksdbErrors.RocksDBReadError;
}
for (0..value_length) |i| {
try buf.append(v[i]);
}
}
pub fn getIter(self: Self, prefix: []const u8) !Iter {
return Iter.init(self.db, prefix);
}
- 第三步,我们需要在build.zig中添加RocksDB三方库的link,关于Zigcc的具体细节可以参考这篇文章(https://ziglang.org/learn/build-system/)。
const rocksdb_unit_tests = b.addTest(.{
.root_source_file = b.path("src/Rocksdb.zig"),
.target = target,
.optimize = optimize,
});
rocksdb_unit_tests.linkLibC();
rocksdb_unit_tests.addIncludePath(LazyPath{ .cwd_relative = "./include" });
rocksdb_unit_tests.linkSystemLibrary("rocksdb");
rocksdb_unit_tests.addLibraryPath(LazyPath{ .cwd_relative = "/opt/homebrew/lib" });
const run_rocksdb_unit_tests = b.addRunArtifact(rocksdb_unit_tests);
Lexer
在完成了RocksDB的桥接层后,我们可以着手编写SQL语句的词法分析器。顾名思义,词法分析就是将用户输入的SQL语句转换为一组Token的过程。
- 第一步,我们需要总结一下SQL语句中总共有哪些分词:

分析上述的几个SQL语句,我们可以将Token分为如下分类:
pub const Kind = enum {
unknown,
// ----------- 保留关键字 ------------
select_keyword, // select
create_table_keyword, // create table
insert_keyword, // insert into
values_keyword, // values
from_keyword, // from
where_keyword,// where
// ----------- 运算符关键字 -------------
plus_operator,// +
equal_operator,// =
lt_operator,// <
gt_operator, // >
concat_operator, // ||
// ---------- 符号关键字 -------------
left_paren_syntax, // (
right_paren_syntax, // )
comma_syntax, // ,
identifier, // 普通标识符
integer, // 整型
string, // 字符串
pub fn toString(self: Kind) []const u8 {
return @tagName(self);
}
};
const BUILTINS = [_]Builtin{
.{ .name = "CREATE TABLE", .Kind = Token.Kind.create_table_keyword },
.{ .name = "INSERT INTO", .Kind = Token.Kind.insert_keyword },
.{ .name = "SELECT", .Kind = Token.Kind.select_keyword },
.{ .name = "VALUES", .Kind = Token.Kind.values_keyword },
.{ .name = "WHERE", .Kind = Token.Kind.where_keyword },
.{ .name = "FROM", .Kind = Token.Kind.from_keyword },
.{ .name = "||", .Kind = Token.Kind.concat_operator },
.{ .name = "=", .Kind = Token.Kind.equal_operator },
.{ .name = "+", .Kind = Token.Kind.plus_operator },
.{ .name = "<", .Kind = Token.Kind.lt_operator },
.{ .name = ">", .Kind = Token.Kind.gt_operator },
.{ .name = "(", .Kind = Token.Kind.left_paren_syntax },
.{ .name = ")", .Kind = Token.Kind.right_paren_syntax },
.{ .name = ",", .Kind = Token.Kind.comma_syntax },
};
- 第二步,我们定义一下Token的数据结构:
start: u64,
end: u64,
kind: Kind,
source: []const u8,
pub fn init(start: u64, end: u64, kind: Kind, source: []const u8) Self {
return Self{
.start = start,
.end = end,
.kind = kind,
.source = source,
};
}
pub fn getKind(self: Self) Kind {
return self.kind;
}
pub fn string(self: Self) []const u8 {
return self.source[self.start..self.end];
}
- 第三步,我们需要完成上述种类中三类关键字的解析:
fn nextKeyword(self: *LexIterator) ?Token {
var longest_len: usize = 0;
var kind = Token.Kind.unknown;
for (BUILTINS) |builtin| {
if (self.index + builtin.name.len > self.source.len) continue;
// 大小写不敏感
if (asciiCaseInsensitiveEqual(self.source[self.index .. self.index + builtin.name.len], builtin.name)) {
longest_len = builtin.name.len;
kind = builtin.Kind;
break;
}
}
// 由于我们关键字是按长度倒排序的,所以匹配到的一定是最长的keyword
if (longest_len == 0) return null;
defer self.index += longest_len;
return Token.init(self.index, self.index + longest_len, kind, self.source);
}
- 第四步,完成整型、字符串和普通标识符解析:
fn nextInteger(self: *LexIterator) ?Token {
var end = self.index;
var i = self.index;
while (i < self.source.len and self.source[i] >= '0' and self.source[i] <= '9') {
end += 1;
i += 1;
}
if (self.index == end) return null;
defer self.index = end;
return Token.init(self.index, end, Token.Kind.integer, self.source);
}
fn nextString(self: *LexIterator) ?Token {
var i = self.index;
if (self.source[i] != '\'') return null;
i += 1;
const start = i;
var end = i;
while (i < self.source.len and self.source[i] != '\'') {
end += 1;
i += 1;
}
if (self.source[i] == '\'') i += 1;
if (start == end) return null;
defer self.index = i;
return Token.init(start, end, Token.Kind.string, self.source);
}
fn nextIdentifier(self: *LexIterator) ?Token {
var i = self.index;
var end = self.index;
while (i < self.source.len and ((self.source[i] >= 'a' and self.source[i] <= 'z') or (self.source[i] >= 'A' and self.source[i] <= 'Z') or self.source[i] == '*')) {
i += 1;
end += 1;
}
if (self.index == end) return null;
defer self.index = end;
return Token.init(self.index, end, Token.Kind.identifier, self.source);
}
- 第五步,按照关键字>整型>字符串>普通标识符的优先级完成Lexer
pub fn hasNext(self: *LexIterator) bool {
self.index = eatWhitespace(self.source, self.index);
return self.index < self.source.len;
}
pub fn next(self: *LexIterator) !Token {
// std.debug.print("index: {d}, len: {d}, src: {s}\n", .{ self.index, self.source.len, self.source[self.index..] });
self.index = eatWhitespace(self.source, self.index);
if (self.index >= self.source.len) return error.OutOfSource;
if (self.nextKeyword()) |token| {

最低0.47元/天 解锁文章
437

被折叠的 条评论
为什么被折叠?



