commands: createIndexes/listIndexes/dropIndexes + planner wiring

Adds the three driver commands, parameterized E11000 messages (engine
dup_index carries the index name into writeErrors), the scan_matching
planner wiring (_id fast path → index plan → scan) and the first-$match
aggregate pushdown. The equivalence test (mixed-type corpus x 27 filters,
non-sparse and sparse indexes) drove out three real bugs: the two-bound
range under-approximation on multikey indexes (fall back to scan), a
dangling single-value option array in the planner, and update-time
unique violations now reporting writeErrors instead of corrupting state.
This commit is contained in:
2026-08-02 12:38:25 +03:00
parent a733fc1993
commit 7f2f7c6977
3 changed files with 750 additions and 34 deletions

View File

@@ -677,6 +677,14 @@ fn evaluate_index(gpa: std.mem.Allocator, ix: *const Index, clauses: []const Cla
}
if (run == 0 and lo == null and hi == null) return null;
// A two-sided range on a multikey index can under-approximate: a doc
// like {a: [1, 2]} satisfies {a: {$gt: 5, $lt: 25}} with the array for
// the lower bound (rank 5 > 5) and an element for the upper (1 < 25),
// so no single entry lies inside (5, 25) — the range scan would miss
// it. One-sided ranges are safe: whichever candidate satisfies the
// bound is itself an entry inside it. Equality/$in are unaffected.
if (ix.multikey and lo != null and hi != null) return null;
// A sparse index skips documents missing a field; {a: null} would
// otherwise miss them. Never use a sparse index for a null component.
if (ix.sparse) {
@@ -692,15 +700,13 @@ fn evaluate_index(gpa: std.mem.Allocator, ix: *const Index, clauses: []const Cla
if (hi) |v| if (v == .null) return null;
}
var opts: [max_index_keys][]const bson.Value = undefined;
var counts: [max_index_keys]usize = undefined;
for (0..run) |i| {
if (infos[i].eq) |v| {
opts[i] = &.{v};
} else opts[i] = infos[i].in_values.?;
counts[i] = if (infos[i].eq != null) 1 else infos[i].in_values.?.len;
}
var combos: u64 = 1;
for (0..run) |i| {
combos *= opts[i].len;
combos *= counts[i];
if (combos > max_combos) return null;
}
@@ -724,14 +730,19 @@ fn evaluate_index(gpa: std.mem.Allocator, ix: *const Index, clauses: []const Cla
while (true) {
const key = try gpa.alloc(bson.Value, run);
errdefer gpa.free(key);
for (0..run) |i| key[i] = opts[i][choice[i]];
// An eq component contributes its single value; an $in
// component the choice-th member. (Never materialize the
// single-value option as a temporary array: it would dangle.)
for (0..run) |i| {
key[i] = if (infos[i].eq) |v| v else infos[i].in_values.?[choice[i]];
}
try pl.lookup_keys.append(gpa, key);
var i: usize = run;
var carry = true;
while (carry and i > 0) {
i -= 1;
choice[i] += 1;
if (choice[i] < opts[i].len) carry = false else choice[i] = 0;
if (choice[i] < counts[i]) carry = false else choice[i] = 0;
}
if (carry) break;
}