//! Query engine: filter matching (MongoDB query operators), sorting by //! canonical BSON order, and projections. Includes a small backtracking //! regex engine for $regex. const std = @import("std"); const bson = @import("bson.zig"); // --------------------------------------------------------------------------- // Filter matching // --------------------------------------------------------------------------- /// The byte matcher operates on stored (canonical, validated) bytes, so an /// InvalidBson from the walker means a storage bug rather than hostile /// input — but it must still be a possible error, not a panic. pub const QueryError = error{ OutOfMemory, InvalidBson }; pub fn matches( gpa: std.mem.Allocator, filter: *const bson.Document, doc: *const bson.Document, ) QueryError!bool { for (filter.pairs) |p| { if (p.key.len > 0 and p.key[0] == '$') { if (!try match_top_level(gpa, p.key, p.value, doc)) return false; } else { if (!try field_matches(gpa, p.key, p.value, doc)) return false; } } return true; } fn match_top_level( gpa: std.mem.Allocator, op: []const u8, value: bson.Value, doc: *const bson.Document, ) QueryError!bool { if (std.mem.eql(u8, op, "$and") or std.mem.eql(u8, op, "$or")) { const want_and = std.mem.eql(u8, op, "$and"); const filters = switch (value) { .array => |arr| arr, else => return false, }; for (filters) |item| { const f = switch (item) { .doc => |pairs| pairs, else => return false, }; const matched = try matches(gpa, &.{ .arena = undefined, .pairs = f }, doc); if (want_and and !matched) return false; if (!want_and and matched) return true; } return want_and; } if (std.mem.eql(u8, op, "$nor")) { const filters = switch (value) { .array => |arr| arr, else => return false, }; for (filters) |item| { const f = switch (item) { .doc => |pairs| pairs, else => return false, }; if (try matches(gpa, &.{ .arena = undefined, .pairs = f }, doc)) return false; } return true; } 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 { const pairs = switch (value) { .doc => |p| p, else => return null, }; if (pairs.len > 0 and !all_operator_keys(pairs)) return null; return pairs; } /// Values a path yields before spilling to the heap. A document almost /// always contributes exactly one value per field; arrays make it a /// handful. Collecting those on the stack removes an allocate/free pair per /// filter field per candidate document, which is the dominant cost of a /// collection scan. const inline_candidates = 8; fn field_matches( gpa: std.mem.Allocator, path: []const u8, expected: bson.Value, doc: *const bson.Document, ) QueryError!bool { var stack_fallback = std.heap.stackFallback(inline_candidates * @sizeOf(bson.Value), gpa); const alloc = stack_fallback.get(); var candidates: std.ArrayListUnmanaged(bson.Value) = .empty; defer candidates.deinit(alloc); try collect_values(alloc, doc.pairs, path, &candidates, 0); try expand_arrays(alloc, &candidates); return apply_expected(gpa, expected, candidates.items); } /// The byte counterpart of field_matches: collects values by walking the /// canonical BSON element stream of a stored document, skipping by length /// any field the filter does not name. fn field_matches_bytes( gpa: std.mem.Allocator, path: []const u8, expected: bson.Value, bytes: []const u8, ) QueryError!bool { // An arena, not a stack fallback: the byte walker materializes nested // doc/array values (whole-array equality, embedded docs) into the // allocator it is given, and those must be freed with it. var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const alloc = arena.allocator(); var candidates: std.ArrayListUnmanaged(bson.Value) = .empty; try collect_values_bytes(alloc, bytes, path, &candidates, 0); try expand_arrays(alloc, &candidates); return apply_expected(gpa, expected, candidates.items); } /// MongoDB applies queries to array elements as well as the array itself. /// Index the snapshot length, re-reading items each iteration: appending /// may reallocate the buffer, which would invalidate a captured slice. fn expand_arrays( alloc: std.mem.Allocator, candidates: *std.ArrayListUnmanaged(bson.Value), ) QueryError!void { const direct_count = candidates.items.len; var i: usize = 0; while (i < direct_count) : (i += 1) { const a = candidates.items[i]; if (a == .array) { for (a.array) |elem| try candidates.append(alloc, elem); } } } /// The operator/equality half of field matching, shared by the tree and /// byte collectors. fn apply_expected( gpa: std.mem.Allocator, expected: bson.Value, candidates: []const bson.Value, ) QueryError!bool { if (is_operator_doc(expected)) |pairs| { // $options modifies $regex wherever it appears in the document, so // it has to be known before any operator runs. var options: []const u8 = ""; for (pairs) |p| { if (parse_op(p.key) == .options and p.value == .string) options = p.value.string; } for (pairs) |p| { const op = parse_op(p.key); if (op == .options) continue; if (!try match_operator(gpa, op, p.value, candidates, options)) return false; } return true; } // Bare BSON regex value: {field: /re/} behaves like {$regex: "re"}. if (expected == .regex) { for (candidates) |a| { if (a == .string and regex_match(expected.regex.pattern, expected.regex.options, a.string)) return true; } return false; } // Bare equality — matches if any candidate equals the expected value. for (candidates) |actual| { if (bson.compare(actual, expected) == .eq) return true; } return false; } /// Whether a stored document (canonical BSON bytes) matches `filter` — the /// byte-matcher counterpart of `matches`, used by scans. Same semantics, /// different collection: fields the filter does not name are skipped by /// length instead of materialized. pub fn matches_bytes( gpa: std.mem.Allocator, filter: []const bson.Pair, bytes: []const u8, ) QueryError!bool { for (filter) |p| { if (p.key.len > 0 and p.key[0] == '$') { if (!try match_top_level_bytes(gpa, p.key, p.value, bytes)) return false; } else { if (!try field_matches_bytes(gpa, p.key, p.value, bytes)) return false; } } return true; } fn match_top_level_bytes( gpa: std.mem.Allocator, op: []const u8, value: bson.Value, bytes: []const u8, ) QueryError!bool { if (std.mem.eql(u8, op, "$and") or std.mem.eql(u8, op, "$or")) { const want_and = std.mem.eql(u8, op, "$and"); const filters = switch (value) { .array => |arr| arr, else => return false, }; for (filters) |item| { const f = switch (item) { .doc => |pairs| pairs, else => return false, }; const matched = try matches_bytes(gpa, f, bytes); if (want_and and !matched) return false; if (!want_and and matched) return true; } return want_and; } if (std.mem.eql(u8, op, "$nor")) { const filters = switch (value) { .array => |arr| arr, else => return false, }; for (filters) |item| { const f = switch (item) { .doc => |pairs| pairs, else => return false, }; if (try matches_bytes(gpa, f, bytes)) return false; } return true; } return false; } /// Collect values reachable at `path` from a document's canonical bytes — /// the byte counterpart of `collect_values`, with the same traversal, the /// same order and the same multikey semantics. Appends into `out`. pub fn collect_values_bytes( gpa: std.mem.Allocator, bytes: []const u8, 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; const rest = it.rest(); var idx: usize = 4; // skip the document length prefix while (idx + 1 < bytes.len and bytes[idx] != 0) { const tag = bytes[idx]; idx += 1; const key = bson.element_key(bytes, &idx) orelse return; if (std.mem.eql(u8, key, first)) { if (rest.len == 0) { if (depth < 8) { try out.append(gpa, try bson.read_value(gpa, bytes, &idx, tag)); } else { try bson.skip_value(bytes, &idx, tag); } } else { try collect_from_value_bytes(gpa, bytes, &idx, tag, rest, out, depth + 1); } } else { try bson.skip_value(bytes, &idx, tag); } } } fn collect_from_value_bytes( gpa: std.mem.Allocator, bytes: []const u8, idx: *usize, tag: u8, path: []const u8, out: *std.ArrayListUnmanaged(bson.Value), depth: usize, ) QueryError!void { if (depth > 8) { try bson.skip_value(bytes, idx, tag); return; } switch (tag) { 0x03 => { const start = idx.*; try collect_values_bytes(gpa, bytes[start..], path, out, depth); idx.* = start + std.mem.readInt(u32, bytes[start..][0..4], .little); }, 0x04 => { const start = idx.*; const total: u32 = std.mem.readInt(u32, bytes[start..][0..4], .little); const array_end = start + total; var pit = std.mem.splitScalar(u8, path, '.'); const seg = pit.next() orelse return; if (std.fmt.parseInt(usize, seg, 10)) |aidx| { var e: usize = 0; var a = start + 4; while (a < array_end - 1 and bytes[a] != 0) { const atag = bytes[a]; a += 1; _ = bson.element_key(bytes, &a) orelse return; if (e == aidx) { const rest = pit.rest(); if (rest.len == 0) { if (depth < 8) { try out.append(gpa, try bson.read_value(gpa, bytes, &a, atag)); } else { try bson.skip_value(bytes, &a, atag); } } else { try collect_from_value_bytes(gpa, bytes, &a, atag, rest, out, depth + 1); } break; } try bson.skip_value(bytes, &a, atag); e += 1; } } else |_| { // Multikey semantics: descend into embedded documents. var a = start + 4; while (a < array_end - 1 and bytes[a] != 0) { const atag = bytes[a]; a += 1; _ = bson.element_key(bytes, &a) orelse return; if (atag == 0x03) { const dstart = a; try collect_values_bytes(gpa, bytes[dstart..], path, out, depth); } try bson.skip_value(bytes, &a, atag); } } idx.* = array_end; }, else => try bson.skip_value(bytes, idx, tag), } } /// The query operators, resolved from their names once per filter field /// instead of re-comparing strings for every candidate document. const Op = enum { eq, ne, gt, gte, lt, lte, in, nin, exists, regex, options, not, size, all, elem_match, /// Not an operator we implement; matches nothing, as before. unknown, }; const op_names = std.StaticStringMap(Op).initComptime(.{ .{ "$eq", .eq }, .{ "$ne", .ne }, .{ "$gt", .gt }, .{ "$gte", .gte }, .{ "$lt", .lt }, .{ "$lte", .lte }, .{ "$in", .in }, .{ "$nin", .nin }, .{ "$exists", .exists }, .{ "$regex", .regex }, .{ "$options", .options }, .{ "$not", .not }, .{ "$size", .size }, .{ "$all", .all }, .{ "$elemMatch", .elem_match }, }); fn parse_op(name: []const u8) Op { return op_names.get(name) orelse .unknown; } fn match_operator( gpa: std.mem.Allocator, op: Op, value: bson.Value, actuals: []const bson.Value, regex_options: []const u8, ) QueryError!bool { if (op == .eq) { for (actuals) |a| if (bson.compare(a, value) == .eq) return true; return false; } if (op == .ne) { for (actuals) |a| if (bson.compare(a, value) == .eq) return false; return true; } if (op == .gt or op == .gte or op == .lt or op == .lte) { for (actuals) |a| { const o = bson.compare(a, value); const hit = switch (op) { .gt => o == .gt, .gte => o != .lt, .lt => o == .lt, .lte => o != .gt, else => unreachable, }; if (hit) return true; } return false; } if (op == .in or op == .nin) { const members = switch (value) { .array => |arr| arr, else => return false, }; const want_in = op == .in; for (actuals) |a| { for (members) |m| { if (bson.compare(a, m) == .eq) return want_in; } } return !want_in; } if (op == .exists) { const want = switch (value) { .bool => |b| b, else => return false, }; return (actuals.len > 0) == want; } if (op == .regex) { const pattern = switch (value) { .string => |s| s, .doc => |pairs| blk: { const pat = bson.get_pair(pairs, "$regex") orelse return false; break :blk switch (pat) { .string => |s| s, else => return false, }; }, else => return false, }; for (actuals) |a| { if (a == .string and regex_match(pattern, regex_options, a.string)) return true; } return false; } if (op == .not) { const pairs = is_operator_doc(value) orelse { // $not with a bare value means $ne-ish semantics; treat as // "not equal to this regex or value". if (value == .string) { for (actuals) |a| { if (a == .string and regex_match(value.string, "", a.string)) return false; } return true; } return false; }; for (pairs) |p| { if (try match_operator(gpa, parse_op(p.key), p.value, actuals, regex_options)) return false; } return true; } if (op == .size) { const want = switch (value) { .int32 => |i| i, .int64 => |i| @as(i32, @intCast(i)), else => return false, }; for (actuals) |a| { if (a == .array and a.array.len == @as(usize, @intCast(want))) return true; } return false; } if (op == .all) { const members = switch (value) { .array => |arr| arr, else => return false, }; outer: for (members) |m| { for (actuals) |a| { if (a == .array) { for (a.array) |elem| { if (bson.compare(elem, m) == .eq) continue :outer; } } } return false; } return true; } if (op == .elem_match) { const operand = switch (value) { .doc => |pairs| pairs, else => return false, }; const all_operators = all_operator_keys(operand); for (actuals) |a| { if (a != .array) continue; for (a.array) |elem| { if (all_operators) { var single: [1]bson.Value = .{elem}; var ok = true; for (operand) |p| { if (!try match_operator(gpa, parse_op(p.key), p.value, single[0..], "")) { ok = false; break; } } if (ok) return true; } else { switch (elem) { .doc => |pairs| { if (try matches(gpa, &.{ .arena = undefined, .pairs = operand }, &.{ .arena = undefined, .pairs = pairs })) return true; }, else => {}, } } } } return false; } return false; } /// 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). 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; for (pairs) |p| { if (!std.mem.eql(u8, p.key, first)) continue; const rest = it.rest(); if (rest.len == 0) { if (depth < 8) { try out.append(gpa, p.value); } } else { try collect_from_value(gpa, p.value, rest, out, depth + 1); } } } fn collect_from_value( gpa: std.mem.Allocator, v: bson.Value, path: []const u8, out: *std.ArrayListUnmanaged(bson.Value), depth: usize, ) QueryError!void { if (depth > 8) return; switch (v) { .doc => |pairs| try collect_values(gpa, pairs, path, out, depth), .array => |items| { // Numeric first segment: address an element by index ("tags.0"). var pit = std.mem.splitScalar(u8, path, '.'); const seg = pit.next() orelse return; if (std.fmt.parseInt(usize, seg, 10)) |idx| { if (idx < items.len) { const rest = pit.rest(); if (rest.len == 0) { if (depth < 8) try out.append(gpa, items[idx]); } else { try collect_from_value(gpa, items[idx], rest, out, depth + 1); } } return; } else |_| {} // Multikey semantics: descend into embedded documents of the array. for (items) |item| { switch (item) { .doc => try collect_values(gpa, item.doc, path, out, depth), else => {}, } } }, else => return, } } // --------------------------------------------------------------------------- // Regex (subset): ^ $ . * + ? [...] ( ) | and escaped literals // --------------------------------------------------------------------------- pub fn regex_match(pattern: []const u8, options: []const u8, text: []const u8) bool { // Bound recursion depth: deeply nested groups would overflow the stack. var depth: usize = 0; var max_depth: usize = 0; var ri: usize = 0; while (ri < pattern.len) : (ri += 1) { if (pattern[ri] == '\\') { ri += 1; continue; } if (pattern[ri] == '(') { depth += 1; max_depth = @max(max_depth, depth); } else if (pattern[ri] == ')') { depth -|= 1; } } if (max_depth > 256) return false; const case_insensitive = std.mem.indexOfScalar(u8, options, 'i') != null; const dot_all = std.mem.indexOfScalar(u8, options, 's') != null; const anchored = pattern.len > 0 and pattern[0] == '^'; const start_pattern = if (anchored) pattern[1..] else pattern; var p: usize = 0; if (anchored) { if (match_here(start_pattern, &p, text, 0, case_insensitive, dot_all) == null) return false; return p == start_pattern.len; } var t: usize = 0; while (t <= text.len) : (t += 1) { p = 0; if (match_here(start_pattern, &p, text, t, case_insensitive, dot_all) != null and p == start_pattern.len) return true; } return false; } /// Match `pattern[p..]` against `text[t..]`, returning the new text /// position on success (null on failure). Backtracks via recursion. fn match_here( pattern: []const u8, p: *usize, text: []const u8, t: usize, ci: bool, dot_all: bool, ) ?usize { var pos = t; while (p.* < pattern.len) { const c = pattern[p.*]; switch (c) { '$' => { if (p.* + 1 == pattern.len) { p.* += 1; return if (pos == text.len) pos else null; } if (pos >= text.len) return null; if (!chars_eq(c, text[pos], ci)) return null; p.* += 1; pos += 1; }, '^' => { if (pos != 0) return null; p.* += 1; }, '(' => { const end = find_group_end(pattern, p.*) orelse return null; const inner = pattern[p.* + 1 .. end - 1]; var parts: [8][]const u8 = undefined; var nparts: usize = 0; var seg_start: usize = 0; var depth: usize = 0; var i: usize = 0; while (i < inner.len) : (i += 1) { const ic = inner[i]; if (ic == '\\') { i += 1; continue; } if (ic == '(') depth +|= 1; if (ic == ')') depth -|= 1; if (ic == '|' and depth == 0) { if (nparts < parts.len) parts[nparts] = inner[seg_start..i]; nparts += 1; seg_start = i + 1; } } if (nparts < parts.len) parts[nparts] = inner[seg_start..]; nparts += 1; if (nparts == 1) { var gp: usize = 0; const after = match_here(parts[0], &gp, text, pos, ci, dot_all) orelse return null; if (gp != parts[0].len) return null; p.* = end; pos = after; } else { var matched = false; for (parts[0..nparts]) |part| { var gp: usize = 0; const after = match_here(part, &gp, text, pos, ci, dot_all) orelse continue; if (gp == part.len) { p.* = end; pos = after; matched = true; break; } } if (!matched) return null; } }, ')' => return null, // unbalanced '\\' => { if (p.* + 1 >= pattern.len) return null; const lit = pattern[p.* + 1]; if (pos >= text.len or !chars_eq(lit, text[pos], ci)) return null; p.* += 2; pos += 1; }, else => { var element_end: usize = undefined; if (c == '[') { var close = p.* + 1; if (close < pattern.len and pattern[close] == '^') close += 1; while (close < pattern.len and pattern[close] != ']') close += 1; if (close >= pattern.len) return null; element_end = close + 1; } else { element_end = p.* + 1; } const element = pattern[p.*..element_end]; var q_end = element_end; var min: usize = 1; var max: usize = 1; if (element_end < pattern.len and (pattern[element_end] == '*' or pattern[element_end] == '+' or pattern[ element_end ] == '?')) { switch (pattern[element_end]) { '*' => { min = 0; max = std.math.maxInt(usize); }, '+' => { min = 1; max = std.math.maxInt(usize); }, '?' => { min = 0; }, else => {}, } q_end = element_end + 1; } // Greedy: consume as many as possible, then backtrack. var consumed: usize = 0; var pos_cur = pos; while (max == std.math.maxInt(usize) or consumed < max) { if (element_matches(element, text, pos_cur, ci, dot_all)) { pos_cur += 1; consumed += 1; } else break; } var attempt = consumed; while (attempt >= min) : (attempt -= 1) { p.* = q_end; if (match_here(pattern, p, text, pos_cur - (consumed - attempt), ci, dot_all)) |after| { return after; } if (attempt == 0) break; } return null; }, } } return pos; } fn chars_eq(a: u8, b: u8, ci: bool) bool { if (ci) return std.ascii.toLower(a) == std.ascii.toLower(b); return a == b; } fn in_range(lo: u8, hi: u8, c: u8, ci: bool) bool { if (ci) { const l = std.ascii.toLower(c); return l >= std.ascii.toLower(lo) and l <= std.ascii.toLower(hi); } return c >= lo and c <= hi; } fn find_group_end(pattern: []const u8, open: usize) ?usize { var depth: usize = 1; var i = open + 1; while (i < pattern.len) : (i += 1) { if (pattern[i] == '\\') { i += 1; continue; } if (pattern[i] == '(') depth += 1; if (pattern[i] == ')') { depth -= 1; if (depth == 0) return i + 1; } } return null; } fn element_matches(element: []const u8, text: []const u8, t: usize, ci: bool, dot_all: bool) bool { if (t >= text.len) return false; if (element[0] == '.') { return dot_all or text[t] != '\n'; } if (element[0] == '[') { var negated = false; var i: usize = 1; if (i < element.len and element[i] == '^') { negated = true; i += 1; } var matched = false; while (i < element.len and element[i] != ']') { if (i + 2 < element.len and element[i + 1] == '-') { if (in_range(element[i], element[i + 2], text[t], ci)) matched = true; i += 3; } else { if (chars_eq(element[i], text[t], ci)) matched = true; i += 1; } } return matched != negated; } return chars_eq(element[0], text[t], ci); } // --------------------------------------------------------------------------- // Sort // --------------------------------------------------------------------------- pub const SortKey = struct { path: []const u8, descending: bool, }; /// The first value `collect_values` would append at `path`, or null when it /// would append none. Same traversal in the same order, same depth cutoff — /// it just stops at the first hit instead of building a list. /// /// Sorting only ever reads element 0 of the collected list, so materializing /// the rest cost one allocation per document per sort key. pub fn first_value_at(pairs: []const bson.Pair, path: []const u8, depth: usize) ?bson.Value { var it = std.mem.splitScalar(u8, path, '.'); const first = it.next() orelse return null; const rest = it.rest(); for (pairs) |p| { if (!std.mem.eql(u8, p.key, first)) continue; if (rest.len == 0) { if (depth < 8) return p.value; } else { if (first_from_value(p.value, rest, depth + 1)) |v| return v; } } return null; } fn first_from_value(v: bson.Value, path: []const u8, depth: usize) ?bson.Value { if (depth > 8) return null; switch (v) { .doc => |pairs| return first_value_at(pairs, path, depth), .array => |items| { var pit = std.mem.splitScalar(u8, path, '.'); const seg = pit.next() orelse return null; if (std.fmt.parseInt(usize, seg, 10)) |idx| { if (idx >= items.len) return null; const rest = pit.rest(); if (rest.len == 0) { if (depth < 8) return items[idx]; return null; } return first_from_value(items[idx], rest, depth + 1); } else |_| {} for (items) |item| { switch (item) { .doc => if (first_value_at(item.doc, path, depth)) |x| return x, else => {}, } } return null; }, else => return null, } } /// A document paired with the one value per sort key the comparator reads. const SortedDoc = struct { doc: *const bson.Document, /// `keys.len` values; a path that yields nothing sorts as null. vals: []const bson.Value, }; const SortCtx = struct { keys: []const SortKey, fn less(ctx: @This(), a: SortedDoc, b: SortedDoc) bool { for (ctx.keys, 0..) |k, ki| { const o = bson.compare(a.vals[ki], b.vals[ki]); if (o != .eq) return if (k.descending) o == .gt else o == .lt; } return false; } }; /// Pull each document's sort-key values into one flat allocation, so the /// comparator is pure and cannot fail. fn decorate( arena: std.mem.Allocator, docs: []*const bson.Document, keys: []const SortKey, ) QueryError![]SortedDoc { const entries = try arena.alloc(SortedDoc, docs.len); const flat = try arena.alloc(bson.Value, docs.len * keys.len); for (docs, 0..) |d, i| { const vals = flat[i * keys.len ..][0..keys.len]; for (keys, 0..) |k, ki| { vals[ki] = first_value_at(d.pairs, k.path, 0) orelse .null; } entries[i] = .{ .doc = d, .vals = vals }; } return entries; } /// Sort `docs` in place by `keys`. 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 entries = try decorate(arena, docs, keys); std.mem.sort(SortedDoc, entries, SortCtx{ .keys = keys }, SortCtx.less); for (entries, 0..) |e, i| docs[i] = e.doc; } /// Place the `k` smallest documents by `keys`, in order, at the front of /// `docs`. **`docs[k..]` is left in an unspecified order** — callers must /// only read the first `k`. /// /// A query that sorts a whole collection to return one page pays /// n log n comparisons to discard almost all of the result. This keeps a /// k-element max-heap instead: one comparison against the heap root per /// document, and only the survivors are ever ordered. pub fn sort_docs_top_k( arena: std.mem.Allocator, docs: []*const bson.Document, keys: []const SortKey, k: usize, ) QueryError!void { if (keys.len == 0 or docs.len < 2) return; if (k == 0) return; if (k >= docs.len) return sort_docs(arena, docs, keys); const entries = try decorate(arena, docs, keys); const ctx = SortCtx{ .keys = keys }; // Max-heap over the first k: the root is the worst of the best-so-far. var i = k / 2; while (i > 0) { i -= 1; sift_down(entries[0..k], i, ctx); } // Anything better than the root replaces it; anything worse is dropped // after a single comparison. for (entries[k..]) |e| { if (!ctx.less(e, entries[0])) continue; entries[0] = e; sift_down(entries[0..k], 0, ctx); } // Drain the heap back-to-front, which leaves entries[0..k] ascending. var end = k; while (end > 1) { end -= 1; const tmp = entries[0]; entries[0] = entries[end]; entries[end] = tmp; sift_down(entries[0..end], 0, ctx); } for (entries[0..k], 0..) |e, j| docs[j] = e.doc; } /// Restore the max-heap property at `root` over `heap`. fn sift_down(heap: []SortedDoc, root: usize, ctx: SortCtx) void { var parent = root; while (true) { const left = parent * 2 + 1; if (left >= heap.len) return; const right = left + 1; // The larger child under `less`, i.e. the one that must rise. var largest = left; if (right < heap.len and ctx.less(heap[left], heap[right])) largest = right; if (!ctx.less(heap[parent], heap[largest])) return; const tmp = heap[parent]; heap[parent] = heap[largest]; heap[largest] = tmp; parent = largest; } } // --------------------------------------------------------------------------- // Projection // --------------------------------------------------------------------------- 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). pub fn project( arena: std.mem.Allocator, doc: *const bson.Document, proj: *const bson.Document, out: *std.ArrayListUnmanaged(bson.Pair), ) ProjectionError!void { var inclusion: ?bool = null; var non_id_count: usize = 0; for (proj.pairs) |p| { if (std.mem.eql(u8, p.key, "_id")) continue; non_id_count += 1; const flag = truthy(p.value); inclusion = if (inclusion == null) flag else inclusion; } // {_id: 0} alone means "drop _id, keep everything else". const include = if (non_id_count > 0) (inclusion orelse true) else false; if (include) { // Inclusion list: _id unless excluded, plus listed paths. var include_id = true; if (bson.get_pair(proj.pairs, "_id")) |idv| { include_id = truthy(idv); } if (include_id) { if (bson.get_pair(doc.pairs, "_id")) |idv| { try out.append(arena, .{ .key = "_id", .value = try bson.copy_value(arena, idv) }); } } for (proj.pairs) |p| { if (std.mem.eql(u8, p.key, "_id")) continue; if (!truthy(p.value)) continue; try project_path(arena, doc.pairs, p.key, out); } } else { // Exclusion: copy everything except excluded paths (and _id if set). for (doc.pairs) |p| { if (std.mem.eql(u8, p.key, "_id")) { var excluded = false; if (bson.get_pair(proj.pairs, "_id")) |idv| excluded = !truthy(idv); if (excluded) continue; } if (is_excluded(proj, p.key)) continue; if (p.value == .doc and has_deeper_exclusion(proj, p.key)) { const filtered = try exclude_doc(arena, p.value.doc, proj, p.key); try out.append(arena, .{ .key = try arena.dupe(u8, p.key), .value = .{ .doc = filtered } }); } else { try out.append(arena, .{ .key = try arena.dupe(u8, p.key), .value = try bson.copy_value(arena, p.value) }); } } } } /// Recursively apply exclusions to a nested document given the parent path. fn exclude_doc( arena: std.mem.Allocator, pairs: []const bson.Pair, proj: *const bson.Document, parent: []const u8, ) ProjectionError![]const bson.Pair { var out: std.ArrayListUnmanaged(bson.Pair) = .empty; errdefer out.deinit(arena); for (pairs) |p| { const full = if (parent.len > 0) try std.fmt.allocPrint(arena, "{s}.{s}", .{ parent, p.key }) else p.key; if (is_excluded(proj, full)) continue; if (p.value == .doc and has_deeper_exclusion(proj, full)) { const filtered = try exclude_doc(arena, p.value.doc, proj, full); try out.append(arena, .{ .key = try arena.dupe(u8, p.key), .value = .{ .doc = filtered } }); } else { try out.append(arena, .{ .key = try arena.dupe(u8, p.key), .value = try bson.copy_value(arena, p.value) }); } } return out.toOwnedSlice(arena); } fn is_excluded(proj: *const bson.Document, key: []const u8) bool { return bson.get_pair(proj.pairs, key) != null; } fn has_deeper_exclusion(proj: *const bson.Document, key: []const u8) bool { for (proj.pairs) |pp| { if (is_prefix_or_equal(key, pp.key)) return true; } return false; } /// MongoDB's truthiness for an option/flag value (projection flags, index /// spec options). pub fn truthy(v: bson.Value) bool { return switch (v) { .bool => |b| b, .int32 => |i| i != 0, .int64 => |i| i != 0, .double => |d| d != 0, else => false, }; } /// Include a dotted path (e.g. "a.b.c"), creating nested documents as needed. fn project_path( arena: std.mem.Allocator, pairs: []const bson.Pair, path: []const u8, out: *std.ArrayListUnmanaged(bson.Pair), ) ProjectionError!void { var it = std.mem.splitScalar(u8, path, '.'); const first = it.next() orelse return; const rest = it.rest(); // Does the doc have this top-level field? const field = bson.get_pair(pairs, first); if (rest.len == 0) { if (field) |f| { try out.append(arena, .{ .key = try arena.dupe(u8, first), .value = try bson.copy_value(arena, f) }); } return; } if (field) |f| { switch (f) { .doc => |sub| { var nested: std.ArrayListUnmanaged(bson.Pair) = .empty; errdefer nested.deinit(arena); try project_path(arena, sub, rest, &nested); if (nested.items.len > 0) { try out.append(arena, .{ .key = try arena.dupe(u8, first), .value = .{ .doc = try nested.toOwnedSlice(arena) } }); } }, else => {}, } } } fn is_prefix_or_equal(prefix: []const u8, key: []const u8) bool { if (std.mem.eql(u8, prefix, key)) return true; if (prefix.len < key.len and std.mem.eql(u8, prefix, key[0..prefix.len])) { return key[prefix.len] == '.'; } return false; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- const testing = std.testing; fn doc_of(pairs: []const bson.Pair) bson.Document { return .{ .arena = undefined, .pairs = pairs }; } test "basic filters" { const d = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "age", .value = .{ .int32 = 30 } }, .{ .key = "name", .value = .{ .string = "alice" } }, .{ .key = "tags", .value = .{ .array = &.{ .{ .string = "a" }, .{ .string = "b" } } } }, }); try testing.expect(try matches(testing.allocator, &doc_of(&.{}), &d)); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "age", .value = .{ .int32 = 30 } }}), &d)); try testing.expect(!try matches(testing.allocator, &doc_of(&.{.{ .key = "age", .value = .{ .int32 = 31 } }}), &d)); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "age", .value = .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = 20 } }} } }}), &d)); try testing.expect(!try matches(testing.allocator, &doc_of(&.{.{ .key = "age", .value = .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = 30 } }} } }}), &d)); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "age", .value = .{ .doc = &.{.{ .key = "$in", .value = .{ .array = &.{ .{ .int32 = 29 }, .{ .int32 = 30 } } } }} } }}), &d)); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "missing", .value = .{ .doc = &.{.{ .key = "$exists", .value = .{ .bool = false } }} } }}), &d)); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "tags", .value = .{ .string = "b" } }}), &d)); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "tags", .value = .{ .doc = &.{.{ .key = "$size", .value = .{ .int32 = 2 } }} } }}), &d)); } test "regex filter" { const d = doc_of(&.{.{ .key = "name", .value = .{ .string = "Alice Smith" } }}); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "name", .value = .{ .doc = &.{ .{ .key = "$regex", .value = .{ .string = "^al" } }, .{ .key = "$options", .value = .{ .string = "i" } }, } } }}), &d)); try testing.expect(!try matches(testing.allocator, &doc_of(&.{.{ .key = "name", .value = .{ .doc = &.{.{ .key = "$regex", .value = .{ .string = "^X" } }} } }}), &d)); try testing.expect(!try matches(testing.allocator, &doc_of(&.{.{ .key = "name", .value = .{ .doc = &.{.{ .key = "$regex", .value = .{ .string = "sm[i]th$" } }} } }}), &d)); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "name", .value = .{ .doc = &.{.{ .key = "$regex", .value = .{ .string = "th$" } }} } }}), &d)); } test "regex engine basics" { try testing.expect(regex_match("abc", "", "xabcx")); try testing.expect(regex_match("^abc", "", "abc")); try testing.expect(!regex_match("^abc", "", "xabc")); try testing.expect(regex_match("a.c", "", "abc")); try testing.expect(regex_match("a*b", "", "aaab")); try testing.expect(regex_match("a+b", "", "aab")); try testing.expect(regex_match("colou?r", "", "color")); try testing.expect(regex_match("colou?r", "", "colour")); try testing.expect(regex_match("[0-9]+", "", "abc123def")); try testing.expect(!regex_match("^[0-9]+$", "", "abc123")); try testing.expect(regex_match("(ab|cd)e", "", "cde")); try testing.expect(regex_match("a\\.b", "", "a.b")); try testing.expect(regex_match("^foo$", "", "foo")); try testing.expect(!regex_match("^foo$", "", "foobar")); try testing.expect(regex_match("hello", "i", "HELLO")); try testing.expect(regex_match("^a+$", "", "aaaa")); try testing.expect(!regex_match("^a+$", "", "aaab")); } test "regex hostile input does not crash" { // Escaped paren inside a group used to underflow the alternation scan. // Pattern is: "(" ++ "\" ++ "))" — a group containing an escaped ')'. const hostile = "(" ++ "\\" ++ "))"; try testing.expect(regex_match(hostile, "", ")")); try testing.expect(!regex_match(hostile, "", "x")); try testing.expect(regex_match("a" ++ "\\" ++ "|b", "", "a|b")); // Deeply nested groups must be rejected, not blow the stack. const deep = "(" ** 300 ++ "x" ++ ")" ** 300; try testing.expect(!regex_match(deep, "", "x")); } test "long array values are not truncated" { var items: [30]bson.Value = undefined; for (0..30) |i| items[i] = .{ .int32 = @intCast(i) }; const d = doc_of(&.{.{ .key = "tags", .value = .{ .array = &items } }}); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "tags", .value = .{ .int32 = 29 } }}), &d)); try testing.expect(!try matches(testing.allocator, &doc_of(&.{.{ .key = "tags", .value = .{ .int32 = 30 } }}), &d)); } test "multikey path with several array candidates does not use-after-free" { // Two array candidates at the path; flattening the first appends past // the initial capacity, which used to realloc the buffer while a // captured slice was still being iterated. var big: [20]bson.Value = undefined; for (0..20) |j| big[j] = .{ .int32 = @intCast(j) }; const d = doc_of(&.{.{ .key = "items", .value = .{ .array = &.{ .{ .doc = &.{.{ .key = "tags", .value = .{ .array = &big } }} }, .{ .doc = &.{.{ .key = "tags", .value = .{ .array = &.{.{ .string = "needle" }} } }} }, } } }}); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "items.tags", .value = .{ .string = "needle" } }}), &d)); // And the flattened long array is searched, not just the first element. try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "items.tags", .value = .{ .int32 = 19 } }}), &d)); } test "OOM during value collection propagates, not a false match" { // Candidate collection failing must surface as an error rather than // leaving an empty candidate list, which would make negating operators // like $ne and $exists:false report a match — a wrong answer, not a // failed one. // // Collection only reaches the allocator once a path yields more than // `inline_candidates` values, so use an array long enough to spill. var many: [32]bson.Value = undefined; for (&many, 0..) |*v, i| v.* = .{ .int32 = @intCast(i) }; const d = doc_of(&.{.{ .key = "x", .value = .{ .array = &many } }}); const ne = doc_of(&.{.{ .key = "x", .value = .{ .doc = &.{.{ .key = "$ne", .value = .{ .int32 = 999 } }} } }}); var buf: [16]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&buf); try testing.expectError(error.OutOfMemory, matches(fba.allocator(), &ne, &d)); const ex = doc_of(&.{.{ .key = "x", .value = .{ .doc = &.{.{ .key = "$exists", .value = .{ .bool = false } }} } }}); try testing.expectError(error.OutOfMemory, matches(fba.allocator(), &ex, &d)); } test "the common single-value match needs no allocator at all" { // The counterpart to the test above: a field yielding a handful of // values is collected on the stack, so a scan does not allocate per // filter field per document. A failing allocator must therefore still // produce the correct answer rather than an error. const d = doc_of(&.{ .{ .key = "x", .value = .{ .int32 = 5 } }, .{ .key = "s", .value = .{ .string = "hi" } }, }); var buf: [0]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&buf); const failing = fba.allocator(); const eq = doc_of(&.{.{ .key = "x", .value = .{ .int32 = 5 } }}); try testing.expect(try matches(failing, &eq, &d)); const ne = doc_of(&.{.{ .key = "x", .value = .{ .doc = &.{.{ .key = "$ne", .value = .{ .int32 = 5 } }} } }}); try testing.expect(!try matches(failing, &ne, &d)); const missing = doc_of(&.{.{ .key = "zz", .value = .{ .doc = &.{.{ .key = "$exists", .value = .{ .bool = false } }} } }}); try testing.expect(try matches(failing, &missing, &d)); const range = doc_of(&.{.{ .key = "x", .value = .{ .doc = &.{ .{ .key = "$gte", .value = .{ .int32 = 1 } }, .{ .key = "$lt", .value = .{ .int32 = 10 } }, } } }}); try testing.expect(try matches(failing, &range, &d)); } test "documents compare by field name too" { try testing.expectEqual(std.math.Order.lt, bson.compare( .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} }, .{ .doc = &.{.{ .key = "b", .value = .{ .int32 = 1 } }} }, )); } test "dot path filters" { const d = doc_of(&.{ .{ .key = "user", .value = .{ .doc = &.{ .{ .key = "profile", .value = .{ .doc = &.{.{ .key = "age", .value = .{ .int32 = 25 } }} } }, } } }, .{ .key = "items", .value = .{ .array = &.{ .{ .doc = &.{.{ .key = "sku", .value = .{ .string = "x" } }} }, .{ .doc = &.{.{ .key = "sku", .value = .{ .string = "y" } }} }, } } }, }); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "user.profile.age", .value = .{ .int32 = 25 } }}), &d)); try testing.expect(!try matches(testing.allocator, &doc_of(&.{.{ .key = "user.profile.age", .value = .{ .int32 = 26 } }}), &d)); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "items.sku", .value = .{ .string = "y" } }}), &d)); } test "array index dot path and bare regex value" { const d = doc_of(&.{ .{ .key = "tags", .value = .{ .array = &.{ .{ .string = "c" }, .{ .string = "a" }, } } }, .{ .key = "name", .value = .{ .string = "carol" } }, }); // tags.0 addresses the first element. try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "tags.0", .value = .{ .string = "c" } }}), &d)); try testing.expect(!try matches(testing.allocator, &doc_of(&.{.{ .key = "tags.1", .value = .{ .string = "c" } }}), &d)); try testing.expect(!try matches(testing.allocator, &doc_of(&.{.{ .key = "tags.5", .value = .{ .string = "c" } }}), &d)); // Bare BSON regex value behaves like {$regex: "re"}. try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "name", .value = .{ .regex = .{ .pattern = "^c", .options = "" } } }}), &d)); try testing.expect(!try matches(testing.allocator, &doc_of(&.{.{ .key = "name", .value = .{ .regex = .{ .pattern = "^z", .options = "" } } }}), &d)); } test "sort compares by BSON order" { const a = doc_of(&.{ .{ .key = "n", .value = .{ .int32 = 2 } }, .{ .key = "x", .value = .{ .string = "a" } } }); const b = doc_of(&.{ .{ .key = "n", .value = .{ .int32 = 10 } }, .{ .key = "x", .value = .{ .string = "b" } } }); var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); var docs = [_]*const bson.Document{ &b, &a }; const asc = [_]SortKey{.{ .path = "n", .descending = false }}; 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(arena.allocator(), &docs2, &desc); try testing.expect(docs2[0] == &b); const missing = [_]SortKey{.{ .path = "zz", .descending = false }}; try sort_docs(arena.allocator(), &docs2, &missing); try testing.expect(docs2[0] == &b); // stable-ish: order untouched by missing key } test "first_value_at agrees with collect_values on its first element" { // This equivalence is the entire correctness argument for the sort // decorate pass, so exercise the traversal shapes that differ: // dotted paths, arrays of documents (multikey), numeric element // addressing, repeated keys, and the depth cutoff. const gpa = testing.allocator; const inner = [_]bson.Pair{ .{ .key = "x", .value = .{ .int32 = 7 } }, .{ .key = "y", .value = .{ .string = "deep" } }, }; const arr_docs = [_]bson.Value{ .{ .doc = &[_]bson.Pair{.{ .key = "v", .value = .{ .int32 = 1 } }} }, .{ .doc = &[_]bson.Pair{.{ .key = "v", .value = .{ .int32 = 2 } }} }, }; const plain_arr = [_]bson.Value{ .{ .int32 = 10 }, .{ .int32 = 20 } }; const d = doc_of(&.{ .{ .key = "n", .value = .{ .int32 = 5 } }, .{ .key = "sub", .value = .{ .doc = &inner } }, .{ .key = "items", .value = .{ .array = &arr_docs } }, .{ .key = "nums", .value = .{ .array = &plain_arr } }, // A repeated key: collect_values appends both, sorting takes the first. .{ .key = "dup", .value = .{ .int32 = 100 } }, .{ .key = "dup", .value = .{ .int32 = 200 } }, }); const paths = [_][]const u8{ "n", "sub", "sub.x", "sub.y", "sub.missing", "items", "items.v", "items.0", "items.1.v", "items.9", "nums", "nums.0", "nums.1", "nums.5", "dup", "missing", "n.deeper", "", "sub.x.y", }; for (paths) |path| { var list: std.ArrayListUnmanaged(bson.Value) = .empty; defer list.deinit(gpa); try collect_values(gpa, d.pairs, path, &list, 0); const first = first_value_at(d.pairs, path, 0); if (list.items.len == 0) { testing.expect(first == null) catch |e| { std.debug.print("path '{s}': collect empty but first_value_at returned a value\n", .{ path, }); return e; }; } else { testing.expect(first != null) catch |e| { std.debug.print("path '{s}': collect got {d} values but first_value_at returned null\n", .{ path, list.items.len, }); return e; }; testing.expectEqual(std.math.Order.eq, bson.compare(list.items[0], first.?)) catch |e| { std.debug.print("path '{s}': first value mismatch\n", .{path}); return e; }; } } } test "top-k selection matches a full sort on the leading page" { const gpa = testing.allocator; var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); // Values with many ties, so the heap's boundary behaviour is exercised. var prng = std.Random.DefaultPrng.init(0xA11CE); const rand = prng.random(); const n = 300; var storage: [n]bson.Document = undefined; var pairs: [n][2]bson.Pair = undefined; for (0..n) |i| { pairs[i] = .{ .{ .key = "a", .value = .{ .int32 = rand.intRangeAtMost(i32, 0, 9) } }, .{ .key = "b", .value = .{ .int32 = @intCast(i) } }, }; storage[i] = doc_of(pairs[i][0..]); } const key_sets = [_][]const SortKey{ &.{.{ .path = "a", .descending = false }}, &.{.{ .path = "a", .descending = true }}, // Second key breaks every tie, so the page is fully determined. &.{ .{ .path = "a", .descending = false }, .{ .path = "b", .descending = false } }, &.{ .{ .path = "a", .descending = true }, .{ .path = "b", .descending = false } }, }; for (key_sets) |keys| { for ([_]usize{ 1, 2, 20, 299, 300, 301 }) |k| { var full: [n]*const bson.Document = undefined; var topk: [n]*const bson.Document = undefined; for (0..n) |i| { full[i] = &storage[i]; topk[i] = &storage[i]; } try sort_docs(arena.allocator(), &full, keys); try sort_docs_top_k(arena.allocator(), &topk, keys, k); const page = @min(k, n); for (0..page) |i| { // Ties make document identity ambiguous, so compare the // sort keys rather than the pointers. for (keys) |sk| { const want = first_value_at(full[i].pairs, sk.path, 0) orelse bson.Value.null; const got = first_value_at(topk[i].pairs, sk.path, 0) orelse bson.Value.null; testing.expectEqual(std.math.Order.eq, bson.compare(want, got)) catch |e| { std.debug.print("k={d} pos={d} key='{s}' diverged from the full sort\n", .{ k, i, sk.path, }); return e; }; } } } } } test "projection inclusion and exclusion" { const d = doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 1 } }, .{ .key = "a", .value = .{ .int32 = 1 } }, .{ .key = "b", .value = .{ .int32 = 2 } }, .{ .key = "nested", .value = .{ .doc = &.{ .{ .key = "x", .value = .{ .int32 = 3 } }, .{ .key = "y", .value = .{ .int32 = 4 } }, } } }, }); var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); var out: std.ArrayListUnmanaged(bson.Pair) = .empty; defer out.deinit(arena.allocator()); try project(arena.allocator(), &d, &doc_of(&.{ .{ .key = "a", .value = .{ .int32 = 1 } }, .{ .key = "nested.x", .value = .{ .int32 = 1 } }, }), &out); try testing.expectEqual(@as(usize, 3), out.items.len); try testing.expect(bson.get_pair(out.items, "a") != null); try testing.expect(bson.get_pair(out.items, "b") == null); const nx = bson.get_pair(out.items, "nested").?; try testing.expectEqual(@as(usize, 1), nx.doc.len); try testing.expectEqualStrings("x", nx.doc[0].key); // {_id: 0} alone: everything except _id. out.clearRetainingCapacity(); try project(arena.allocator(), &d, &doc_of(&.{.{ .key = "_id", .value = .{ .int32 = 0 } }}), &out); try testing.expect(bson.get_pair(out.items, "_id") == null); try testing.expect(bson.get_pair(out.items, "a") != null); try testing.expect(bson.get_pair(out.items, "b") != null); // Mixed inclusion with _id: 0: only listed fields, no _id. out.clearRetainingCapacity(); try project(arena.allocator(), &d, &doc_of(&.{ .{ .key = "_id", .value = .{ .int32 = 0 } }, .{ .key = "a", .value = .{ .int32 = 1 } }, }), &out); try testing.expect(bson.get_pair(out.items, "_id") == null); try testing.expect(bson.get_pair(out.items, "a") != null); try testing.expect(bson.get_pair(out.items, "b") == null); out.clearRetainingCapacity(); try project(arena.allocator(), &d, &doc_of(&.{ .{ .key = "b", .value = .{ .int32 = 0 } }, .{ .key = "nested.x", .value = .{ .int32 = 0 } }, }), &out); try testing.expect(bson.get_pair(out.items, "a") != null); try testing.expect(bson.get_pair(out.items, "b") == null); const n = bson.get_pair(out.items, "nested").?; try testing.expectEqual(@as(usize, 1), n.doc.len); try testing.expectEqualStrings("y", n.doc[0].key); } test "and/or filters" { const d = doc_of(&.{ .{ .key = "a", .value = .{ .int32 = 1 } }, .{ .key = "b", .value = .{ .int32 = 2 } } }); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "$and", .value = .{ .array = &.{ .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} }, .{ .doc = &.{.{ .key = "b", .value = .{ .int32 = 2 } }} }, } } }}), &d)); try testing.expect(try matches(testing.allocator, &doc_of(&.{.{ .key = "$or", .value = .{ .array = &.{ .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 99 } }} }, .{ .doc = &.{.{ .key = "b", .value = .{ .int32 = 2 } }} }, } } }}), &d)); try testing.expect(!try matches(testing.allocator, &doc_of(&.{.{ .key = "$or", .value = .{ .array = &.{ .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 99 } }} }, .{ .doc = &.{.{ .key = "b", .value = .{ .int32 = 99 } }} }, } } }}), &d)); } test "byte matcher agrees with the tree matcher on a corpus" { // The scan path matches stored documents as canonical BSON bytes, // skipping fields by length; the tree path walks materialized pairs. // They must agree exactly, so the byte matcher is checked against the // existing matcher over a corpus that exercises scalars, operators, // dot paths, multikey arrays, nested docs, $and/$or and missing fields. const gpa = testing.allocator; var prng = std.Random.DefaultPrng.init(0xB17E_7E); const rand = prng.random(); const n = 200; var bytes_list: std.ArrayListUnmanaged([]u8) = .empty; defer { for (bytes_list.items) |b| gpa.free(b); bytes_list.deinit(gpa); } for (0..n) |_| { const a = rand.intRangeAtMost(i32, 0, 10); var pairs: [4]bson.Pair = undefined; var np: usize = 0; pairs[np] = .{ .key = "a", .value = .{ .int32 = a } }; np += 1; pairs[np] = .{ .key = "b", .value = .{ .string = if (rand.boolean()) "x" else "y" } }; np += 1; if (rand.boolean()) { pairs[np] = .{ .key = "tags", .value = .{ .array = &.{ .{ .string = "m" }, .{ .string = "n" } } } }; np += 1; } if (rand.boolean()) { pairs[np] = .{ .key = "d", .value = .{ .doc = &.{.{ .key = "e", .value = .{ .int32 = a } }} } }; np += 1; } var out: std.ArrayListUnmanaged(u8) = .empty; defer out.deinit(gpa); try bson.write_doc(pairs[0..np], gpa, &out); try bytes_list.append(gpa, try gpa.dupe(u8, out.items)); } for (0..500) |case| { const a = rand.intRangeAtMost(i32, 0, 12); const s = if (rand.boolean()) "x" else "m"; // Values are copied into a per-iteration arena so nested literals // cannot dangle. var farena = std.heap.ArenaAllocator.init(gpa); defer farena.deinit(); const fa = farena.allocator(); var f_pairs: std.ArrayListUnmanaged(bson.Pair) = .empty; defer f_pairs.deinit(fa); switch (rand.intRangeAtMost(u8, 0, 9)) { 0 => try f_pairs.append(fa, .{ .key = "a", .value = .{ .int32 = a } }), 1 => { const v = try bson.copy_value(fa, .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = a } }} }); try f_pairs.append(fa, .{ .key = "a", .value = v }); }, 2 => try f_pairs.append(fa, .{ .key = "b", .value = .{ .string = s } }), 3 => try f_pairs.append(fa, .{ .key = "tags", .value = .{ .string = s } }), 4 => try f_pairs.append(fa, .{ .key = "d.e", .value = .{ .int32 = a } }), 5 => { const v = try bson.copy_value(fa, .{ .doc = &.{.{ .key = "$in", .value = .{ .array = &.{ .{ .int32 = a }, .{ .int32 = a + 1 } } } }} }); try f_pairs.append(fa, .{ .key = "a", .value = v }); }, 6 => { const v = try bson.copy_value(fa, .{ .array = &.{ .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = a } }} }, .{ .doc = &.{.{ .key = "b", .value = .{ .string = s } }} }, } }); try f_pairs.append(fa, .{ .key = "$or", .value = v }); }, 7 => { const v = try bson.copy_value(fa, .{ .doc = &.{.{ .key = "$exists", .value = .{ .bool = rand.boolean() } }} }); try f_pairs.append(fa, .{ .key = "tags", .value = v }); }, 8 => { const v = try bson.copy_value(fa, .{ .doc = &.{ .{ .key = "$gte", .value = .{ .int32 = a } }, .{ .key = "$lt", .value = .{ .int32 = a + 3 } }, } }); try f_pairs.append(fa, .{ .key = "a", .value = v }); }, else => try f_pairs.append(fa, .{ .key = "missing", .value = .{ .int32 = a } }), } const filter_doc = bson.Document{ .arena = undefined, .pairs = f_pairs.items }; for (bytes_list.items) |bytes| { var doc = try bson.Document.parse(gpa, bytes); defer doc.deinit(); const tree = try matches(gpa, &filter_doc, &doc); const byt = try matches_bytes(gpa, f_pairs.items, bytes); if (tree != byt) { std.debug.print("case {d}: filter mismatch: tree={} bytes={}\n", .{ case, tree, byt, }); return error.ByteMatcherMismatch; } } } } /// Public single-value operator matcher, used by $pull and $elemMatch. pub fn value_matches_operator( gpa: std.mem.Allocator, op: []const u8, value: bson.Value, actual: bson.Value, ) QueryError!bool { var single: [1]bson.Value = .{actual}; return match_operator(gpa, parse_op(op), value, single[0..], ""); }