diff --git a/PLAN.md b/PLAN.md index c0f3972..b9a8a2b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -467,7 +467,7 @@ answers, and the trade is only acceptable because the lie is removed first. | M1 | **Cursors + wire polish** | getMore / killCursors / batchSize; server-side cursor state with idle timeout; sessions plumbing (lsid accepted) as drivers send it; hello advertisement updates; **`moreToCome` on requests** (see the bug below); command-monitoring assertions in the spec runner | crud spec suite green; e2e green | | M2 | **The `aggregate` command surface** | `$out` and `$merge` (7 of the 13 failures), and refusing every pipeline construct the engine does not implement instead of answering `0` (amendment A6). The other 6 failures are blocked on M2.5, M4 and M8 — see `docs/M2_DESIGN_REVIEW.md` §7 | `aggregate-*.json`: 0 fail among the 7 reachable cases | | M2.5 | **The aggregation engine** | expression evaluator, per-stage document iterator, the accumulators, `$unwind`; `$lookup`/`$facet` explicitly out of the first cut (amendment A6) | a purpose-built stage corpus, every expectation measured against mongod | -| M3 | **Update operators + index types** | `distinct` (**done**); positional paths refused rather than destructive (**done**); `$`/`$[]`/`$[]` + `arrayFilters` (**done**); $setOnInsert, $addToSet, $mul, $min/$max, $pop, $pullAll, $currentDate + `$push`'s modifiers (**done**); pipeline-style updates (**done**); partial indexes (**done**); hashed indexes (**done**); then the implication test that lets a partial index serve a read | `tests/spec/positional/` 0 fail (51), `tests/spec/operators/` 0 fail (125) and `tests/spec/indexes/` 0 fail (42), all recorded from mongod — **green**; the named gate could not see any of the three, see below; remaining crud coverage; e2e3/e2e4 green | +| M3 | **Update operators + index types** | `distinct` (**done**); positional paths refused rather than destructive (**done**); `$`/`$[]`/`$[]` + `arrayFilters` (**done**); $setOnInsert, $addToSet, $mul, $min/$max, $pop, $pullAll, $currentDate + `$push`'s modifiers (**done**); pipeline-style updates (**done**); partial indexes (**done**); hashed indexes (**done**); the implication test that lets a partial index serve a read (**done**) — **the row is closed** | `tests/spec/positional/` 0 fail (51), `tests/spec/operators/` 0 fail (125) and `tests/spec/indexes/` 0 fail (48), all recorded from mongod — **green**; the named gate could not see any of the three, see below; remaining crud coverage; e2e3/e2e4 green | | M4 | **Sessions + transactions** | logical sessions, snapshot isolation on the mmap engine, write concern at commit | sessions + transactions spec suites green | | M5 | **Change streams** | change feed + resume tokens (likely log-seq based), getMore integration | change-streams spec suite green | | M6 | **Admin/ops commands** | dbStats, collStats, serverStatus, ping, buildInfo, listDatabases filters, dropDatabase durability (log it) | mongosh UX smoke; e2e green | @@ -1151,6 +1151,7 @@ has to be its own commit with its own re-recorded scorecard. to read from a partial index**: it holds a subset, so answering a query from it is only correct when the query implies the filter, and that implication test is the last step of the row. Too few documents is worse than no index. + (That step has since landed; see the implication entry below.) **Hashed indexes landed**; `hashed.json` is 18/18, so `tests/spec/indexes/` is 42/42. A hashed component stores a tag byte and a 64-bit hash of the @@ -1181,7 +1182,8 @@ has to be its own commit with its own re-recorded scorecard. cannot tell apart from a plain subdocument — so the check walks the path rather than counting what it yields. - **`tests/spec/indexes/` is the gate**, recorded red at 3/39 across 42 cases. + **`tests/spec/indexes/` is the gate**, recorded red at 3/39 across 42 cases + and since grown to 48 by the implication work. A case there is a *sequence* -- create, insert, read, list -- because an index outlives a `deleteMany` and every case is about which indexes exist. Recording it corrected two of the review's own guesses: `$in` in a partial @@ -1195,6 +1197,33 @@ has to be its own commit with its own re-recorded scorecard. exists the safe rule is to maintain the index and never read from it. Too few documents is the one failure worse than no index at all. +- **The implication test closes M3's row, and the two gates see opposite + halves of it.** A partial index may answer a query that cannot match a + document its filter left out. Two routes: the query pins a value at the + filter's path and the *real matcher* is run against a stand-in document + holding it (sound because every operator the partial grammar admits is + existential, so more values at a path can only satisfy it more easily), or + the query's bounds are at least as tight as the filter's. An array value and + a null value are refused rather than approximated — `{a: [1, 2]}` matches + `{a: [[1, 2], 3]}`, whose values at `a` do not include 1 or 2, and + `{a: null}` matches a document with no `a` at all, which has no values there + rather than more of them. + + Soundness is judged against **this server's** matcher, not mongod's: + `query.matches_bytes` decides the index's contents in `build_entries` and + re-filters every candidate. Where this server's comparison differs from + mongod's — the type-bracketing item below — both halves are wrong together, + which is a matching bug and not a lost document. + + Measured with two mutations, and worth keeping because the answer is not + symmetric. Forcing the test to `true` takes `partial.json` to 23/7 and every + failure reads "expected N, got N-1" — the exact shape of the bug. Forcing it + to `false`, which is the behaviour it replaced, leaves the corpus **30/30**: + no client can observe *that* an index was used, only that an answer went + missing. So the corpus guards soundness and the unit test on `plan()` is the + only thing that can see the feature work at all. A read path with no + `explain` has no third option. + - **What the index corpus caught that was not about indexes: `{a: null}` did not match a missing field.** `find({a: null})` has to match a document with no `a` as well as one holding an explicit null. This server matched only the diff --git a/docs/M3_INDEX_TYPES_DESIGN_REVIEW.md b/docs/M3_INDEX_TYPES_DESIGN_REVIEW.md index 32e0783..270bc4a 100644 --- a/docs/M3_INDEX_TYPES_DESIGN_REVIEW.md +++ b/docs/M3_INDEX_TYPES_DESIGN_REVIEW.md @@ -154,7 +154,8 @@ list used for its own format change and it holds here for the same reason. ## Outcome -Steps 1–4 landed in that order. `tests/spec/indexes/` is 42/42. +All five steps landed in that order, which closes M3's row. +`tests/spec/indexes/` is 48/48. Three things this review got wrong, kept here because the point of writing it before the code was to find out which parts would not survive contact: @@ -175,8 +176,22 @@ And one thing the corpus found that this review had no reason to look for: `find({a: null})` did not match a document with no `a`, index or no index. See PLAN §6. -Step 5, the implication test, is still open. Until it exists a partial index -is maintained, enforces `unique`, and is never read from. +And one thing §4 asserted without saying how: the implication test itself. It +turned out not to need a comparison of its own. Every operator +`check_partial_filter` admits is *existential* — "some value at this path +satisfies it" — so when the query pins a value, running the real matcher +against a stand-in document holding it settles `$eq`, `$in`, `$type`, +`$exists` and the bounds together. Only two shapes escape that argument (an +array value, whose elements a real document need not have; and null, which +also matches a document with no such field), and only ranges need reasoning +of their own. + +The unnoticed half was the gates. A corpus recorded from mongod can see an +implication test that says *yes* too readily — the answer loses documents — +but not one that never says yes at all, because no client can observe which +index a read used. Measured: forcing the test true takes `partial.json` to +23/7; forcing it false leaves it 30/30. The unit test on `plan()` is the only +thing that sees the feature work. ## 6. Not covered diff --git a/src/index.zig b/src/index.zig index dbc9e19..255513c 100644 --- a/src/index.zig +++ b/src/index.zig @@ -2659,6 +2659,264 @@ pub const Plan = struct { } }; +// --------------------------------------------------------------------------- +// Partial-index implication +// --------------------------------------------------------------------------- + +/// Whether every document this query matches is one the partial filter also +/// matches — the test that decides when a partial index may answer a read. +/// +/// A partial index holds a subset of the collection, so reading from it is +/// only correct when the query cannot match a document the filter left out. +/// Too few documents is the one failure worse than having no index at all, so +/// every answer here is one-sided: a `false` costs a scan, a `true` has to be +/// right. Everything below is written to be wrong in that direction. +/// +/// Soundness is judged against *this server's* matcher, not mongod's. Both +/// halves of the question are decided by the same code: `query.matches_bytes` +/// is what `build_entries` consults to decide the index's contents, and what +/// every candidate this plan yields is re-filtered through. Where this +/// server's comparison differs from mongod's — PLAN §6 records that the +/// comparison operators are not type-bracketed — both halves are wrong +/// together, which is a matching bug and not a lost document. +/// +/// Only the operators `check_partial_filter` admits can appear on the filter +/// side, and every one of them is *existential*: "some value at this path +/// satisfies it". That is what the two routes below rest on. +fn query_implies_filter( + gpa: std.mem.Allocator, + filter: []const bson.Pair, + clauses: []const Clause, +) query.QueryError!bool { + for (filter) |f| { + if (!try conjunct_implied(gpa, f, clauses)) return false; + } + return true; +} + +// Explicit rather than inferred: this and `query_implies_filter` call each +// other, which an inferred error set cannot resolve. +fn conjunct_implied( + gpa: std.mem.Allocator, + f: bson.Pair, + clauses: []const Clause, +) query.QueryError!bool { + if (f.key.len > 0 and f.key[0] == '$') { + const branches = switch (f.value) { + .array => |a| a, + else => return false, + }; + if (std.mem.eql(u8, f.key, "$and")) { + for (branches) |b| { + const sub = switch (b) { + .doc => |d| d, + else => return false, + }; + if (!try query_implies_filter(gpa, sub, clauses)) return false; + } + return true; + } + if (std.mem.eql(u8, f.key, "$or")) { + // One implied branch is enough. Implying none is not proof that + // the disjunction fails -- a query can imply `{$or: [A, B]}` + // without implying either -- so this is sufficient and not + // necessary, which is the side to be wrong on. + for (branches) |b| { + const sub = switch (b) { + .doc => |d| d, + else => continue, + }; + if (try query_implies_filter(gpa, sub, clauses)) return true; + } + return false; + } + return false; + } + + // The query's constraint on this path, accumulated exactly the way + // `evaluate_index` accumulates it. Whatever survives is a *necessary* + // condition of matching the query: an operator this does not understand + // clears the info rather than weakening it. + var qi = CompInfo{}; + for (clauses) |cl| { + if (std.mem.eql(u8, cl.path, f.key)) analyze_clause(cl.value, &qi); + } + if (try pinned_values_imply(gpa, f.key, f.value, qi)) return true; + return bounds_imply(f.value, qi); +} + +/// Route one: the query pins the path to a known value, or to one of a known +/// set, and each of them satisfies the filter. +fn pinned_values_imply( + gpa: std.mem.Allocator, + path: []const u8, + fv: bson.Value, + qi: CompInfo, +) query.QueryError!bool { + if (qi.eq) |v| return value_implies(gpa, path, v, fv); + if (qi.in_values) |list| { + // `{$in: []}` matches nothing and so implies everything, but an index + // read of a query with no answers is not worth a special case. + if (list.len == 0) return false; + for (list) |m| { + if (!try value_implies(gpa, path, m, fv)) return false; + } + return true; + } + return false; +} + +/// Whether a query pinning `v` at `path` implies the filter predicate `fv` +/// there. +/// +/// Answered by running the real matcher against a stand-in document holding +/// exactly `v` at `path`, rather than by reimplementing eight operators +/// against a comparison that would then have two definitions. It is sound +/// because the filter's operators are existential, so a document with *more* +/// values at the path satisfies them at least as easily -- and every document +/// the query matches has `v` among its values there. +/// +/// Two shapes break that argument and are refused rather than approximated: +/// +/// - **`v` is an array.** `{a: [1, 2]}` matches `{a: [[1, 2], 3]}`, whose +/// values at `a` are the outer array, `[1, 2]` and `3` -- only one level +/// is expanded, so 1 and 2 are not among them and the real document's +/// value set is not a superset of the stand-in's. +/// - **`v` is null.** `{a: null}` also matches a document with no `a` at +/// all, which has *no* values at the path rather than more of them. The +/// stand-in alone would report `{a: {$exists: true}}` as implied, so an +/// empty document is tested too and both have to agree. +fn value_implies( + gpa: std.mem.Allocator, + path: []const u8, + v: bson.Value, + fv: bson.Value, +) query.QueryError!bool { + if (v == .array) return false; + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + const stand_in = (try nest_value(a, path, v)) orelse return false; + const pred = [_]bson.Pair{.{ .key = path, .value = fv }}; + const f_doc = bson.Document{ .arena = undefined, .pairs = &pred }; + if (!try query.matches(a, &f_doc, &stand_in)) return false; + if (v == .null) { + const empty = bson.Document{ .arena = undefined, .pairs = &.{} }; + if (!try query.matches(a, &f_doc, &empty)) return false; + } + return true; +} + +/// The deepest path a stand-in document is built for. Past it the answer is +/// "cannot tell", which costs a scan. +const max_path_segments = 16; + +/// A document holding `v` at a dotted `path`. Null when a segment is numeric: +/// `a.0` may address an array element, and a document with a literal "0" +/// field is a different question than the one being asked. +fn nest_value( + a: std.mem.Allocator, + path: []const u8, + v: bson.Value, +) std.mem.Allocator.Error!?bson.Document { + var segs: [max_path_segments][]const u8 = undefined; + var n: usize = 0; + var it = std.mem.splitScalar(u8, path, '.'); + while (it.next()) |s| { + if (s.len == 0 or n == segs.len) return null; + if (std.fmt.parseInt(usize, s, 10)) |_| return null else |_| {} + segs[n] = s; + n += 1; + } + if (n == 0) return null; + var value = v; + var i = n; + while (i > 1) { + i -= 1; + const inner = try a.alloc(bson.Pair, 1); + inner[0] = .{ .key = segs[i], .value = value }; + value = .{ .doc = inner }; + } + const outer = try a.alloc(bson.Pair, 1); + outer[0] = .{ .key = segs[0], .value = value }; + return .{ .arena = undefined, .pairs = outer }; +} + +/// Route two: the query's bounds on a path are at least as tight as the +/// filter's. The only route that needs neither side to name a document -- +/// `{a: {$gt: 5}}` implies `{a: {$gt: 0}}` with no value in common. +/// +/// Restricted to the operators whose implication *is* an order comparison. +/// Anything else in the filter predicate leaves this to route one. +fn bounds_imply(fv: bson.Value, qi: CompInfo) bool { + const ops = switch (fv) { + .doc => |d| d, + else => return false, + }; + if (!query.all_operator_keys(ops)) return false; + + var lo: ?bson.Value = null; + var lo_incl = false; + var hi: ?bson.Value = null; + var hi_incl = false; + var wants_exists = false; + for (ops) |op| { + if (std.mem.eql(u8, op.key, "$gt")) { + lo = op.value; + lo_incl = false; + } else if (std.mem.eql(u8, op.key, "$gte")) { + lo = op.value; + lo_incl = true; + } else if (std.mem.eql(u8, op.key, "$lt")) { + hi = op.value; + hi_incl = false; + } else if (std.mem.eql(u8, op.key, "$lte")) { + hi = op.value; + hi_incl = true; + } else if (std.mem.eql(u8, op.key, "$exists")) { + // `$exists: false` is anti-monotone -- more values at a path make + // it *less* true -- so none of the reasoning here applies to it. + if (op.value != .bool or !op.value.bool) return false; + wants_exists = true; + } else { + return false; + } + } + if (lo == null and hi == null and !wants_exists) return false; + + if (lo) |f| { + const q = qi.lo orelse return false; + if (!bound_implies(q, qi.lo_incl, f, lo_incl, .gt)) return false; + } + if (hi) |f| { + const q = qi.hi orelse return false; + if (!bound_implies(q, qi.hi_incl, f, hi_incl, .lt)) return false; + } + // A bound predicate never matches a document with no value at the path: + // the comparison loop has nothing to run over. So any bound at all on the + // query's side settles `$exists: true`. + if (wants_exists and qi.lo == null and qi.hi == null) return false; + return true; +} + +/// Whether a query bound is at least as tight as a filter bound on the same +/// side. `tighter` is the direction that narrows: `.gt` for a lower bound, +/// `.lt` for an upper one. +fn bound_implies( + q: bson.Value, + q_incl: bool, + f: bson.Value, + f_incl: bool, + tighter: std.math.Order, +) bool { + const o = bson.compare(q, f); + if (o == tighter) return true; + if (o != .eq) return false; + // The same endpoint: the query implies the filter unless it admits that + // endpoint where the filter excludes it. + return f_incl or !q_incl; +} + /// Pick the index (if any) that can generate a superset of the matching /// documents: the one covering the longest leading run of equality/$in /// predicates, optionally with a range on the next key. Returns null when @@ -2690,13 +2948,11 @@ pub fn plan( } } for (indexes) |ix| { - // A partial index holds a *subset* of the collection, so answering a - // query from it is only correct when the query's predicates imply its - // filter. That implication test does not exist yet, and reading from - // the index without it returns too few documents -- the one failure - // worse than having no index at all. So it is maintained, it enforces - // `unique`, and reads scan. PLAN §6. - if (ix.partial != null) continue; + // A partial index holds a *subset* of the collection, so it may only + // answer a query that cannot match a document its filter left out. + if (ix.partial) |f| { + if (!try query_implies_filter(gpa, f.pairs, clauses.items)) continue; + } var cand = (try evaluate_index(gpa, ix, clauses.items, sort)) orelse continue; if (best) |b| { if (plan_better(&cand, &b)) { @@ -4248,6 +4504,189 @@ test "the _id index plan covers equality, ranges and _id sort order" { } } +test "a partial index answers only a query that implies its filter" { + const gpa = testing.allocator; + + // Every row is a (partial filter, query) pair and whether the query is + // allowed to read the index. The two halves this table is really about + // are opposite failures: a `true` that should be false loses documents + // silently, a `false` that should be true only costs a scan. So the rows + // are weighted towards the first. + const Row = struct { + filter: []const bson.Pair, + query: []const bson.Pair, + implied: bool, + why: []const u8, + }; + const eq_true: []const bson.Pair = &.{.{ .key = "t", .value = .{ .bool = true } }}; + const gt_zero: []const bson.Pair = &.{ + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = 0 } }} } }, + }; + const exists: []const bson.Pair = &.{ + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$exists", .value = .{ .bool = true } }} } }, + }; + + const rows = [_]Row{ + // -- route one: the query pins a value ----------------------------- + .{ .why = "the same equality", .implied = true, .filter = eq_true, .query = &.{ + .{ .key = "a", .value = .{ .int32 = 1 } }, + .{ .key = "t", .value = .{ .bool = true } }, + } }, + .{ .why = "the opposite equality", .implied = false, .filter = eq_true, .query = &.{ + .{ .key = "a", .value = .{ .int32 = 1 } }, + .{ .key = "t", .value = .{ .bool = false } }, + } }, + .{ .why = "no predicate on the filter's path", .implied = false, .filter = eq_true, .query = &.{ + .{ .key = "a", .value = .{ .int32 = 1 } }, + } }, + .{ .why = "an equality inside the filter's range", .implied = true, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .int32 = 5 } }, + } }, + .{ .why = "an equality outside it", .implied = false, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .int32 = -5 } }, + } }, + .{ .why = "an $in wholly inside", .implied = true, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$in", .value = .{ .array = &.{ + .{ .int32 = 5 }, + .{ .int32 = 9 }, + } } }} } }, + } }, + .{ .why = "an $in straddling it", .implied = false, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$in", .value = .{ .array = &.{ + .{ .int32 = 5 }, + .{ .int32 = 0 }, + } } }} } }, + } }, + .{ .why = "an equality implies $exists", .implied = true, .filter = exists, .query = &.{ + .{ .key = "n", .value = .{ .int32 = 5 } }, + } }, + // `{n: null}` matches a document with no `n` at all, so it cannot + // imply that `n` exists. This is the row that makes the stand-in + // document insufficient on its own. + .{ .why = "equality to null does not imply $exists", .implied = false, .filter = exists, .query = &.{ + .{ .key = "n", .value = .null }, + } }, + // ...but it does imply an equality to null, and the index does hold + // those documents: `build_entries` runs the same matcher. + .{ .why = "equality to null implies equality to null", .implied = true, .query = &.{ + .{ .key = "n", .value = .null }, + }, .filter = &.{.{ .key = "n", .value = .null }} }, + // An array value cannot stand in for itself: `{a: [1, 2]}` matches + // `{a: [[1, 2], 3]}`, whose values at `a` do not include 1 or 2. + .{ .why = "an array equality is refused", .implied = false, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .array = &.{ .{ .int32 = 5 }, .{ .int32 = 9 } } } }, + } }, + + // -- route two: bounds --------------------------------------------- + .{ .why = "a tighter lower bound", .implied = true, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = 5 } }} } }, + } }, + .{ .why = "the identical bound", .implied = true, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = 0 } }} } }, + } }, + .{ .why = "a looser lower bound", .implied = false, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = -1 } }} } }, + } }, + // $gte 0 admits 0 where the filter's $gt 0 excludes it -- the whole + // of what inclusivity decides. + .{ .why = "the same endpoint, inclusive against exclusive", .implied = false, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$gte", .value = .{ .int32 = 0 } }} } }, + } }, + .{ .why = "a bound implies $exists", .implied = true, .filter = exists, .query = &.{ + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = 5 } }} } }, + } }, + .{ .why = "an upper bound does not imply a lower one", .implied = false, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$lt", .value = .{ .int32 = 5 } }} } }, + } }, + // An operator the analysis does not understand clears the info, so + // the tight bound beside it cannot be leaned on. + .{ .why = "an unusable operator beside a bound", .implied = false, .filter = gt_zero, .query = &.{ + .{ .key = "n", .value = .{ .doc = &.{ + .{ .key = "$gt", .value = .{ .int32 = 5 } }, + .{ .key = "$bogus", .value = .{ .int32 = 1 } }, + } } }, + } }, + + // -- the filter's own shape ---------------------------------------- + .{ .why = "both conjuncts implied", .implied = true, .query = &.{ + .{ .key = "t", .value = .{ .bool = true } }, + .{ .key = "n", .value = .{ .int32 = 5 } }, + }, .filter = &.{ + .{ .key = "t", .value = .{ .bool = true } }, + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = 0 } }} } }, + } }, + .{ .why = "one conjunct implied is not enough", .implied = false, .query = &.{ + .{ .key = "t", .value = .{ .bool = true } }, + }, .filter = &.{ + .{ .key = "t", .value = .{ .bool = true } }, + .{ .key = "n", .value = .{ .doc = &.{.{ .key = "$gt", .value = .{ .int32 = 0 } }} } }, + } }, + .{ .why = "one branch of the filter's $or", .implied = true, .query = &.{ + .{ .key = "t", .value = .{ .bool = true } }, + }, .filter = &.{.{ .key = "$or", .value = .{ .array = &.{ + .{ .doc = &.{.{ .key = "t", .value = .{ .bool = true } }} }, + .{ .doc = &.{.{ .key = "n", .value = .{ .int32 = 9 } }} }, + } } }} }, + .{ .why = "no branch of the filter's $or", .implied = false, .query = &.{ + .{ .key = "t", .value = .{ .bool = false } }, + }, .filter = &.{.{ .key = "$or", .value = .{ .array = &.{ + .{ .doc = &.{.{ .key = "t", .value = .{ .bool = true } }} }, + .{ .doc = &.{.{ .key = "n", .value = .{ .int32 = 9 } }} }, + } } }} }, + // The query's own $or is skipped by `flatten_clauses`, so its members + // are not predicates the query is known to enforce. + .{ .why = "a query $or supplies nothing", .implied = false, .filter = eq_true, .query = &.{ + .{ .key = "$or", .value = .{ .array = &.{ + .{ .doc = &.{.{ .key = "t", .value = .{ .bool = true } }} }, + } } }, + } }, + // ...but a query $and does: it is a conjunction like the top level. + .{ .why = "a query $and supplies its members", .implied = true, .filter = eq_true, .query = &.{ + .{ .key = "$and", .value = .{ .array = &.{ + .{ .doc = &.{.{ .key = "t", .value = .{ .bool = true } }} }, + } } }, + } }, + // A dotted path is nested into the stand-in document... + .{ .why = "a dotted filter path", .implied = true, .query = &.{ + .{ .key = "m.t", .value = .{ .bool = true } }, + }, .filter = &.{.{ .key = "m.t", .value = .{ .bool = true } }} }, + // ...unless a segment is numeric, where it might mean an array index. + .{ .why = "a numeric path segment is refused", .implied = false, .query = &.{ + .{ .key = "m.0", .value = .{ .bool = true } }, + }, .filter = &.{.{ .key = "m.0", .value = .{ .bool = true } }} }, + }; + + for (rows) |r| { + errdefer std.debug.print("row: {s}\n", .{r.why}); + var clauses: std.ArrayListUnmanaged(Clause) = .empty; + defer clauses.deinit(gpa); + try flatten_clauses(gpa, r.query, &clauses); + try testing.expectEqual(r.implied, try query_implies_filter(gpa, r.filter, clauses.items)); + } + + // And the whole point of the answer: a partial index the query implies is + // planned, one it does not is left alone. Mutation check: make + // `query_implies_filter` return true unconditionally and the second + // expectation here starts producing a plan -- which is the shape of a + // read that silently returns too few documents. + var ix = try simple_index(gpa, test_pager(), &.{"a"}, false, false); + defer ix.deinit(gpa); + try ix.set_partial(gpa, eq_true); + { + const f = [_]bson.Pair{ + .{ .key = "a", .value = .{ .int32 = 1 } }, + .{ .key = "t", .value = .{ .bool = true } }, + }; + var p = (try plan(gpa, null, &.{&ix}, &f, &.{})).?; + defer p.deinit(gpa); + try testing.expectEqual(@as(usize, 1), p.key_len()); + } + { + const f = [_]bson.Pair{.{ .key = "a", .value = .{ .int32 = 1 } }}; + try testing.expect((try plan(gpa, null, &.{&ix}, &f, &.{})) == null); + } +} + test "planner picks eq run, ranges, and bails on sparse null" { const gpa = testing.allocator; var ix = try simple_index(gpa, test_pager(), &.{ "a", "b" }, false, false); diff --git a/tests/spec/indexes/README.md b/tests/spec/indexes/README.md index dcd622b..69a5a53 100644 --- a/tests/spec/indexes/README.md +++ b/tests/spec/indexes/README.md @@ -50,7 +50,7 @@ halves have since been driven green: ``` hashed.json 18 pass 0 fail 0 skip -partial.json 24 pass 0 fail 0 skip +partial.json 30 pass 0 fail 0 skip ``` The three that passed at the start were the reads a partial index does not @@ -63,6 +63,24 @@ to match a document with no `a`, and this server matched only an explicit null — with or without an index. No other test in the repository asks, and the pinned crud+aggregate scorecard did not move when it was fixed. +## What this corpus cannot see + +Six of `partial.json`'s cases exist for the implication test — the rule that +lets a partial index answer a read — and it is worth being exact about what +they guard, because it is only half of it. + +A partial index holds a subset, so reading from one the query does not imply +returns **too few** documents, and that is an answer a result comparison +catches. It was measured: forcing the implication test to always say yes takes +this file to 23 pass / 7 fail, every failure reading "expected N, got N-1". + +The other direction is invisible here. Forcing it to always say *no* — the +behaviour before the test existed, where a partial index was maintained and +never read — leaves this file at **30 pass / 0 fail**. No client can observe +which index a read used, and this server has no `explain`. So the corpus +guards soundness and a unit test on `plan()` is the only thing that sees the +feature work at all. + ## What recording it settled Two of the review's own guesses were wrong, which is why it was recorded diff --git a/tests/spec/indexes/partial.json b/tests/spec/indexes/partial.json index 504c54d..45f993d 100644 --- a/tests/spec/indexes/partial.json +++ b/tests/spec/indexes/partial.json @@ -1 +1 @@ -{"description":"partial","schemaVersion":"1.4","createEntities":[{"client":{"id":"client0"}},{"database":{"id":"database0","client":"client0","databaseName":"index-corpus"}},{"collection":{"id":"collection0","database":"database0","collectionName":"coll"}}],"initialData":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}],"tests":[{"description":"a partial index is created and reports its filter","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a query the filter covers still answers every match","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$gte":{"$numberInt":"5"}}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a query the filter excludes still answers every match","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$numberInt":"1"}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a query straddling the filter still answers every match","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$gte":{"$numberInt":"1"}}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]},{"object":"collection0","name":"countDocuments","arguments":{"filter":{}},"expectResult":{"$numberInt":"4"}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a unique partial index accepts duplicates its filter excludes","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"unique":true,"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false}]},"expectResult":{"insertedCount":{"$numberInt":"2"},"insertedIds":{"0":{"$numberInt":"1"},"1":{"$numberInt":"2"}}}},{"object":"collection0","name":"find","arguments":{"filter":{},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false}]}]},{"description":"a unique partial index refuses duplicates its filter selects","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"unique":true,"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":true}]},"expectError":{"isError":true,"errorCode":{"$numberInt":"11000"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true}]}]},{"description":"a document leaving the filter frees the value it held","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"unique":true,"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true}]},"expectResult":{"insertedCount":{"$numberInt":"1"},"insertedIds":{"0":{"$numberInt":"1"}}}},{"object":"collection0","name":"updateOne","arguments":{"filter":{"_id":{"$numberInt":"1"}},"update":{"$set":{"t":false}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}},{"object":"collection0","name":"insertOne","arguments":{"document":{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":true}},"expectResult":{"insertedId":{"$numberInt":"2"}}},{"object":"collection0","name":"find","arguments":{"filter":{},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":false},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":true}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":false},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":true}]}]},{"description":"a document entering the filter takes the value it names","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"unique":true,"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":false}]},"expectResult":{"insertedCount":{"$numberInt":"2"},"insertedIds":{"0":{"$numberInt":"1"},"1":{"$numberInt":"2"}}}},{"object":"collection0","name":"updateOne","arguments":{"filter":{"_id":{"$numberInt":"2"}},"update":{"$set":{"t":true}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"11000"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":false}]}]},{"description":"a filter on $exists","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$exists":true}}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"a":{"$exists":true}}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter with two predicates","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}},"s":"x"}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}},"s":"x"}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter with an explicit $and","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"$and":[{"a":{"$gt":{"$numberInt":"0"}}}]}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"$and":[{"a":{"$gt":{"$numberInt":"0"}}}]}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter on a field the index does not name","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"s":"x"}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"s":"x"}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter using $regex is refused","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"s":{"$regex":"x"}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"67"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter using $ne is refused","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$ne":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"67"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter using $in is refused","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$in":[{"$numberInt":"1"},{"$numberInt":"2"}]}}},"expectResult":"a_1"}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter that is not a document is refused","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"$numberInt":"1"}},"expectError":{"isError":true,"errorCode":{"$numberInt":"14"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"an empty filter","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"sparse and a partial filter may not be combined","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"sparse":true,"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"67"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a partial filter with a TTL","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"expireAfterSeconds":{"$numberInt":"100"},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}},"expireAfterSeconds":{"$numberInt":"100"}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a partial index on a compound key","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"},"s":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"a_1_s_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"},"s":{"$numberInt":"1"}},"name":"a_1_s_1","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"two indexes on one key differing only in their filter","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"name":"one","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"one"},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"name":"two","partialFilterExpression":{"a":{"$gt":{"$numberInt":"4"}}}},"expectResult":"two"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"one","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"two","partialFilterExpression":{"a":{"$gt":{"$numberInt":"4"}}}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"the same index twice is idempotent","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"a_1"},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"the same name with a different filter conflicts","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"a_1"},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"4"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"86"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a partial index is dropped by name","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"name":"pa","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"pa"},{"object":"collection0","name":"dropIndex","arguments":{"name":"pa"},"expectResult":{}},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]}]} +{"description":"partial","schemaVersion":"1.4","createEntities":[{"client":{"id":"client0"}},{"database":{"id":"database0","client":"client0","databaseName":"index-corpus"}},{"collection":{"id":"collection0","database":"database0","collectionName":"coll"}}],"initialData":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}],"tests":[{"description":"a partial index is created and reports its filter","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a query the filter covers still answers every match","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$gte":{"$numberInt":"5"}}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a query the filter excludes still answers every match","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$numberInt":"1"}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a query straddling the filter still answers every match","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$gte":{"$numberInt":"1"}}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]},{"object":"collection0","name":"countDocuments","arguments":{"filter":{}},"expectResult":{"$numberInt":"4"}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a unique partial index accepts duplicates its filter excludes","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"unique":true,"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false}]},"expectResult":{"insertedCount":{"$numberInt":"2"},"insertedIds":{"0":{"$numberInt":"1"},"1":{"$numberInt":"2"}}}},{"object":"collection0","name":"find","arguments":{"filter":{},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false}]}]},{"description":"a unique partial index refuses duplicates its filter selects","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"unique":true,"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":true}]},"expectError":{"isError":true,"errorCode":{"$numberInt":"11000"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true}]}]},{"description":"a document leaving the filter frees the value it held","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"unique":true,"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true}]},"expectResult":{"insertedCount":{"$numberInt":"1"},"insertedIds":{"0":{"$numberInt":"1"}}}},{"object":"collection0","name":"updateOne","arguments":{"filter":{"_id":{"$numberInt":"1"}},"update":{"$set":{"t":false}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}},{"object":"collection0","name":"insertOne","arguments":{"document":{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":true}},"expectResult":{"insertedId":{"$numberInt":"2"}}},{"object":"collection0","name":"find","arguments":{"filter":{},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":false},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":true}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":false},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":true}]}]},{"description":"a document entering the filter takes the value it names","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"unique":true,"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":false}]},"expectResult":{"insertedCount":{"$numberInt":"2"},"insertedIds":{"0":{"$numberInt":"1"},"1":{"$numberInt":"2"}}}},{"object":"collection0","name":"updateOne","arguments":{"filter":{"_id":{"$numberInt":"2"}},"update":{"$set":{"t":true}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"11000"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":false}]}]},{"description":"a query that leaves the filter's field out still sees past it","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"3"},"a":{"$numberInt":"2"},"t":true}]}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$numberInt":"1"}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false}]},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$numberInt":"1"},"t":true},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":true}]},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$numberInt":"1"},"t":false},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"3"},"a":{"$numberInt":"2"},"t":true}]}]},{"description":"an $in straddling the filter still answers every match","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gte":{"$numberInt":"5"}}}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$in":[{"$numberInt":"1"},{"$numberInt":"5"}]}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$in":[{"$numberInt":"5"},{"$numberInt":"9"}]}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a query for null does not belong to an index that requires the field","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"}},{"_id":{"$numberInt":"2"}},{"_id":{"$numberInt":"3"},"a":null}]}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$exists":true}}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":null},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"2"}},{"_id":{"$numberInt":"3"},"a":null}]},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$numberInt":"1"}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"}},{"_id":{"$numberInt":"2"}},{"_id":{"$numberInt":"3"},"a":null}]}]},{"description":"an equality inside the filter's range, and one outside it","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"3"}}}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$numberInt":"5"}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$numberInt":"1"}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"}]},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$gt":{"$numberInt":"4"}}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$gt":{"$numberInt":"0"}}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a sort a partial index could serve","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"3"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"3"},"a":{"$numberInt":"2"},"t":true}]}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"t":true},"sort":{"a":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"3"},"a":{"$numberInt":"2"},"t":true},{"_id":{"$numberInt":"1"},"a":{"$numberInt":"3"},"t":true}]},{"object":"collection0","name":"find","arguments":{"filter":{},"sort":{"a":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"3"},"a":{"$numberInt":"2"},"t":true},{"_id":{"$numberInt":"1"},"a":{"$numberInt":"3"},"t":true}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"3"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"1"},"t":false},{"_id":{"$numberInt":"3"},"a":{"$numberInt":"2"},"t":true}]}]},{"description":"a unique partial index answers the read it constrains","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":false}]}},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"unique":true,"partialFilterExpression":{"t":true}},"expectResult":"a_1"},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$numberInt":"9"},"t":true},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true}]},{"object":"collection0","name":"find","arguments":{"filter":{"a":{"$numberInt":"9"}},"sort":{"_id":{"$numberInt":"1"}}},"expectResult":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":false}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"9"},"t":true},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"9"},"t":false}]}]},{"description":"a filter on $exists","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$exists":true}}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"a":{"$exists":true}}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter with two predicates","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}},"s":"x"}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}},"s":"x"}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter with an explicit $and","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"$and":[{"a":{"$gt":{"$numberInt":"0"}}}]}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"$and":[{"a":{"$gt":{"$numberInt":"0"}}}]}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter on a field the index does not name","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"s":"x"}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"s":"x"}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter using $regex is refused","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"s":{"$regex":"x"}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"67"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter using $ne is refused","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$ne":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"67"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter using $in is refused","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$in":[{"$numberInt":"1"},{"$numberInt":"2"}]}}},"expectResult":"a_1"}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a filter that is not a document is refused","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"$numberInt":"1"}},"expectError":{"isError":true,"errorCode":{"$numberInt":"14"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"an empty filter","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"sparse and a partial filter may not be combined","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"sparse":true,"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"67"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a partial filter with a TTL","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"expireAfterSeconds":{"$numberInt":"100"},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}},"expireAfterSeconds":{"$numberInt":"100"}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a partial index on a compound key","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"},"s":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"a_1_s_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"},"s":{"$numberInt":"1"}},"name":"a_1_s_1","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"two indexes on one key differing only in their filter","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"name":"one","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"one"},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"name":"two","partialFilterExpression":{"a":{"$gt":{"$numberInt":"4"}}}},"expectResult":"two"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"one","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"two","partialFilterExpression":{"a":{"$gt":{"$numberInt":"4"}}}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"the same index twice is idempotent","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"a_1"},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"a_1"},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"a":{"$numberInt":"1"}},"name":"a_1","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"the same name with a different filter conflicts","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"a_1"},{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"partialFilterExpression":{"a":{"$gt":{"$numberInt":"4"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"86"}}}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]},{"description":"a partial index is dropped by name","operations":[{"object":"collection0","name":"createIndex","arguments":{"keys":{"a":{"$numberInt":"1"}},"name":"pa","partialFilterExpression":{"a":{"$gt":{"$numberInt":"0"}}}},"expectResult":"pa"},{"object":"collection0","name":"dropIndex","arguments":{"name":"pa"},"expectResult":{}},{"object":"collection0","name":"listIndexes","arguments":{},"expectResult":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}]}],"outcome":[{"collectionName":"coll","databaseName":"index-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"s":"x"},{"_id":{"$numberInt":"2"},"a":{"$numberInt":"5"},"s":"y"},{"_id":{"$numberInt":"3"},"s":"z"},{"_id":{"$numberInt":"4"},"a":{"$numberInt":"5"},"s":"x"}]}]}]} diff --git a/tests/spec/indexes/sources/partial.json b/tests/spec/indexes/sources/partial.json index b1daade..52b4929 100644 --- a/tests/spec/indexes/sources/partial.json +++ b/tests/spec/indexes/sources/partial.json @@ -79,6 +79,85 @@ { "name": "updateOne", "arguments": { "filter": { "_id": 2 }, "update": { "$set": { "t": true } } } } ] }, + { + "_comment": [ + "The reads below exist for the implication test: a partial index may", + "only answer a query that cannot match a document its filter left", + "out. Each pairs a query that implies the filter with one that does", + "not and touches the same field, so an implication test that says yes", + "too readily loses the documents outside the filter -- which is a", + "wrong answer a result comparison can see, unlike the index being", + "used at all, which no client can observe." + ], + "description": "a query that leaves the filter's field out still sees past it", + "documents": [ + { "_id": 1, "a": 1, "t": true }, + { "_id": 2, "a": 1, "t": false }, + { "_id": 3, "a": 2, "t": true } + ], + "ops": [ + { "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "t": true } } }, + { "name": "find", "arguments": { "filter": { "a": 1 }, "sort": { "_id": 1 } } }, + { "name": "find", "arguments": { "filter": { "a": 1, "t": true }, "sort": { "_id": 1 } } }, + { "name": "find", "arguments": { "filter": { "a": 1, "t": false }, "sort": { "_id": 1 } } } + ] + }, + { + "description": "an $in straddling the filter still answers every match", + "ops": [ + { "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gte": 5 } } } }, + { "name": "find", "arguments": { "filter": { "a": { "$in": [1, 5] } }, "sort": { "_id": 1 } } }, + { "name": "find", "arguments": { "filter": { "a": { "$in": [5, 9] } }, "sort": { "_id": 1 } } } + ] + }, + { + "description": "a query for null does not belong to an index that requires the field", + "documents": [ + { "_id": 1, "a": 1 }, + { "_id": 2 }, + { "_id": 3, "a": null } + ], + "ops": [ + { "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$exists": true } } } }, + { "name": "find", "arguments": { "filter": { "a": null }, "sort": { "_id": 1 } } }, + { "name": "find", "arguments": { "filter": { "a": 1 }, "sort": { "_id": 1 } } } + ] + }, + { + "description": "an equality inside the filter's range, and one outside it", + "ops": [ + { "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gt": 3 } } } }, + { "name": "find", "arguments": { "filter": { "a": 5 }, "sort": { "_id": 1 } } }, + { "name": "find", "arguments": { "filter": { "a": 1 }, "sort": { "_id": 1 } } }, + { "name": "find", "arguments": { "filter": { "a": { "$gt": 4 } }, "sort": { "_id": 1 } } }, + { "name": "find", "arguments": { "filter": { "a": { "$gt": 0 } }, "sort": { "_id": 1 } } } + ] + }, + { + "description": "a sort a partial index could serve", + "documents": [ + { "_id": 1, "a": 3, "t": true }, + { "_id": 2, "a": 1, "t": false }, + { "_id": 3, "a": 2, "t": true } + ], + "ops": [ + { "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "t": true } } }, + { "name": "find", "arguments": { "filter": { "t": true }, "sort": { "a": 1 } } }, + { "name": "find", "arguments": { "filter": {}, "sort": { "a": 1 } } } + ] + }, + { + "description": "a unique partial index answers the read it constrains", + "documents": [ + { "_id": 1, "a": 9, "t": true }, + { "_id": 2, "a": 9, "t": false } + ], + "ops": [ + { "name": "createIndex", "arguments": { "keys": { "a": 1 }, "unique": true, "partialFilterExpression": { "t": true } } }, + { "name": "find", "arguments": { "filter": { "a": 9, "t": true }, "sort": { "_id": 1 } } }, + { "name": "find", "arguments": { "filter": { "a": 9 }, "sort": { "_id": 1 } } } + ] + }, { "description": "a filter on $exists", "ops": [