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

@@ -217,35 +217,38 @@ pub const Index = struct {
/// equality on `{tags: ["a","b"]}` are covered. Returns an empty list
/// for a sparse index when a path yields no values (the document is
/// skipped); a non-sparse index indexes missing fields as null.
pub fn build_entries(self: *const Index, gpa: std.mem.Allocator, doc: *const bson.Document, id: []const u8) !BuiltEntries {
pub fn build_entries(self: *const Index, gpa: std.mem.Allocator, doc: []const u8, id: []const u8) !BuiltEntries {
// One arena for the whole call: the collected values and any nested
// spines the byte walker materializes (whole-array/document values)
// live here, so nothing leaks. The finished keys are still
// exact-sized gpa copies that BuiltEntries owns.
var arena = std.heap.ArenaAllocator.init(gpa);
defer arena.deinit();
const a = arena.allocator();
var per_path: std.ArrayListUnmanaged(std.ArrayListUnmanaged(bson.Value)) = .empty;
defer {
for (per_path.items) |*list| list.deinit(gpa);
per_path.deinit(gpa);
}
var multikey = false;
var multi_paths: usize = 0;
for (self.keys) |k| {
var values: std.ArrayListUnmanaged(bson.Value) = .empty;
errdefer values.deinit(gpa);
try query.collect_values(gpa, doc.pairs, k.path, &values, 0);
try query.collect_values_bytes(a, doc, k.path, &values, 0);
// Index the array itself and each element, like field_matches.
const direct = values.items.len;
var i: usize = 0;
while (i < direct) : (i += 1) {
if (values.items[i] == .array) {
multikey = true;
for (values.items[i].array) |elem| try values.append(gpa, elem);
for (values.items[i].array) |elem| try values.append(a, elem);
}
}
if (direct > 1) multikey = true;
if (values.items.len > 1) multi_paths += 1;
if (values.items.len == 0) {
if (self.sparse) return .{ .entries = .empty, .multikey = false };
try values.append(gpa, .null);
try values.append(a, .null);
}
try per_path.append(gpa, values);
try per_path.append(a, values);
}
if (multi_paths > 1) return error.ParallelArrays;
@@ -263,10 +266,9 @@ pub const Index = struct {
// One reused buffer; each finished key is copied out to its own
// exact-sized allocation.
var enc: std.ArrayListUnmanaged(u8) = .empty;
defer enc.deinit(gpa);
while (true) {
enc.clearRetainingCapacity();
for (0..nkeys) |ci| try bson.encode_key(per_path.items[ci].items[choice[ci]], gpa, &enc);
for (0..nkeys) |ci| try bson.encode_key(per_path.items[ci].items[choice[ci]], a, &enc);
const key = try gpa.dupe(u8, enc.items);
errdefer gpa.free(key);
try out.append(gpa, .{ .key = key, .id = id });
@@ -326,7 +328,7 @@ pub const Index = struct {
/// With `enforce_unique` false a duplicate is tolerated rather than
/// rejected (the rebuild path keeps the index and warns); the return
/// value reports whether that happened.
pub fn add_doc(self: *Index, gpa: std.mem.Allocator, doc: *const bson.Document, id: []const u8, enforce_unique: bool) !bool {
pub fn add_doc(self: *Index, gpa: std.mem.Allocator, doc: []const u8, id: []const u8, enforce_unique: bool) !bool {
var built = try self.build_entries(gpa, doc, id);
// Runs on success too: the batch's keys are copied into the tree,
// so deinit frees exactly what this call allocated.
@@ -352,7 +354,7 @@ pub const Index = struct {
/// every entry, so building an index over n documents would move O(n²)
/// bytes — that was the whole cost of createIndex on a large
/// collection. Staging and packing is O(n log n) and no memmove.
pub fn append_doc_entries(self: *Index, gpa: std.mem.Allocator, doc: *const bson.Document, id: []const u8) !void {
pub fn append_doc_entries(self: *Index, gpa: std.mem.Allocator, doc: []const u8, id: []const u8) !void {
var built = try self.build_entries(gpa, doc, id);
// Runs on success too: the append below moves the keys into the
// staging array, leaving only the (now empty) ArrayList buffer.
@@ -422,7 +424,7 @@ pub const Index = struct {
/// Infallible by construction: regeneration allocates and can fail, and
/// a document the index cannot key contributed nothing to remove, so
/// either way it falls back to the scan, which is always correct.
pub fn remove_doc(self: *Index, gpa: std.mem.Allocator, doc: *const bson.Document, id: []const u8) void {
pub fn remove_doc(self: *Index, gpa: std.mem.Allocator, doc: []const u8, id: []const u8) void {
var built = self.build_entries(gpa, doc, id) catch return self.remove_id(gpa, id);
defer built.deinit(gpa);
// Sparse index that skipped this document: nothing was inserted.
@@ -1707,6 +1709,17 @@ fn evaluate_index(gpa: std.mem.Allocator, ix: *const Index, clauses: []const Cla
const testing = std.testing;
/// Serialize a fabricated doc's pairs to canonical bytes (owned by the
/// caller), since entry generation now reads stored documents as bytes.
fn bytes_of(gpa: std.mem.Allocator, pairs: []const bson.Pair) ![]u8 {
var out: std.ArrayListUnmanaged(u8) = .empty;
defer out.deinit(gpa);
try bson.write_doc(pairs, gpa, &out);
return out.toOwnedSlice(gpa);
}
/// A fabricated tree document for functions that still parse specs from
/// pairs (parse_spec). Never deinit'd — mirrors the old doc_of.
fn doc_of(pairs: []const bson.Pair) bson.Document {
return .{ .arena = undefined, .pairs = pairs };
}
@@ -1746,16 +1759,21 @@ test "entries sort across numeric types and string/null/objectid" {
var ix = try simple_index(gpa, &.{"a"}, false, false);
defer ix.deinit(gpa);
const d_int = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .int32 = 5 } } });
const d_dbl = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 2 } }, .{ .key = "a", .value = .{ .double = 5.0 } } });
const d_str = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 3 } }, .{ .key = "a", .value = .{ .string = "b" } } });
const d_nul = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 4 } }, .{ .key = "a", .value = .null } });
const d_oid = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 5 } }, .{ .key = "a", .value = .{ .object_id = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 } } } });
_ = try ix.add_doc(gpa, &d_int, "i1", true);
_ = try ix.add_doc(gpa, &d_dbl, "i2", true);
_ = try ix.add_doc(gpa, &d_str, "i3", true);
_ = try ix.add_doc(gpa, &d_nul, "i4", true);
_ = try ix.add_doc(gpa, &d_oid, "i5", true);
const d_int = try bytes_of(gpa, &.{ .{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .int32 = 5 } } });
defer gpa.free(d_int);
const d_dbl = try bytes_of(gpa, &.{ .{ .key = "_id", .value = .{ .int32 = 2 } }, .{ .key = "a", .value = .{ .double = 5.0 } } });
defer gpa.free(d_dbl);
const d_str = try bytes_of(gpa, &.{ .{ .key = "_id", .value = .{ .int32 = 3 } }, .{ .key = "a", .value = .{ .string = "b" } } });
defer gpa.free(d_str);
const d_nul = try bytes_of(gpa, &.{ .{ .key = "_id", .value = .{ .int32 = 4 } }, .{ .key = "a", .value = .null } });
defer gpa.free(d_nul);
const d_oid = try bytes_of(gpa, &.{ .{ .key = "_id", .value = .{ .int32 = 5 } }, .{ .key = "a", .value = .{ .object_id = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 } } } });
defer gpa.free(d_oid);
_ = try ix.add_doc(gpa, d_int, "i1", true);
_ = try ix.add_doc(gpa, d_dbl, "i2", true);
_ = try ix.add_doc(gpa, d_str, "i3", true);
_ = try ix.add_doc(gpa, d_nul, "i4", true);
_ = try ix.add_doc(gpa, d_oid, "i5", true);
// An int64 query finds both the int32 and double entries: compare-equal.
try expect_ids(gpa, &ix, &.{.{ .int64 = 5 }}, &.{ "i1", "i2" });
@@ -1778,14 +1796,15 @@ test "missing field is indexed as null; sparse skips the document" {
const gpa = testing.allocator;
var ix = try simple_index(gpa, &.{"a"}, false, false);
defer ix.deinit(gpa);
const d = doc_of(&.{.{ .key = "_id", .value = .{ .int32 = 1 } }});
_ = try ix.add_doc(gpa, &d, "m1", true);
const d = try bytes_of(gpa, &.{.{ .key = "_id", .value = .{ .int32 = 1 } }});
defer gpa.free(d);
_ = try ix.add_doc(gpa, d, "m1", true);
try testing.expectEqual(@as(usize, 1), ix.count());
try expect_ids(gpa, &ix, &.{.null}, &.{"m1"});
var sp = try simple_index(gpa, &.{"a"}, false, true);
defer sp.deinit(gpa);
_ = try sp.add_doc(gpa, &d, "m2", true);
_ = try sp.add_doc(gpa, d, "m2", true);
try testing.expectEqual(@as(usize, 0), sp.count());
}
@@ -1794,8 +1813,9 @@ test "multikey expansion indexes the array and its elements" {
var ix = try simple_index(gpa, &.{"tags"}, false, false);
defer ix.deinit(gpa);
const d = doc_of(&.{.{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "tags", .value = .{ .array = &.{ .{ .string = "a" }, .{ .string = "b" } } } }});
_ = try ix.add_doc(gpa, &d, "mk1", true);
const d = try bytes_of(gpa, &.{.{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "tags", .value = .{ .array = &.{ .{ .string = "a" }, .{ .string = "b" } } } }});
defer gpa.free(d);
_ = try ix.add_doc(gpa, d, "mk1", true);
// 3 entries: the array itself, "a", "b".
try testing.expectEqual(@as(usize, 3), ix.count());
@@ -1811,8 +1831,9 @@ test "per-document dedup keeps {a: [1,1]} under a unique index" {
const gpa = testing.allocator;
var ix = try simple_index(gpa, &.{"a"}, true, false);
defer ix.deinit(gpa);
const d = doc_of(&.{.{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .array = &.{ .{ .int32 = 1 }, .{ .int32 = 1 } } } }});
_ = try ix.add_doc(gpa, &d, "d1", true);
const d = try bytes_of(gpa, &.{.{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .array = &.{ .{ .int32 = 1 }, .{ .int32 = 1 } } } }});
defer gpa.free(d);
_ = try ix.add_doc(gpa, d, "d1", true);
// Entries after dedup: the array itself and one element.
try testing.expectEqual(@as(usize, 2), ix.count());
}
@@ -1821,20 +1842,22 @@ test "parallel arrays are rejected" {
const gpa = testing.allocator;
var ix = try simple_index(gpa, &.{ "a", "b" }, false, false);
defer ix.deinit(gpa);
const d = doc_of(&.{
const d = try bytes_of(gpa, &.{
.{ .key = "_id", .value = .{ .int32 = 1 } },
.{ .key = "a", .value = .{ .array = &.{ .{ .int32 = 1 }, .{ .int32 = 2 } } } },
.{ .key = "b", .value = .{ .array = &.{ .{ .int32 = 3 }, .{ .int32 = 4 } } } },
});
try testing.expectError(error.ParallelArrays, ix.add_doc(gpa, &d, "p1", true));
defer gpa.free(d);
try testing.expectError(error.ParallelArrays, ix.add_doc(gpa, d, "p1", true));
// One array path is fine.
const ok = doc_of(&.{
const ok = try bytes_of(gpa, &.{
.{ .key = "_id", .value = .{ .int32 = 2 } },
.{ .key = "a", .value = .{ .array = &.{ .{ .int32 = 1 }, .{ .int32 = 2 } } } },
.{ .key = "b", .value = .{ .int32 = 3 } },
});
_ = try ix.add_doc(gpa, &ok, "p2", true);
defer gpa.free(ok);
_ = try ix.add_doc(gpa, ok, "p2", true);
try testing.expectEqual(@as(usize, 3), ix.count());
}
@@ -1843,17 +1866,20 @@ test "unique conflict across documents, replace of own entries allowed" {
var ix = try simple_index(gpa, &.{"a"}, true, false);
defer ix.deinit(gpa);
const d1 = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .int32 = 10 } } });
_ = try ix.add_doc(gpa, &d1, "u1", true);
const d1 = try bytes_of(gpa, &.{ .{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .int32 = 10 } } });
defer gpa.free(d1);
_ = try ix.add_doc(gpa, d1, "u1", true);
const d2 = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 2 } }, .{ .key = "a", .value = .{ .int32 = 10 } } });
try testing.expectError(error.DuplicateKeyIndex, ix.add_doc(gpa, &d2, "u2", true));
const d2 = try bytes_of(gpa, &.{ .{ .key = "_id", .value = .{ .int32 = 2 } }, .{ .key = "a", .value = .{ .int32 = 10 } } });
defer gpa.free(d2);
try testing.expectError(error.DuplicateKeyIndex, ix.add_doc(gpa, d2, "u2", true));
// A replace keeps its own key: remove old entries first (the engine's
// evict_doc does this), then add the new ones.
const d1b = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .int32 = 20 } } });
const d1b = try bytes_of(gpa, &.{ .{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .int32 = 20 } } });
defer gpa.free(d1b);
ix.remove_id(gpa, "u1");
_ = try ix.add_doc(gpa, &d1b, "u1", true);
_ = try ix.add_doc(gpa, d1b, "u1", true);
try expect_ids(gpa, &ix, &.{.{ .int32 = 20 }}, &.{"u1"});
try expect_ids(gpa, &ix, &.{.{ .int32 = 10 }}, &.{});
}
@@ -1870,8 +1896,9 @@ test "range bounds inclusive and exclusive" {
.{ .id = "r5", .a = 5 },
};
for (docs) |s| {
const d = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = s.a } }, .{ .key = "a", .value = .{ .int32 = s.a } } });
_ = try ix.add_doc(gpa, &d, s.id, true);
const d = try bytes_of(gpa, &.{ .{ .key = "_id", .value = .{ .int32 = s.a } }, .{ .key = "a", .value = .{ .int32 = s.a } } });
defer gpa.free(d);
_ = try ix.add_doc(gpa, d, s.id, true);
}
var out: std.ArrayListUnmanaged([]const u8) = .empty;
@@ -1901,8 +1928,9 @@ test "empty index and remove_id" {
try ix.lookup_eq(gpa, &.{.{ .int32 = 1 }}, &out);
try testing.expectEqual(@as(usize, 0), out.items.len);
const d = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .int32 = 1 } } });
_ = try ix.add_doc(gpa, &d, "e1", true);
const d = try bytes_of(gpa, &.{ .{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .int32 = 1 } } });
defer gpa.free(d);
_ = try ix.add_doc(gpa, d, "e1", true);
ix.remove_id(gpa, "e1");
try testing.expectEqual(@as(usize, 0), ix.count());
try ix.lookup_eq(gpa, &.{.{ .int32 = 1 }}, &out);
@@ -1923,12 +1951,13 @@ test "compound index prefix search and range on the next key" {
.{ .id = id3, .a = 2, .b = 1 },
};
for (specs) |s| {
const d = doc_of(&.{
const d = try bytes_of(gpa, &.{
.{ .key = "_id", .value = .{ .int32 = s.a } },
.{ .key = "a", .value = .{ .int32 = s.a } },
.{ .key = "b", .value = .{ .int32 = s.b } },
});
_ = try ix.add_doc(gpa, &d, s.id, true);
defer gpa.free(d);
_ = try ix.add_doc(gpa, d, s.id, true);
}
// Prefix on a only.
@@ -1969,7 +1998,7 @@ test "remove_doc leaves the index identical to a full scan removal" {
}
var arrays: [n][3]bson.Value = undefined;
var pairs: [n][2]bson.Pair = undefined;
var docs: [n]bson.Document = undefined;
var docs: [n][]u8 = undefined;
for (0..n) |i| {
const id = try std.fmt.allocPrint(gpa, "id{d}", .{i});
@@ -1999,9 +2028,9 @@ test "remove_doc leaves the index identical to a full scan removal" {
// Missing both.
else => np = 0,
}
docs[i] = doc_of(pairs[i][0..np]);
try by_doc.append_doc_entries(gpa, &docs[i], id);
try by_scan.append_doc_entries(gpa, &docs[i], id);
docs[i] = try bytes_of(gpa, pairs[i][0..np]);
try by_doc.append_doc_entries(gpa, docs[i], id);
try by_scan.append_doc_entries(gpa, docs[i], id);
}
_ = try by_doc.finish_bulk(gpa, false);
_ = try by_scan.finish_bulk(gpa, false);
@@ -2018,7 +2047,7 @@ test "remove_doc leaves the index identical to a full scan removal" {
defer doc_refs.deinit(gpa);
defer scan_refs.deinit(gpa);
for (order) |i| {
by_doc.remove_doc(gpa, &docs[i], ids.items[i]);
by_doc.remove_doc(gpa, docs[i], ids.items[i]);
by_scan.remove_id(gpa, ids.items[i]);
doc_refs.clearRetainingCapacity();
@@ -2037,6 +2066,7 @@ test "remove_doc leaves the index identical to a full scan removal" {
try testing.expect(std.mem.eql(u8, x.id, y.id));
}
}
for (docs) |b| gpa.free(b);
try testing.expectEqual(@as(usize, 0), by_doc.count());
}
}
@@ -2067,12 +2097,13 @@ test "incremental inserts and removals stay identical to a brute-force model" {
const a = rand.intRangeAtMost(i32, 0, 30);
const b = rand.intRangeAtMost(i32, 0, 30);
const id = try std.fmt.allocPrint(gpa, "id{d}", .{i});
const d = doc_of(&.{
const d = try bytes_of(gpa, &.{
.{ .key = "_id", .value = .{ .int32 = @intCast(i) } },
.{ .key = "a", .value = .{ .int32 = a } },
.{ .key = "b", .value = .{ .int32 = b } },
});
_ = try ix.add_doc(gpa, &d, id, false);
defer gpa.free(d);
_ = try ix.add_doc(gpa, d, id, false);
try model.append(gpa, .{ .a = a, .b = b, .id = id });
try live.append(gpa, true);
@@ -2086,12 +2117,13 @@ test "incremental inserts and removals stay identical to a brute-force model" {
rand.shuffle(usize, order.items);
for (order.items) |i| {
const m = model.items[i];
const d = doc_of(&.{
const d = try bytes_of(gpa, &.{
.{ .key = "_id", .value = .{ .int32 = @intCast(i) } },
.{ .key = "a", .value = .{ .int32 = m.a } },
.{ .key = "b", .value = .{ .int32 = m.b } },
});
ix.remove_doc(gpa, &d, m.id);
defer gpa.free(d);
ix.remove_doc(gpa, d, m.id);
live.items[i] = false;
try verify_model(gpa, &ix, model.items, live.items, rand);
}
@@ -2158,12 +2190,13 @@ test "lookup_range matches a brute-force filter over random data" {
facts[i] = .{ .a = a, .b = b };
const id = try std.fmt.allocPrint(gpa, "id{d}", .{i});
try ids.append(gpa, id);
const d = doc_of(&.{
const d = try bytes_of(gpa, &.{
.{ .key = "_id", .value = .{ .int32 = @intCast(i) } },
.{ .key = "a", .value = .{ .int32 = a } },
.{ .key = "b", .value = .{ .int32 = b } },
});
try ix.append_doc_entries(gpa, &d, id);
defer gpa.free(d);
try ix.append_doc_entries(gpa, d, id);
}
_ = try ix.finish_bulk(gpa, false);
@@ -2337,13 +2370,14 @@ test "the _id index plan covers equality, ranges and _id sort order" {
var ix = try simple_index(gpa, &.{"_id"}, false, false);
defer ix.deinit(gpa);
for (0..5) |i| {
const d = doc_of(&.{
const d = try bytes_of(gpa, &.{
.{ .key = "_id", .value = .{ .int32 = @intCast(i + 1) } },
.{ .key = "v", .value = .{ .int32 = @intCast(i) } },
});
defer gpa.free(d);
const id = try std.fmt.allocPrint(gpa, "d{d}", .{i + 1});
defer gpa.free(id);
_ = try ix.add_doc(gpa, &d, id, false);
_ = try ix.add_doc(gpa, d, id, false);
}
// {_id: 3} → an equality plan whose candidates are just that doc.