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:
156
src/index.zig
156
src/index.zig
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user