index: secondary index core — entries, search, planner, _id fast path

Adds src/index.zig with the full secondary-index machinery: entry
generation mirroring field_matches (array value + elements), BSON-order
sorted entries with binary search, compound prefix and range lookups,
unique/sparse options, the query planner (longest equality/$in run +
optional range, $in cartesian cap, sparse/null bail), and the _id_ fast
path guarded against serialization-ambiguous values (numbers, strings,
symbols, codes, opaque payloads).

query.collect_values is now pub so entry generation can mirror it exactly.
storage.zig gains record_type_index_create/drop; lib.zig exports index.
This commit is contained in:
2026-08-02 12:21:22 +03:00
parent 662df9b121
commit a38ddc2f50
10 changed files with 1824 additions and 782 deletions

View File

@@ -13,7 +13,7 @@ const max_path_segments = 16;
/// Apply an update document (whose fields are operator documents) to `doc`.
pub fn apply(doc: *bson.Document, update: *const bson.Document) UpdateError!void {
const arena = doc.arena.allocator();
var pairs = try copy_pairs_to_list(arena, doc.pairs);
var pairs = try copy_to_list(bson.Pair, arena, doc.pairs);
for (update.pairs) |op| {
if (op.key.len == 0 or op.key[0] != '$') return error.InvalidUpdate;
try apply_operator(arena, &pairs, op.key, op.value);
@@ -48,7 +48,7 @@ fn apply_operator(arena: std.mem.Allocator, pairs: *std.ArrayListUnmanaged(bson.
const n = split_path(p.key, &segs) orelse return error.InvalidUpdate;
const current = get_value(pairs.items, segs[0..n]) orelse bson.Value{ .int32 = 0 };
if (!current.is_number() or !p.value.is_number()) return error.InvalidUpdate;
const sum = try numeric_add(arena, current, p.value);
const sum = try numeric_add(current, p.value);
try set_path(arena, pairs, segs[0..n], sum);
}
return;
@@ -145,19 +145,15 @@ fn parse_index(seg: []const u8) ?usize {
return std.fmt.parseInt(usize, seg, 10) catch null;
}
fn copy_pairs_to_list(arena: std.mem.Allocator, pairs: []const bson.Pair) UpdateError!std.ArrayListUnmanaged(bson.Pair) {
var out: std.ArrayListUnmanaged(bson.Pair) = .empty;
/// Shallow-copy a slice into a growable list backed by `arena`.
fn copy_to_list(comptime T: type, arena: std.mem.Allocator, items: []const T) UpdateError!std.ArrayListUnmanaged(T) {
var out: std.ArrayListUnmanaged(T) = .empty;
errdefer out.deinit(arena);
try out.appendSlice(arena, pairs);
try out.appendSlice(arena, items);
return out;
}
fn find_pair(pairs: []const bson.Pair, key: []const u8) ?usize {
for (pairs, 0..) |p, i| {
if (std.mem.eql(u8, p.key, key)) return i;
}
return null;
}
const find_pair = bson.get_pair_index;
fn get_value(pairs: []const bson.Pair, segs: []const []const u8) ?bson.Value {
const idx = find_pair(pairs, segs[0]) orelse return null;
@@ -193,7 +189,7 @@ fn set_path(arena: std.mem.Allocator, pairs: *std.ArrayListUnmanaged(bson.Pair),
};
switch (pairs.items[idx].value) {
.doc => |sub| {
var sub_pairs = try copy_pairs_to_list(arena, sub);
var sub_pairs = try copy_to_list(bson.Pair, arena, sub);
defer sub_pairs.deinit(arena);
try set_path(arena, &sub_pairs, segs[1..], value);
pairs.items[idx].value = .{ .doc = try sub_pairs.toOwnedSlice(arena) };
@@ -207,7 +203,7 @@ fn set_path(arena: std.mem.Allocator, pairs: *std.ArrayListUnmanaged(bson.Pair),
pairs.items[idx].value = .{ .doc = try sub_pairs.toOwnedSlice(arena) };
return;
};
var items = try copy_array_to_list(arena, arr);
var items = try copy_to_list(bson.Value, arena, arr);
defer items.deinit(arena);
if (index >= items.items.len) {
try items.appendNTimes(arena, .null, index + 1 - items.items.len);
@@ -217,7 +213,7 @@ fn set_path(arena: std.mem.Allocator, pairs: *std.ArrayListUnmanaged(bson.Pair),
} else {
switch (items.items[index]) {
.doc => |sub| {
var sub_pairs = try copy_pairs_to_list(arena, sub);
var sub_pairs = try copy_to_list(bson.Pair, arena, sub);
defer sub_pairs.deinit(arena);
try set_path(arena, &sub_pairs, segs[2..], value);
items.items[index] = .{ .doc = try sub_pairs.toOwnedSlice(arena) };
@@ -241,13 +237,6 @@ fn set_path(arena: std.mem.Allocator, pairs: *std.ArrayListUnmanaged(bson.Pair),
}
}
fn copy_array_to_list(arena: std.mem.Allocator, arr: []const bson.Value) UpdateError!std.ArrayListUnmanaged(bson.Value) {
var out: std.ArrayListUnmanaged(bson.Value) = .empty;
errdefer out.deinit(arena);
try out.appendSlice(arena, arr);
return out;
}
fn unset_path(arena: std.mem.Allocator, pairs: *std.ArrayListUnmanaged(bson.Pair), segs: []const []const u8) void {
if (segs.len == 1) {
if (find_pair(pairs.items, segs[0])) |idx| {
@@ -258,7 +247,7 @@ fn unset_path(arena: std.mem.Allocator, pairs: *std.ArrayListUnmanaged(bson.Pair
const idx = find_pair(pairs.items, segs[0]) orelse return;
switch (pairs.items[idx].value) {
.doc => |sub| {
var sub_pairs = copy_pairs_to_list(arena, sub) catch return;
var sub_pairs = copy_to_list(bson.Pair, arena, sub) catch return;
unset_path(arena, &sub_pairs, segs[1..]);
pairs.items[idx].value = .{ .doc = sub_pairs.items };
},
@@ -273,14 +262,7 @@ fn pull_matches(arena: std.mem.Allocator, condition: bson.Value, elem: bson.Valu
.doc => |pairs| pairs,
else => return false,
};
var all_operators = cond_pairs.len > 0;
for (cond_pairs) |p| {
if (p.key.len == 0 or p.key[0] != '$') {
all_operators = false;
break;
}
}
if (all_operators) {
if (query.all_operator_keys(cond_pairs)) {
// Operator condition against the element's value at each
// operator's field — treat element doc as the doc.
var ok = true;
@@ -297,8 +279,7 @@ fn pull_matches(arena: std.mem.Allocator, condition: bson.Value, elem: bson.Valu
}
}
fn numeric_add(arena: std.mem.Allocator, a: bson.Value, b: bson.Value) UpdateError!bson.Value {
_ = arena;
fn numeric_add(a: bson.Value, b: bson.Value) UpdateError!bson.Value {
if (a == .double or b == .double) {
const sum: f64 = @floatCast(a.as_f128() + b.as_f128());
return .{ .double = sum };