storage: byte documents in a per-collection slab (roadmap item 4)

Documents live as canonical BSON bytes in a segmented per-collection slab
(fixed 8 MiB segments keep capacity slack under one segment); the docs map
holds flat offsets that stay valid across segment growth, and removed
documents leave garbage bytes until compaction rewrites. The per-document
ArenaAllocator and its second full Pair-tree copy are gone.

The matcher walks the stored bytes directly, skipping by length any field
the filter does not name (a new bson byte-walker: element_key, skip_value,
read_value with borrowed leaves, get_at, and a borrowed spine parse). The
byte matcher is differential-tested against the tree matcher on a corpus
and shares its operator logic. Stored documents are never materialized on
the scan path or in aggregate $match; $group reads group keys and sums
straight off the bytes. Sort, projection, findAndModify, updates and
index entry generation use a borrowed spine into the slab (or the byte
collector, which also replaced collect_values in build_entries). The
compaction threshold now counts uncompressed data volume, since a
compressed log would otherwise never trigger.

Measured (tests/e2e/results/phase5.txt): server RSS 1979 -> 539 MB (2.4x
smaller than MongoDB; phase1 baseline 2.0 GB), range-scan 22.5 -> ~12 ms
(parity, best run faster than MongoDB), proj 4.1 -> 3.4 ms, createIndex
parity. Verified: unit suite in all three modes with zero leaks, the
crash pair, e2e6, and the stress/spill programs.
This commit is contained in:
2026-08-02 22:15:07 +03:00
parent b4585106f1
commit 570900a6ef
12 changed files with 985 additions and 253 deletions

View File

