index: a leaf record's payload becomes the document's slab offset
PLAN amendment A3. The B+tree leaf had nowhere to put a document's slab offset -- `Slot.extra` is the payload length for a leaf and the child node id for an internal separator -- which is what blocks the `_id_` tree from becoming the primary lookup once the docs hashmap goes away. A leaf record is now `key ++ offset_le`, so `extra` is always 8 and every byte-accounting site (fits, record_cost, slot_cost, balanced_cut, repack_keep_prefix) is untouched. Records get *smaller*: an ObjectId `_id_` record goes from 26 bytes to 21. `Entry.id` is deleted rather than re-owned. Every entry one document contributes shares one document, so which document it is belongs on the call that commits the entries -- which also makes it impossible to confuse the offset a replace is removing with the one it is inserting. The old field aliased the docs map's key and was only safe because removal happened at the one chokepoint where a document dies; that constraint is gone. Done for secondary indexes too, not just `_id_`. That deletes the per-candidate `coll.docs.get(id)` in scan_sorted outright rather than replacing it with an `_id_` descent, and it is free on the write path because a replace already removes and reinserts every entry in every index. Consequences worth knowing: - lookup_eq/lookup_range/Plan.search yield u64. Those are values, immune to the tree mutation that invalidated the id slices they used to hand back -- which is why ttl_sweep_coll can drop the dupe-and-free dance it needed to survive `remove` freeing the key its entries pointed at. - One safety net is gone. A stale entry used to be swallowed by `docs.get(id) orelse continue`; now it resolves to superseded-but-parseable bytes the re-applied filter might accept. That trades an invisible under-approximation for a visible wrong answer, which is the better failure to have, but it is a trade. - A checkpoint may never renumber slab offsets (already recorded in PLAN §4): every index leaf now holds a physical one. `zig build fuzz` earned its keep immediately -- it caught the API break in all four B+tree harnesses, which `zig build test` cannot see. Benchmarks A/B'd at 256m on one harness, before and after: all rows flat. updateMany and deleteOne+insertOne first looked 10-13% slower, which three repeat runs showed to be single-sample noise (0.70/0.71/0.70 against 0.70).
This commit is contained in:
@@ -753,18 +753,22 @@ fn scan_sorted(
|
||||
var n: usize = 0;
|
||||
|
||||
// Index plan (the implicit _id_ index first, then the secondaries):
|
||||
// candidates in index order, re-filtered. The returned ids alias the
|
||||
// docs map keys, valid under the read lock.
|
||||
// candidates in index order, re-filtered. A candidate *is* a slab offset
|
||||
// now, so the map lookup that used to translate an id into one is gone --
|
||||
// and so is the accidental safety net it provided: a stale entry used to be
|
||||
// dropped silently by `orelse continue`, where now it resolves to
|
||||
// superseded-but-parseable bytes that the re-applied filter might accept.
|
||||
// Loud beats silent: a wrong answer a test can see beats a missing
|
||||
// candidate nothing can.
|
||||
if (try index.plan(ctx.gpa, &coll.id_index, coll.indexes.items, filter, sort)) |p| {
|
||||
var plan = p;
|
||||
defer plan.deinit(ctx.gpa);
|
||||
var ids: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
defer ids.deinit(ctx.gpa);
|
||||
try plan.search(ctx.gpa, &ids);
|
||||
var offs: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer offs.deinit(ctx.gpa);
|
||||
try plan.search(ctx.gpa, &offs);
|
||||
if (sorted) |flag| flag.* = plan.provides_sort;
|
||||
if (plan.provides_sort) lim = limit;
|
||||
for (ids.items) |id| {
|
||||
const off = coll.docs.get(id) orelse continue;
|
||||
for (offs.items) |off| {
|
||||
if (!try query.matches_bytes(ctx.gpa, filter, coll.doc_bytes(off))) continue;
|
||||
if (out) |list| try list.append(ctx.gpa, off);
|
||||
n += 1;
|
||||
|
||||
79
src/db.zig
79
src/db.zig
@@ -276,9 +276,10 @@ pub const Engine = struct {
|
||||
|
||||
/// Drop the document stored under `id_key`, freeing it and its key.
|
||||
/// No-op when the id is absent. This is the single chokepoint where a
|
||||
/// document dies, so index entries are removed here — while the
|
||||
/// document and the docs map key are both still alive, which is what
|
||||
/// keeps `Entry.id`'s aliasing of that key safe.
|
||||
/// document dies, so index entries are removed here, keyed by the slab
|
||||
/// offset the map hands back. It used to matter that the map key was still
|
||||
/// alive at this point, because entries aliased it; entries carry an
|
||||
/// offset now, so that constraint is gone.
|
||||
///
|
||||
/// The document itself is handed to the index: entries are located by
|
||||
/// regenerating them from it, which is far cheaper than scanning.
|
||||
@@ -287,8 +288,10 @@ pub const Engine = struct {
|
||||
// Resolve the bytes before any mutation; the slab is untouched by
|
||||
// index removal, so the slice is safe for the call.
|
||||
const old_bytes = coll.doc_bytes(old.value);
|
||||
coll.id_index.remove_doc(self.gpa, old_bytes, old.key);
|
||||
for (coll.indexes.items) |ix| ix.remove_doc(self.gpa, old_bytes, old.key);
|
||||
// Entries are keyed by the document's slab offset now, which is exactly
|
||||
// what the map just gave us.
|
||||
coll.id_index.remove_doc(self.gpa, old_bytes, old.value);
|
||||
for (coll.indexes.items) |ix| ix.remove_doc(self.gpa, old_bytes, old.value);
|
||||
self.gpa.free(old.key);
|
||||
// This document's log record (and its slab bytes) just became garbage.
|
||||
// The fetchRemove above succeeded, so a live document was counted.
|
||||
@@ -564,14 +567,14 @@ pub const Engine = struct {
|
||||
// before the log append, inserted infallibly after it. Built
|
||||
// *first* so it is checked first below -- MongoDB reports _id_
|
||||
// when a write violates both it and a unique secondary.
|
||||
var built = try coll.id_index.build_entries(self.gpa, doc_bytes, id_key);
|
||||
var built = try coll.id_index.build_entries(self.gpa, doc_bytes);
|
||||
built_list.append(self.gpa, .{ .built = built, .ix = &coll.id_index }) catch |err| {
|
||||
built.deinit(self.gpa);
|
||||
return err;
|
||||
};
|
||||
}
|
||||
for (coll.indexes.items) |ix| {
|
||||
var built = try ix.build_entries(self.gpa, doc_bytes, id_key);
|
||||
var built = try ix.build_entries(self.gpa, doc_bytes);
|
||||
built_list.append(self.gpa, .{ .built = built, .ix = ix }) catch |err| {
|
||||
built.deinit(self.gpa);
|
||||
return err;
|
||||
@@ -584,9 +587,11 @@ pub const Engine = struct {
|
||||
// (PLAN amendment A3) -- and the tree answers it better, since it
|
||||
// is keyed on the canonical encode_key rather than serialize_value
|
||||
// (A4). Exclude-self is null for an insert: the document has no
|
||||
// entries yet, and passing its id would hide precisely the
|
||||
// same-_id collision this must catch.
|
||||
const exclude: ?[]const u8 = if (mode == .replace) id_key else null;
|
||||
// entries yet, and passing its offset would hide precisely the
|
||||
// same-_id collision this must catch. For a replace it is the
|
||||
// document's *current* slab offset, since that is what its existing
|
||||
// entries carry -- the new offset does not exist yet.
|
||||
const exclude: ?u64 = if (mode == .replace) coll.docs.get(id_key) else null;
|
||||
for (built_list.items) |*b| {
|
||||
if (!b.ix.unique) continue;
|
||||
b.ix.check_unique(b.built.entries.items, exclude) catch {
|
||||
@@ -620,7 +625,7 @@ pub const Engine = struct {
|
||||
self.live_docs += 1;
|
||||
for (built_list.items) |*b| {
|
||||
if (b.built.multikey) b.ix.multikey = true;
|
||||
b.ix.insert_entries(&b.built);
|
||||
b.ix.insert_entries(&b.built, off);
|
||||
}
|
||||
stored = true;
|
||||
self.note_compact();
|
||||
@@ -739,7 +744,7 @@ pub const Engine = struct {
|
||||
// ix.deinit frees every appended key. Nothing is persisted.
|
||||
var doc_it = coll.docs.iterator();
|
||||
while (doc_it.next()) |entry| {
|
||||
try ix.append_doc_entries(self.gpa, coll.doc_bytes(entry.value_ptr.*), entry.key_ptr.*);
|
||||
try ix.append_doc_entries(self.gpa, coll.doc_bytes(entry.value_ptr.*), entry.value_ptr.*);
|
||||
}
|
||||
_ = try ix.finish_bulk(self.gpa, true);
|
||||
|
||||
@@ -818,14 +823,13 @@ pub const Engine = struct {
|
||||
) !usize {
|
||||
try coll.lock.lock(self.io);
|
||||
defer coll.lock.unlock(self.io);
|
||||
// Ids are duped rather than aliased: `remove` frees the docs-map key
|
||||
// that `Entry.id` points at, which would leave the rest of the batch
|
||||
// pointing into freed memory.
|
||||
var ids: std.ArrayListUnmanaged([]u8) = .empty;
|
||||
defer {
|
||||
for (ids.items) |id| self.gpa.free(id);
|
||||
ids.deinit(self.gpa);
|
||||
}
|
||||
// Offsets, collected before any removal. They are values, so unlike
|
||||
// the id slices this used to dupe -- which aliased a docs-map key that
|
||||
// `remove` would free out from under the rest of the batch -- there is
|
||||
// nothing to own here. Collect-then-remove still matters, because the
|
||||
// iterator below aliases tree pages that removal reshapes.
|
||||
var offs: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer offs.deinit(self.gpa);
|
||||
|
||||
for (coll.indexes.items) |ix| {
|
||||
const ttl = ix.ttl orelse continue;
|
||||
@@ -840,28 +844,33 @@ pub const Engine = struct {
|
||||
while (it.next()) |e| {
|
||||
const ms = bson.encoded_leading_datetime(e.key) orelse break;
|
||||
if (@as(i128, ms) > cutoff) break;
|
||||
try ids.append(self.gpa, try self.gpa.dupe(u8, e.id));
|
||||
try offs.append(self.gpa, e.off);
|
||||
}
|
||||
}
|
||||
if (ids.items.len == 0) return 0;
|
||||
if (offs.items.len == 0) return 0;
|
||||
|
||||
// One document can be expired by several entries (an array
|
||||
// of dates) or by several TTL indexes.
|
||||
std.mem.sort([]u8, ids.items, {}, less_id_bytes);
|
||||
std.mem.sort(u64, offs.items, {}, std.sort.asc(u64));
|
||||
var w: usize = 1;
|
||||
for (ids.items[1..]) |id| {
|
||||
if (std.mem.eql(u8, id, ids.items[w - 1])) {
|
||||
self.gpa.free(id);
|
||||
} else {
|
||||
ids.items[w] = id;
|
||||
for (offs.items[1..]) |off| {
|
||||
if (off != offs.items[w - 1]) {
|
||||
offs.items[w] = off;
|
||||
w += 1;
|
||||
}
|
||||
}
|
||||
ids.items.len = w;
|
||||
offs.items.len = w;
|
||||
|
||||
// `remove` works by _id, so recover each one from the document its
|
||||
// offset names. get_at materializes a spine, hence the arena; the slab
|
||||
// is untouched by the removals, so the bytes stay valid throughout.
|
||||
var arena = std.heap.ArenaAllocator.init(self.gpa);
|
||||
defer arena.deinit();
|
||||
var removed: usize = 0;
|
||||
for (ids.items) |id| {
|
||||
if (try self.remove(db_name, coll_name, id)) removed += 1;
|
||||
for (offs.items) |off| {
|
||||
const id_value = (try bson.get_at(arena.allocator(), coll.doc_bytes(off), "_id")) orelse continue;
|
||||
const id_key = try bson.serialize_value(arena.allocator(), id_value);
|
||||
if (try self.remove(db_name, coll_name, id_key)) removed += 1;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
@@ -1154,7 +1163,7 @@ pub const Engine = struct {
|
||||
if (ix.count() > 0) return; // defensive
|
||||
var doc_it = coll.docs.iterator();
|
||||
while (doc_it.next()) |doc_entry| {
|
||||
ix.append_doc_entries(self.gpa, coll.doc_bytes(doc_entry.value_ptr.*), doc_entry.key_ptr.*) catch |err| switch (err) {
|
||||
ix.append_doc_entries(self.gpa, coll.doc_bytes(doc_entry.value_ptr.*), doc_entry.value_ptr.*) catch |err| switch (err) {
|
||||
error.ParallelArrays => {
|
||||
std.debug.print(
|
||||
"multiforadb: WARNING: index '{s}' cannot index an existing " ++
|
||||
@@ -1205,10 +1214,6 @@ pub const Engine = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn less_id_bytes(_: void, a: []const u8, b: []const u8) bool {
|
||||
return std.mem.order(u8, a, b) == .lt;
|
||||
}
|
||||
|
||||
fn parent_dir(path: []const u8) []const u8 {
|
||||
const last = std.mem.lastIndexOfScalar(u8, path, '/') orelse return ".";
|
||||
if (last == 0) return "/";
|
||||
@@ -1835,7 +1840,7 @@ fn index_count(
|
||||
const coll = engine.get_collection(db_name, coll_name) orelse return 0;
|
||||
for (coll.indexes.items) |ix| {
|
||||
if (std.mem.eql(u8, ix.name, name)) {
|
||||
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
var out: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer out.deinit(gpa);
|
||||
try ix.lookup_eq(gpa, &.{key_value}, &out);
|
||||
return out.items.len;
|
||||
|
||||
@@ -26,9 +26,9 @@ fn make_doc(gpa: std.mem.Allocator, i: usize, s: []const u8) ![]u8 {
|
||||
}
|
||||
|
||||
const Doc = struct {
|
||||
id: []u8,
|
||||
s: []u8,
|
||||
bytes: []u8,
|
||||
off: u64,
|
||||
live: bool,
|
||||
};
|
||||
|
||||
@@ -43,7 +43,6 @@ fn run(seed: u64, ops: usize, max_len: usize) !void {
|
||||
var docs: std.ArrayListUnmanaged(Doc) = .empty;
|
||||
defer {
|
||||
for (docs.items) |d| {
|
||||
gpa.free(d.id);
|
||||
gpa.free(d.s);
|
||||
gpa.free(d.bytes);
|
||||
}
|
||||
@@ -58,7 +57,7 @@ fn run(seed: u64, ops: usize, max_len: usize) !void {
|
||||
for (docs.items) |*d| {
|
||||
if (!d.live) continue;
|
||||
if (pick == 0) {
|
||||
ix.remove_doc(gpa, d.bytes, d.id);
|
||||
ix.remove_doc(gpa, d.bytes, d.off);
|
||||
d.live = false;
|
||||
live -= 1;
|
||||
break;
|
||||
@@ -77,12 +76,11 @@ fn run(seed: u64, ops: usize, max_len: usize) !void {
|
||||
errdefer gpa.free(s);
|
||||
// A small alphabet so keys collide and share prefixes.
|
||||
for (s) |*c| c.* = 'a' + rand.uintLessThan(u8, 4);
|
||||
const id = try std.fmt.allocPrint(gpa, "id{d}", .{op});
|
||||
errdefer gpa.free(id);
|
||||
const id: u64 = @intCast(op + 1);
|
||||
const bytes = try make_doc(gpa, op, s);
|
||||
errdefer gpa.free(bytes);
|
||||
_ = try ix.add_doc(gpa, bytes, id, false);
|
||||
try docs.append(gpa, .{ .id = id, .s = s, .bytes = bytes, .live = true });
|
||||
try docs.append(gpa, .{ .off = id, .s = s, .bytes = bytes, .live = true });
|
||||
live += 1;
|
||||
}
|
||||
|
||||
@@ -101,7 +99,7 @@ fn run(seed: u64, ops: usize, max_len: usize) !void {
|
||||
|
||||
// Every live document is reachable by a descent, not just by
|
||||
// walking the leaf chain: a bad separator breaks only the descent.
|
||||
var found: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
var found: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer found.deinit(gpa);
|
||||
for (docs.items) |d| {
|
||||
if (!d.live) continue;
|
||||
@@ -110,13 +108,13 @@ fn run(seed: u64, ops: usize, max_len: usize) !void {
|
||||
try ix.lookup_eq(gpa, &.{.{ .string = d.s }}, &found);
|
||||
var hit = false;
|
||||
for (found.items) |got| {
|
||||
if (std.mem.eql(u8, got, d.id)) hit = true;
|
||||
if (got == d.off) hit = true;
|
||||
}
|
||||
if (!hit) {
|
||||
std.debug.print("seed {d} op {d}: id {s} (key len {d}) not found by descent\n", .{
|
||||
std.debug.print("seed {d} op {d}: off {d} (key len {d}) not found by descent\n", .{
|
||||
seed,
|
||||
op,
|
||||
d.id,
|
||||
d.off,
|
||||
d.s.len,
|
||||
});
|
||||
return error.EntryUnreachable;
|
||||
|
||||
441
src/index.zig
441
src/index.zig
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@ pub fn main() !void {
|
||||
pairs[0] = .{ .key = "_id", .value = .{ .int32 = @intCast(i) } };
|
||||
pairs[1] = .{ .key = "tag", .value = .{ .string = strings[i] } };
|
||||
docs[i] = try doc_of(gpa, &pairs);
|
||||
_ = try ix.add_doc(gpa, docs[i], &[_]u8{ 'i', 'd', @intCast(i + 1) }, true);
|
||||
_ = try ix.add_doc(gpa, docs[i], @intCast(i + 1), true);
|
||||
}
|
||||
std.debug.print("count={d} leaves={d} depth={d} overflow={d}\n", .{
|
||||
ix.count(),
|
||||
@@ -47,25 +47,25 @@ pub fn main() !void {
|
||||
|
||||
// Every entry is found by exact key.
|
||||
for (lens, 0..) |_, i| {
|
||||
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
var out: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer out.deinit(gpa);
|
||||
try ix.lookup_eq(gpa, &.{.{ .string = strings[i] }}, &out);
|
||||
if (out.items.len != 1) {
|
||||
std.debug.print("lookup {d} got {d}\n", .{ i, out.items.len });
|
||||
return error.Bad;
|
||||
}
|
||||
if (!std.mem.eql(u8, out.items[0], &[_]u8{ 'i', 'd', @intCast(i + 1) })) return error.Bad;
|
||||
if (out.items[0] != i + 1) return error.Bad;
|
||||
}
|
||||
|
||||
// Delete the spilled ones and the inline ones alternately.
|
||||
for (lens, 0..) |_, i| {
|
||||
if (i % 2 == 0) continue;
|
||||
ix.remove_doc(gpa, docs[i], &[_]u8{ 'i', 'd', @intCast(i + 1) });
|
||||
ix.remove_doc(gpa, docs[i], @intCast(i + 1));
|
||||
}
|
||||
if (ix.count() != 3) return error.Bad;
|
||||
for (lens, 0..) |_, i| {
|
||||
if (i % 2 == 0) continue;
|
||||
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
var out: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer out.deinit(gpa);
|
||||
try ix.lookup_eq(gpa, &.{.{ .string = strings[i] }}, &out);
|
||||
if (out.items.len != 0) return error.Bad;
|
||||
|
||||
@@ -22,11 +22,8 @@ pub fn main() !void {
|
||||
// 5000 docs, each with a 2 KiB key: spills on every record, forcing
|
||||
// leaves and internal nodes to hold overflow references.
|
||||
const N = 5000;
|
||||
var ids: std.ArrayListUnmanaged([]u8) = .empty;
|
||||
defer {
|
||||
for (ids.items) |x| gpa.free(x);
|
||||
ids.deinit(gpa);
|
||||
}
|
||||
var ids: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer ids.deinit(gpa);
|
||||
var pairs: [2]bson.Pair = undefined;
|
||||
var buf = try gpa.alloc(u8, 2000);
|
||||
defer gpa.free(buf);
|
||||
@@ -41,7 +38,7 @@ pub fn main() !void {
|
||||
std.mem.writeInt(u32, buf[0..4], @intCast(i), .little);
|
||||
const key = try gpa.dupe(u8, buf);
|
||||
try facts.append(gpa, .{ .key = key });
|
||||
const id = try std.fmt.allocPrint(gpa, "id{d}", .{i});
|
||||
const id: u64 = @intCast(i + 1);
|
||||
try ids.append(gpa, id);
|
||||
pairs[0] = .{ .key = "_id", .value = .{ .int32 = @intCast(i) } };
|
||||
pairs[1] = .{ .key = "tag", .value = .{ .string = key } };
|
||||
@@ -61,10 +58,10 @@ pub fn main() !void {
|
||||
const rand2 = prng2.random();
|
||||
for (0..300) |_| {
|
||||
const i = rand2.intRangeAtMost(usize, 0, N - 1);
|
||||
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
var out: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer out.deinit(gpa);
|
||||
try ix.lookup_eq(gpa, &.{.{ .string = facts.items[i].key }}, &out);
|
||||
if (out.items.len != 1 or !std.mem.eql(u8, out.items[0], ids.items[i])) {
|
||||
if (out.items.len != 1 or out.items[0] != ids.items[i]) {
|
||||
std.debug.print("lookup mismatch at {d}\n", .{i});
|
||||
return error.Bad;
|
||||
}
|
||||
@@ -87,7 +84,7 @@ pub fn main() !void {
|
||||
}
|
||||
}
|
||||
for (order.items) |i| {
|
||||
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
var out: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer out.deinit(gpa);
|
||||
try ix.lookup_eq(gpa, &.{.{ .string = facts.items[i].key }}, &out);
|
||||
if (out.items.len != 0) return error.Bad;
|
||||
|
||||
@@ -25,7 +25,7 @@ fn check_range(
|
||||
facts: []const Fact,
|
||||
alive: []const bool,
|
||||
) !void {
|
||||
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
var out: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer out.deinit(gpa);
|
||||
try ix.lookup_range(gpa, &.{prefix}, lo, true, hi, false, &out);
|
||||
var expected: usize = 0;
|
||||
@@ -54,7 +54,7 @@ pub fn main() !void {
|
||||
const rand = prng.random();
|
||||
|
||||
const N = 30_000;
|
||||
var ids: std.ArrayListUnmanaged([]u8) = .empty;
|
||||
var ids: std.ArrayListUnmanaged(u64) = .empty;
|
||||
var facts: std.ArrayListUnmanaged(Fact) = .empty;
|
||||
var alive: std.ArrayListUnmanaged(bool) = .empty;
|
||||
var pairs: [2]bson.Pair = undefined;
|
||||
@@ -63,7 +63,7 @@ pub fn main() !void {
|
||||
var keys = [_]index.IndexKey{ .{ .path = "a", .descending = false }, .{ .path = "b", .descending = false } };
|
||||
var ix = try index.Index.init(gpa, "ab", &keys, false, false, null);
|
||||
for (0..N) |i| {
|
||||
const id = try std.fmt.allocPrint(gpa, "id{d}", .{i});
|
||||
const id: u64 = @intCast(i + 1);
|
||||
try ids.append(gpa, id);
|
||||
const a = rand.intRangeAtMost(i32, 0, 99);
|
||||
const b = rand.intRangeAtMost(i32, 0, 999);
|
||||
@@ -96,7 +96,7 @@ pub fn main() !void {
|
||||
// 2. Incremental inserts, random order (splits + rebalancing-free path).
|
||||
const M = 20_000;
|
||||
for (0..M) |i| {
|
||||
const id = try std.fmt.allocPrint(gpa, "new{d}", .{i});
|
||||
const id: u64 = @intCast(100_000 + i);
|
||||
try ids.append(gpa, id);
|
||||
const a = rand.intRangeAtMost(i32, 0, 99);
|
||||
const b = rand.intRangeAtMost(i32, 0, 999);
|
||||
@@ -156,7 +156,7 @@ pub fn main() !void {
|
||||
// 4. Equality lookups still exact.
|
||||
for (0..300) |_| {
|
||||
const a = rand.intRangeAtMost(i32, 0, 99);
|
||||
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
var out: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer out.deinit(gpa);
|
||||
try ix.lookup_eq(gpa, &.{.{ .int32 = a }}, &out);
|
||||
var expected: usize = 0;
|
||||
@@ -208,8 +208,8 @@ pub fn main() !void {
|
||||
pairs[1] = .{ .key = "b", .value = .{ .int32 = 42 } };
|
||||
const d2 = try doc_of(gpa, &pairs);
|
||||
defer gpa.free(d2);
|
||||
_ = try ix.add_doc(gpa, d2, "final", false);
|
||||
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
_ = try ix.add_doc(gpa, d2, 999_999, false);
|
||||
var out: std.ArrayListUnmanaged(u64) = .empty;
|
||||
defer out.deinit(gpa);
|
||||
try ix.lookup_eq(gpa, &.{.{ .int32 = 7 }}, &out);
|
||||
if (out.items.len != 1) return error.BadCount;
|
||||
|
||||
Reference in New Issue
Block a user