Files
MultiforaDB/build.zig
Aleksey Shakhmatov 06504127fb index: route arena access through accessors; tighten reserve_for's bound
Groundwork for M0: the node arena and overflow slab are about to move into an
mmap'd data file where a write to a page belonging to the last durable
checkpoint has to copy that page first (PLAN amendment A1). Two changes make
that a small commit rather than a sixty-site one, plus the reformat of this
file (see the preceding style commit for why it rides along here).

Accessors. Every read of a node page now goes through page(), every write
through page_mut(), and every overflow read through ovf(); nothing else touches
nodes.items or overflow.items. Which of the 55 sites mutate was decided by the
compiler rather than by inspection -- page() returns *const Node, so every
mutating site failed to compile until flipped -- and the result is that the
copy-on-write hook has exactly one home. Records the rule COW will impose
(never hold a *Node across a page_mut of the same id) and the audit showing
today's callers already comply.

Comptime layout asserts. These structures are about to become an on-disk
format, and nothing pinned them. Pinning also surfaced that @sizeOf(Slot) is
32, not the 20 its 160 declared bits suggest -- the backing integer's 16-byte
alignment rounds it up, so 12 of every 32 slot bytes are padding and a node
holds 127 slots where 203 would fit. Pinned, deliberately not fixed: narrowing
the slot changes the fanout and so the on-disk shape of every index, which
belongs in the commit that reshapes leaf records.

reserve_for. The old bound stood in for "levels a batch can add" with n/8,
which is ~125 levels for a 1000-entry batch and demands ~528 MiB of headroom.
Growing by g levels needs at least 2^g entries, so log2_ceil(n+1)+1 bounds it,
giving ~70 MiB for that batch. Harmless as ArrayList capacity; real file growth
once the arena is file-backed. Overrunning the reservation is a buffer overrun
on a path that has already appended to the log and cannot report failure, so
alloc_node and store_record now assert, using assert.zig so the checks survive
ReleaseFast. Mutation-checked by dropping the reservation entirely: six tests
go red with the new message. Worth noting the assert guards the allocation, not
the arithmetic -- ensureUnusedCapacity over-allocates, so a slightly-too-small
bound is masked until the reservation becomes exact.

build.zig gains a `fuzz` step. spill, spill2, stress and fuzz_split were in no
build step and are not in lib.zig's test block, so `zig build test` could not
see an API break in the only coverage for records past the inline limit and for
randomized split/remove interleavings -- exactly what this work puts at risk.
2026-08-03 17:09:03 +03:00

79 lines
2.9 KiB
Zig

const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
// ReleaseFast by default: a Debug server is 10-200x slower (measured in
// tests/e2e/compare-run.sh). Devs can still opt into Debug or
// ReleaseSafe with -Doptimize=Debug / -Doptimize=ReleaseSafe.
const optimize = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size") orelse .ReleaseFast;
const lib_mod = b.createModule(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
});
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "mongo", .module = lib_mod },
},
});
const exe = b.addExecutable(.{
.name = "multiforadb",
.root_module = exe_mod,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
const run_step = b.step("run", "Run multiforadb server");
run_step.dependOn(&run_cmd.step);
const test_mod = b.createModule(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
});
const test_step = b.addTest(.{
.root_module = test_mod,
});
const run_tests = b.addRunArtifact(test_step);
const test_help = b.step("test", "Run unit tests");
test_help.dependOn(&run_tests.step);
// The B+tree harnesses were in no build step, so `zig build test` -- which
// only compiles src/lib.zig's test block -- could not see an API break in
// them. They are also the only coverage for records past the inline limit
// and for randomized split/remove interleavings, i.e. exactly what M0's
// arena work puts at risk. Wire them up so they cannot rot unnoticed.
//
// Kept out of `test` because stress.zig runs for seconds and the three
// main harnesses print rather than assert-and-exit; `zig build fuzz` is
// the gate to run alongside the e2e matrix on any index change.
const fuzz_step = b.step("fuzz", "Run the B+tree stress and fuzz harnesses");
const fuzz_split_mod = b.createModule(.{
.root_source_file = b.path("src/fuzz_split.zig"),
.target = target,
.optimize = optimize,
});
const fuzz_split = b.addTest(.{ .root_module = fuzz_split_mod });
fuzz_step.dependOn(&b.addRunArtifact(fuzz_split).step);
for ([_][]const u8{ "spill", "spill2", "stress" }) |name| {
const mod = b.createModule(.{
.root_source_file = b.path(b.fmt("src/{s}.zig", .{name})),
.target = target,
.optimize = optimize,
});
const harness = b.addExecutable(.{ .name = name, .root_module = mod });
fuzz_step.dependOn(&b.addRunArtifact(harness).step);
}
}