@@ -9,7 +9,10 @@ const bson = @import("bson.zig");
// Filter matching
// ---------------------------------------------------------------------------
pub const QueryError = error{OutOfMemory};
/// The byte matcher operates on stored (canonical, validated) bytes, so an
/// InvalidBson from the walker means a storage bug rather than hostile
/// input — but it must still be a possible error, not a panic.
pub const QueryError = error{ OutOfMemory, InvalidBson };
pub fn matches(gpa: std.mem.Allocator, filter: *const bson.Document, doc: *const bson.Document) QueryError!bool {
for (filter.pairs) |p| {
@@ -93,9 +96,31 @@ fn field_matches(gpa: std.mem.Allocator, path: []const u8, expected: bson.Value,
var candidates: std.ArrayListUnmanaged(bson.Value) = .empty;
defer candidates.deinit(alloc);
try collect_values(alloc, doc.pairs, path, &candidates, 0);
// MongoDB applies queries to array elements as well as the array itself.
// Index the snapshot length, re-reading items each iteration: appending
// may reallocate the buffer, which would invalidate a captured slice.
try expand_arrays(alloc, &candidates);
return apply_expected(gpa, expected, candidates.items);
}
/// The byte counterpart of field_matches: collects values by walking the
/// canonical BSON element stream of a stored document, skipping by length
/// any field the filter does not name.
fn field_matches_bytes(gpa: std.mem.Allocator, path: []const u8, expected: bson.Value, bytes: []const u8) QueryError!bool {
// An arena, not a stack fallback: the byte walker materializes nested
// doc/array values (whole-array equality, embedded docs) into the
// allocator it is given, and those must be freed with it.
var arena = std.heap.ArenaAllocator.init(gpa);
defer arena.deinit();
const alloc = arena.allocator();
var candidates: std.ArrayListUnmanaged(bson.Value) = .empty;
try collect_values_bytes(alloc, bytes, path, &candidates, 0);
try expand_arrays(alloc, &candidates);
return apply_expected(gpa, expected, candidates.items);
}
/// MongoDB applies queries to array elements as well as the array itself.
/// Index the snapshot length, re-reading items each iteration: appending
/// may reallocate the buffer, which would invalidate a captured slice.
fn expand_arrays(alloc: std.mem.Allocator, candidates: *std.ArrayListUnmanaged(bson.Value)) QueryError!void {
const direct_count = candidates.items.len;
var i: usize = 0;
while (i < direct_count) : (i += 1) {
@@ -104,7 +129,11 @@ fn field_matches(gpa: std.mem.Allocator, path: []const u8, expected: bson.Value,
for (a.array) |elem| try candidates.append(alloc, elem);
}
}
}
/// The operator/equality half of field matching, shared by the tree and
/// byte collectors.
fn apply_expected(gpa: std.mem.Allocator, expected: bson.Value, candidates: []const bson.Value) QueryError!bool {
if (is_operator_doc(expected)) |pairs| {
// $options modifies $regex wherever it appears in the document, so
// it has to be known before any operator runs.
@@ -115,24 +144,163 @@ fn field_matches(gpa: std.mem.Allocator, path: []const u8, expected: bson.Value,
for (pairs) |p| {
const op = parse_op(p.key);
if (op == .options) continue;
if (!try match_operator(gpa, op, p.value, candidates.items, options)) return false;
if (!try match_operator(gpa, op, p.value, candidates, options)) return false;
}
return true;
}
// Bare BSON regex value: {field: /re/} behaves like {$regex: "re"}.
if (expected == .regex) {
for (candidates.items) |a| {
for (candidates) |a| {
if (a == .string and regex_match(expected.regex.pattern, expected.regex.options, a.string)) return true;
}
return false;
}
// Bare equality — matches if any candidate equals the expected value.
for (candidates.items) |actual| {
for (candidates) |actual| {
if (bson.compare(actual, expected) == .eq) return true;
}
return false;
}
/// Whether a stored document (canonical BSON bytes) matches `filter` — the
/// byte-matcher counterpart of `matches`, used by scans. Same semantics,
/// different collection: fields the filter does not name are skipped by
/// length instead of materialized.
pub fn matches_bytes(gpa: std.mem.Allocator, filter: []const bson.Pair, bytes: []const u8) QueryError!bool {
for (filter) |p| {
if (p.key.len > 0 and p.key[0] == '$') {
if (!try match_top_level_bytes(gpa, p.key, p.value, bytes)) return false;
} else {
if (!try field_matches_bytes(gpa, p.key, p.value, bytes)) return false;
}
}
return true;
}
fn match_top_level_bytes(gpa: std.mem.Allocator, op: []const u8, value: bson.Value, bytes: []const u8) QueryError!bool {
if (std.mem.eql(u8, op, "$and") or std.mem.eql(u8, op, "$or")) {
const want_and = std.mem.eql(u8, op, "$and");
const filters = switch (value) {
.array => |arr| arr,
else => return false,
};
for (filters) |item| {
const f = switch (item) {
.doc => |pairs| pairs,
else => return false,
};
const matched = try matches_bytes(gpa, f, bytes);
if (want_and and !matched) return false;
if (!want_and and matched) return true;
}
return want_and;
}
if (std.mem.eql(u8, op, "$nor")) {
const filters = switch (value) {
.array => |arr| arr,
else => return false,
};
for (filters) |item| {
const f = switch (item) {
.doc => |pairs| pairs,
else => return false,
};
if (try matches_bytes(gpa, f, bytes)) return false;
}
return true;
}
return false;
}
/// Collect values reachable at `path` from a document's canonical bytes —
/// the byte counterpart of `collect_values`, with the same traversal, the
/// same order and the same multikey semantics. Appends into `out`.
pub fn collect_values_bytes(gpa: std.mem.Allocator, bytes: []const u8, path: []const u8, out: *std.ArrayListUnmanaged(bson.Value), depth: usize) QueryError!void {
var it = std.mem.splitScalar(u8, path, '.');
const first = it.next() orelse return;
const rest = it.rest();
var idx: usize = 4; // skip the document length prefix
while (idx + 1 < bytes.len and bytes[idx] != 0) {
const tag = bytes[idx];
idx += 1;
const key = bson.element_key(bytes, &idx) orelse return;
if (std.mem.eql(u8, key, first)) {
if (rest.len == 0) {
if (depth < 8) {
try out.append(gpa, try bson.read_value(gpa, bytes, &idx, tag));
} else {
try bson.skip_value(bytes, &idx, tag);
}
} else {
try collect_from_value_bytes(gpa, bytes, &idx, tag, rest, out, depth + 1);
}
} else {
try bson.skip_value(bytes, &idx, tag);
}
}
}
fn collect_from_value_bytes(gpa: std.mem.Allocator, bytes: []const u8, idx: *usize, tag: u8, path: []const u8, out: *std.ArrayListUnmanaged(bson.Value), depth: usize) QueryError!void {
if (depth > 8) {
try bson.skip_value(bytes, idx, tag);
return;
}
switch (tag) {
0x03 => {
const start = idx.*;
try collect_values_bytes(gpa, bytes[start..], path, out, depth);
idx.* = start + std.mem.readInt(u32, bytes[start..][0..4], .little);
},
0x04 => {
const start = idx.*;
const total: u32 = std.mem.readInt(u32, bytes[start..][0..4], .little);
const array_end = start + total;
var pit = std.mem.splitScalar(u8, path, '.');
const seg = pit.next() orelse return;
if (std.fmt.parseInt(usize, seg, 10)) |aidx| {
var e: usize = 0;
var a = start + 4;
while (a < array_end - 1 and bytes[a] != 0) {
const atag = bytes[a];
a += 1;
_ = bson.element_key(bytes, &a) orelse return;
if (e == aidx) {
const rest = pit.rest();
if (rest.len == 0) {
if (depth < 8) {
try out.append(gpa, try bson.read_value(gpa, bytes, &a, atag));
} else {
try bson.skip_value(bytes, &a, atag);
}
} else {
try collect_from_value_bytes(gpa, bytes, &a, atag, rest, out, depth + 1);
}
break;
}
try bson.skip_value(bytes, &a, atag);
e += 1;
}
} else |_| {
// Multikey semantics: descend into embedded documents.
var a = start + 4;
while (a < array_end - 1 and bytes[a] != 0) {
const atag = bytes[a];
a += 1;
_ = bson.element_key(bytes, &a) orelse return;
if (atag == 0x03) {
const dstart = a;
try collect_values_bytes(gpa, bytes[dstart..], path, out, depth);
}
try bson.skip_value(bytes, &a, atag);
}
}
idx.* = array_end;
},
else => try bson.skip_value(bytes, idx, tag),
}
}
/// The query operators, resolved from their names once per filter field
/// instead of re-comparing strings for every candidate document.
const Op = enum {
@@ -1285,6 +1453,101 @@ test "and/or filters" {
} } }}), &d));
}
test "byte matcher agrees with the tree matcher on a corpus" {
// The scan path matches stored documents as canonical BSON bytes,
// skipping fields by length; the tree path walks materialized pairs.
// They must agree exactly, so the byte matcher is checked against the
// existing matcher over a corpus that exercises scalars, operators,
// dot paths, multikey arrays, nested docs, $and/$or and missing fields.
const gpa = testing.allocator;
var prng = std.Random.DefaultPrng.init(0xB17E_7E);
const rand = prng.random();
const n = 200;
var bytes_list: std.ArrayListUnmanaged([]u8) = .empty;
defer {
for (bytes_list.items) |b| gpa.free(b);
bytes_list.deinit(gpa);
}
for (0..n) |_| {
const a = rand.intRangeAtMost(i32, 0, 10);
var pairs: [4]bson.Pair = undefined;
var np: usize = 0;
pairs[np] = .{ .key = "a", .value = .{ .int32 = a } };
np += 1;
pairs[np] = .{ .key = "b", .value = .{ .string = if (rand.boolean()) "x" else "y" } };
np += 1;
if (rand.boolean()) {
pairs[np] = .{ .key = "tags", .value = .{ .array = &.{ .{ .string = "m" }, .{ .string = "n" } } } };
np += 1;
}
if (rand.boolean()) {
pairs[np] = .{ .key = "d", .value = .{ .doc = &.{ .{ .key = "e", .value = .{ .int32 = a } } } } };
np += 1;
}
var out: std.ArrayListUnmanaged(u8) = .empty;
defer out.deinit(gpa);
try bson.write_doc(pairs[0..np], gpa, &out);
try bytes_list.append(gpa, try gpa.dupe(u8, out.items));
}
for (0..500) |case| {
const a = rand.intRangeAtMost(i32, 0, 12);
const s = if (rand.boolean()) "x" else "m";
// Values are copied into a per-iteration arena so nested literals
// cannot dangle.
var farena = std.heap.ArenaAllocator.init(gpa);
defer farena.deinit();
const fa = farena.allocator();
var f_pairs: std.ArrayListUnmanaged(bson.Pair) = .empty;
defer f_pairs.deinit(fa);
switch (rand.intRangeAtMost(u8, 0, 9)) {
0 => try f_pairs.append(fa, .{ .key = "a", .value = .{ .int32 = a } }),
1 => {
const v = try bson.copy_value(fa, .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = a } }} });
try f_pairs.append(fa, .{ .key = "a", .value = v });
},
2 => try f_pairs.append(fa, .{ .key = "b", .value = .{ .string = s } }),
3 => try f_pairs.append(fa, .{ .key = "tags", .value = .{ .string = s } }),
4 => try f_pairs.append(fa, .{ .key = "d.e", .value = .{ .int32 = a } }),
5 => {
const v = try bson.copy_value(fa, .{ .doc = &.{.{ .key = "$in", .value = .{ .array = &.{ .{ .int32 = a }, .{ .int32 = a + 1 } } } }} });
try f_pairs.append(fa, .{ .key = "a", .value = v });
},
6 => {
const v = try bson.copy_value(fa, .{ .array = &.{
.{ .doc = &.{.{ .key = "a", .value = .{ .int32 = a } }} },
.{ .doc = &.{.{ .key = "b", .value = .{ .string = s } }} },
} });
try f_pairs.append(fa, .{ .key = "$or", .value = v });
},
7 => {
const v = try bson.copy_value(fa, .{ .doc = &.{.{ .key = "$exists", .value = .{ .bool = rand.boolean() } }} });
try f_pairs.append(fa, .{ .key = "tags", .value = v });
},
8 => {
const v = try bson.copy_value(fa, .{ .doc = &.{
.{ .key = "$gte", .value = .{ .int32 = a } },
.{ .key = "$lt", .value = .{ .int32 = a + 3 } },
} });
try f_pairs.append(fa, .{ .key = "a", .value = v });
},
else => try f_pairs.append(fa, .{ .key = "missing", .value = .{ .int32 = a } }),
}
const filter_doc = bson.Document{ .arena = undefined, .pairs = f_pairs.items };
for (bytes_list.items) |bytes| {
var doc = try bson.Document.parse(gpa, bytes);
defer doc.deinit();
const tree = try matches(gpa, &filter_doc, &doc);
const byt = try matches_bytes(gpa, f_pairs.items, bytes);
if (tree != byt) {
std.debug.print("case {d}: filter mismatch: tree={} bytes={}\n", .{ case, tree, byt });
return error.ByteMatcherMismatch;
}
}
}
}
/// Public single-value operator matcher, used by $pull and $elemMatch.
pub fn value_matches_operator(gpa: std.mem.Allocator, op: []const u8, value: bson.Value, actual: bson.Value) QueryError!bool {
var single: [1]bson.Value = .{actual};