🎓 A1Learner
🔍
0 of 46 lessons completed

Zig Project: Database Engine

A database engine is a program that stores values under keys and finds them again later. This lesson builds a small one that keeps its records in a file.

Store values in memory

The store holds each value under its key. A lookup by key returns the stored value.

const std = @import("std");
pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var out = std.io.getStdOut().writer();
    var store = std.StringHashMap([]const u8).init(alloc);
    try store.put("name", "zig");
    try store.put("level", "advanced");
    try out.print("name={s}\n", .{store.get("name").?});
    try out.print("level={s}\n", .{store.get("level").?});
    try out.print("count={d}\n", .{store.count()});
}

Save records to a file

Saving writes one record per line in a fixed order. The file is created fresh on every save.

const std = @import("std");
pub fn main() !void {
    var out = std.io.getStdOut().writer();
    const Entry = struct { key: []const u8, value: []const u8 };
    const entries = [_]Entry{ .{ .key = "name", .value = "zig" }, .{ .key = "level", .value = "advanced" } };
    var file = try std.fs.cwd().createFile("db.txt", .{});
    defer file.close();
    var w = file.writer();
    for (entries) |e| {
        try w.print("{s}={s}\n", .{ e.key, e.value });
    }
    try out.print("saved {d} entries\n", .{entries.len});
}

Load records from a file

Loading reads the file, splits each line at the equals sign, and rebuilds the store. The program prints the values in a fixed order.

const std = @import("std");
pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var out = std.io.getStdOut().writer();
    {
        var file = try std.fs.cwd().createFile("db.txt", .{});
        defer file.close();
        var w = file.writer();
        try w.print("name={s}\n", .{"zig"});
        try w.print("level={s}\n", .{"advanced"});
    }
    const data = try std.fs.cwd().readFileAlloc(alloc, "db.txt", 4096);
    var store = std.StringHashMap([]const u8).init(alloc);
    var it = std.mem.splitScalar(u8, data, '\n');
    while (it.next()) |line| {
        if (line.len == 0) continue;
        const eq = std.mem.indexOfScalar(u8, line, '=') orelse continue;
        try store.put(line[0..eq], line[eq + 1 ..]);
    }
    try out.print("name={s}\n", .{store.get("name").?});
    try out.print("level={s}\n", .{store.get("level").?});
    try out.print("count={d}\n", .{store.count()});
}

Example

const std = @import("std");
pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var out = std.io.getStdOut().writer();
    const Entry = struct { key: []const u8, value: []const u8 };
    const entries = [_]Entry{ .{ .key = "alpha", .value = "one" }, .{ .key = "beta", .value = "two" }, .{ .key = "gamma", .value = "three" } };
    {
        var file = try std.fs.cwd().createFile("db.txt", .{});
        defer file.close();
        var w = file.writer();
        for (entries) |e| {
            try w.print("{s}={s}\n", .{ e.key, e.value });
        }
    }
    const data = try std.fs.cwd().readFileAlloc(alloc, "db.txt", 4096);
    var store = std.StringHashMap([]const u8).init(alloc);
    var it = std.mem.splitScalar(u8, data, '\n');
    while (it.next()) |line| {
        if (line.len == 0) continue;
        const eq = std.mem.indexOfScalar(u8, line, '=') orelse continue;
        try store.put(line[0..eq], line[eq + 1 ..]);
    }
    try out.print("alpha={s}\n", .{store.get("alpha").?});
    try out.print("beta={s}\n", .{store.get("beta").?});
    try out.print("gamma={s}\n", .{store.get("gamma").?});
    try out.print("loaded {d} entries\n", .{store.count()});
}

Key points

  • Key-value store: a key-value store is a collection that pairs each key with one value.
  • Persistence: persistence is the property of data surviving after a program ends, here by writing it to a file.
  • Lookup: a lookup is the act of finding a value by its key.
  • Record: a record is one key and one value written on one line of the file.
  • Reload: a reload is the act of reading saved records back into memory.
  • Overwrite: an overwrite is replacing the old file contents with new contents when saving again.

The program writes three records to a file. It then reads the file back into a fresh store. Finally it prints each value by key, in a fixed order, plus the loaded count.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

What does the store print for a key that was saved to the file and then reloaded?