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:
@@ -57,16 +57,26 @@ fn match_top_level(gpa: std.mem.Allocator, op: []const u8, value: bson.Value, do
|
||||
return false;
|
||||
}
|
||||
|
||||
/// True when every key is a `$`-prefixed operator. The one definition of
|
||||
/// what an operator document looks like — filters, $elemMatch, $pull and
|
||||
/// upsert-document construction all ask this question.
|
||||
pub fn all_operator_keys(pairs: []const bson.Pair) bool {
|
||||
if (pairs.len == 0) return false;
|
||||
for (pairs) |p| {
|
||||
if (p.key.len == 0 or p.key[0] != '$') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The operator pairs of a `{$op: ...}` value, or null if it is not one. An
|
||||
/// empty document counts as an (vacuously satisfied) operator document.
|
||||
fn is_operator_doc(value: bson.Value) ?[]const bson.Pair {
|
||||
return switch (value) {
|
||||
.doc => |pairs| blk: {
|
||||
for (pairs) |p| {
|
||||
if (p.key.len == 0 or p.key[0] != '$') break :blk null;
|
||||
}
|
||||
break :blk pairs;
|
||||
},
|
||||
else => null,
|
||||
const pairs = switch (value) {
|
||||
.doc => |p| p,
|
||||
else => return null,
|
||||
};
|
||||
if (pairs.len > 0 and !all_operator_keys(pairs)) return null;
|
||||
return pairs;
|
||||
}
|
||||
|
||||
fn field_matches(gpa: std.mem.Allocator, path: []const u8, expected: bson.Value, doc: *const bson.Document) QueryError!bool {
|
||||
@@ -220,13 +230,7 @@ fn match_operator(gpa: std.mem.Allocator, op: []const u8, value: bson.Value, act
|
||||
.doc => |pairs| pairs,
|
||||
else => return false,
|
||||
};
|
||||
var all_operators = operand.len > 0;
|
||||
for (operand) |p| {
|
||||
if (p.key.len == 0 or p.key[0] != '$') {
|
||||
all_operators = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const all_operators = all_operator_keys(operand);
|
||||
for (actuals) |a| {
|
||||
if (a != .array) continue;
|
||||
for (a.array) |elem| {
|
||||
@@ -258,8 +262,9 @@ fn match_operator(gpa: std.mem.Allocator, op: []const u8, value: bson.Value, act
|
||||
/// Collect values reachable at `path` (dot-separated), descending into
|
||||
/// documents and, per MongoDB multikey semantics, into arrays of documents.
|
||||
/// Appends into `out`; on OOM, collection stops early (the engine is
|
||||
/// already failing at that point).
|
||||
fn collect_values(gpa: std.mem.Allocator, pairs: []const bson.Pair, path: []const u8, out: *std.ArrayListUnmanaged(bson.Value), depth: usize) QueryError!void {
|
||||
/// already failing at that point). Public because index entry generation
|
||||
/// must mirror field_matches exactly (src/index.zig).
|
||||
pub fn collect_values(gpa: std.mem.Allocator, pairs: []const bson.Pair, 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;
|
||||
|
||||
@@ -550,7 +555,7 @@ pub const SortKey = struct {
|
||||
/// Sort `docs` in place by `keys`. Candidate values are collected up front
|
||||
/// (allocations happen before the sort), so the comparator itself is pure
|
||||
/// and cannot fail — OOM during collection propagates as QueryError.
|
||||
pub fn sort_docs(gpa: std.mem.Allocator, arena: std.mem.Allocator, docs: []*const bson.Document, keys: []const SortKey) QueryError!void {
|
||||
pub fn sort_docs(arena: std.mem.Allocator, docs: []*const bson.Document, keys: []const SortKey) QueryError!void {
|
||||
if (keys.len == 0 or docs.len < 2) return;
|
||||
|
||||
const SortedDoc = struct {
|
||||
@@ -583,14 +588,13 @@ pub fn sort_docs(gpa: std.mem.Allocator, arena: std.mem.Allocator, docs: []*cons
|
||||
std.mem.sort(SortedDoc, entries, Ctx{ .keys = keys }, Ctx.lessThan);
|
||||
|
||||
for (entries, 0..) |e, i| docs[i] = e.doc;
|
||||
_ = gpa;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Projection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub const ProjectionError = error{ OutOfMemory, InvalidProjection };
|
||||
pub const ProjectionError = std.mem.Allocator.Error;
|
||||
|
||||
/// Apply a projection document, writing resulting pairs into `out` (which
|
||||
/// should use the caller's arena so strings are owned).
|
||||
@@ -614,7 +618,7 @@ pub fn project(arena: std.mem.Allocator, doc: *const bson.Document, proj: *const
|
||||
}
|
||||
if (include_id) {
|
||||
if (bson.get_pair(doc.pairs, "_id")) |idv| {
|
||||
try out.append(arena, .{ .key = try arena.dupe(u8, "_id"), .value = try bson.copy_value(arena, idv) });
|
||||
try out.append(arena, .{ .key = "_id", .value = try bson.copy_value(arena, idv) });
|
||||
}
|
||||
}
|
||||
for (proj.pairs) |p| {
|
||||
@@ -659,10 +663,7 @@ fn exclude_doc(arena: std.mem.Allocator, pairs: []const bson.Pair, proj: *const
|
||||
}
|
||||
|
||||
fn is_excluded(proj: *const bson.Document, key: []const u8) bool {
|
||||
for (proj.pairs) |pp| {
|
||||
if (std.mem.eql(u8, pp.key, key)) return true;
|
||||
}
|
||||
return false;
|
||||
return bson.get_pair(proj.pairs, key) != null;
|
||||
}
|
||||
|
||||
fn has_deeper_exclusion(proj: *const bson.Document, key: []const u8) bool {
|
||||
@@ -875,17 +876,17 @@ test "sort compares by BSON order" {
|
||||
|
||||
var docs = [_]*const bson.Document{ &b, &a };
|
||||
const asc = [_]SortKey{.{ .path = "n", .descending = false }};
|
||||
try sort_docs(testing.allocator, arena.allocator(), &docs, &asc);
|
||||
try sort_docs(arena.allocator(), &docs, &asc);
|
||||
try testing.expect(docs[0] == &a);
|
||||
try testing.expect(docs[1] == &b);
|
||||
|
||||
var docs2 = [_]*const bson.Document{ &a, &b };
|
||||
const desc = [_]SortKey{.{ .path = "n", .descending = true }};
|
||||
try sort_docs(testing.allocator, arena.allocator(), &docs2, &desc);
|
||||
try sort_docs(arena.allocator(), &docs2, &desc);
|
||||
try testing.expect(docs2[0] == &b);
|
||||
|
||||
const missing = [_]SortKey{.{ .path = "zz", .descending = false }};
|
||||
try sort_docs(testing.allocator, arena.allocator(), &docs2, &missing);
|
||||
try sort_docs(arena.allocator(), &docs2, &missing);
|
||||
try testing.expect(docs2[0] == &b); // stable-ish: order untouched by missing key
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user