M3: design review of the index types, and the partialFilterExpression refusal #10
26
PLAN.md
26
PLAN.md
@@ -1123,6 +1123,32 @@ has to be its own commit with its own re-recorded scorecard.
|
|||||||
agrees. Worth one commit, and it needs its own measurements first: the
|
agrees. Worth one commit, and it needs its own measurements first: the
|
||||||
padding case (`y.3.b` past the end) is a *legal* creation on mongod, so
|
padding case (`y.3.b` past the end) is a *legal* creation on mongod, so
|
||||||
the fix is not "refuse a non-document element".
|
the fix is not "refuse a non-document element".
|
||||||
|
- **`partialFilterExpression` was accepted and ignored**, and that made
|
||||||
|
`unique` mean the wrong thing. An index built over every document instead of
|
||||||
|
the filtered subset still answers reads correctly -- it holds a superset,
|
||||||
|
never a subset -- but a *unique* one then rejects inserts mongod accepts:
|
||||||
|
|
||||||
|
```js
|
||||||
|
createIndex({a: 1}, {unique: true, partialFilterExpression: {t: true}})
|
||||||
|
insertMany([{a: 1, t: false}, {a: 1, t: false}])
|
||||||
|
// mongod: accepted, neither document is in the index
|
||||||
|
// ours: E11000 duplicate key error
|
||||||
|
```
|
||||||
|
|
||||||
|
Unique-within-a-subset is the whole point of the option, so every use of it
|
||||||
|
was a legal insert refused. Refused at creation now, on the same judgement
|
||||||
|
`cmd_update` already makes about an update spec's `sort`. Nothing in the
|
||||||
|
repository covered this row: the pinned suite is crud and aggregate, and
|
||||||
|
neither `e2e5.js` nor `e2e6.js` writes a partial or hashed spec. Hashed, by
|
||||||
|
contrast, was honestly missing -- refused, with the wrong code (2 where
|
||||||
|
mongod says 67) but the right answer.
|
||||||
|
|
||||||
|
`docs/M3_INDEX_TYPES_DESIGN_REVIEW.md` measures both features' rules and
|
||||||
|
argues the planner rule that decides the design: a partial index may only
|
||||||
|
answer a query whose predicates *imply* its filter, so until that test
|
||||||
|
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.
|
||||||
|
|
||||||
- **M3's second corpus is `tests/spec/operators/`.** The eight operators PLAN
|
- **M3's second corpus is `tests/spec/operators/`.** The eight operators PLAN
|
||||||
§3 names all answered `bad update` with code 2, one message for every
|
§3 names all answered `bad update` with code 2, one message for every
|
||||||
question — and `$push`'s `$slice`, `$position` and `$sort` were parsed,
|
question — and `$push`'s `$slice`, `$position` and `$sort` were parsed,
|
||||||
|
|||||||
@@ -934,6 +934,35 @@ fn cmd_create_indexes(ctx: *Context, msg: *wire.Message, reply: *wire.Reply) !vo
|
|||||||
if (name == .string and std.mem.eql(u8, name.string, "_id_")) {
|
if (name == .string and std.mem.eql(u8, name.string, "_id_")) {
|
||||||
return bad_value(reply, "cannot create index with name '_id_'");
|
return bad_value(reply, "cannot create index with name '_id_'");
|
||||||
}
|
}
|
||||||
|
// Accepted and ignored until this commit, which is the worst of the
|
||||||
|
// three possible answers -- the same judgement `cmd_update` already
|
||||||
|
// makes about an update spec's `sort`.
|
||||||
|
//
|
||||||
|
// An index built over every document instead of the filtered subset
|
||||||
|
// still answers reads correctly: it holds a superset, never a subset.
|
||||||
|
// `unique` is where that stops being true. Measured on mongod 8.3.7:
|
||||||
|
//
|
||||||
|
// createIndex({a: 1}, {unique: true, partialFilterExpression: {t: true}})
|
||||||
|
// insertMany([{a: 1, t: false}, {a: 1, t: false}])
|
||||||
|
//
|
||||||
|
// mongod accepts both -- neither document is in the index, so neither
|
||||||
|
// collides -- and this server answered E11000. Unique-within-a-subset
|
||||||
|
// is the whole point of the option, so every use of it was a legal
|
||||||
|
// insert refused. The other two answers available were to keep doing
|
||||||
|
// that, or to echo the option back from `listIndexes` while not
|
||||||
|
// honouring it, which is a larger lie than saying no.
|
||||||
|
//
|
||||||
|
// See docs/M3_INDEX_TYPES_DESIGN_REVIEW.md. The implementation is the
|
||||||
|
// rest of M3's last row; this is what stands in until then.
|
||||||
|
if (bson.get_pair(spec, "partialFilterExpression") != null) {
|
||||||
|
return reply.put_error(
|
||||||
|
@intFromEnum(ErrorCode.cannot_create_index),
|
||||||
|
"CannotCreateIndex",
|
||||||
|
"partialFilterExpression is not implemented by this server: it would be " ++
|
||||||
|
"accepted and ignored, and a unique index would then be enforced over " ++
|
||||||
|
"documents the filter excludes",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const spec_doc = bson.Document{ .arena = undefined, .pairs = spec };
|
const spec_doc = bson.Document{ .arena = undefined, .pairs = spec };
|
||||||
_ = ctx.engine.create_index(db_name, coll_name, &spec_doc) catch |err| switch (err) {
|
_ = ctx.engine.create_index(db_name, coll_name, &spec_doc) catch |err| switch (err) {
|
||||||
@@ -5655,6 +5684,23 @@ test "TTL index round-trips through createIndexes/listIndexes; bad specs give 67
|
|||||||
.{ .key = "key", .value = .{ .doc = &.{.{ .key = "_id", .value = .{ .int32 = 1 } }} } },
|
.{ .key = "key", .value = .{ .doc = &.{.{ .key = "_id", .value = .{ .int32 = 1 } }} } },
|
||||||
.{ .key = "expireAfterSeconds", .value = .{ .int32 = 60 } },
|
.{ .key = "expireAfterSeconds", .value = .{ .int32 = 60 } },
|
||||||
} } },
|
} } },
|
||||||
|
// Same rule, different option: a partial filter accepted and ignored
|
||||||
|
// would enforce `unique` over documents the filter excludes.
|
||||||
|
.{ .code = 67, .spec = .{ .doc = &.{
|
||||||
|
.{ .key = "key", .value = .{ .doc = &.{.{ .key = "a", .value = .{ .int32 = 1 } }} } },
|
||||||
|
.{ .key = "partialFilterExpression", .value = .{ .doc = &.{
|
||||||
|
.{ .key = "t", .value = .{ .bool = true } },
|
||||||
|
} } },
|
||||||
|
} } },
|
||||||
|
// And with `unique`, which is the combination that made it a wrong
|
||||||
|
// answer rather than only a missing one.
|
||||||
|
.{ .code = 67, .spec = .{ .doc = &.{
|
||||||
|
.{ .key = "key", .value = .{ .doc = &.{.{ .key = "b", .value = .{ .int32 = 1 } }} } },
|
||||||
|
.{ .key = "unique", .value = .{ .bool = true } },
|
||||||
|
.{ .key = "partialFilterExpression", .value = .{ .doc = &.{
|
||||||
|
.{ .key = "t", .value = .{ .bool = true } },
|
||||||
|
} } },
|
||||||
|
} } },
|
||||||
};
|
};
|
||||||
for (bad) |case| {
|
for (bad) |case| {
|
||||||
var ctx = tdb.ctx(io);
|
var ctx = tdb.ctx(io);
|
||||||
@@ -5668,6 +5714,10 @@ test "TTL index round-trips through createIndexes/listIndexes; bad specs give 67
|
|||||||
try dispatch(&ctx, &msg, &reply);
|
try dispatch(&ctx, &msg, &reply);
|
||||||
try testing.expectEqual(case.code, bson.get_pair(reply.pairs.items, "code").?.int32);
|
try testing.expectEqual(case.code, bson.get_pair(reply.pairs.items, "code").?.int32);
|
||||||
}
|
}
|
||||||
|
// Mutation check for the two `partialFilterExpression` rows: delete the
|
||||||
|
// guard in `cmd_create_indexes` and both go green on the code -- and the
|
||||||
|
// second one's index then refuses `{a: 1, t: false}` twice, which mongod
|
||||||
|
// accepts because neither document is in the index at all.
|
||||||
// Nothing partial was registered by the rejected specs.
|
// Nothing partial was registered by the rejected specs.
|
||||||
try testing.expectEqual(@as(usize, 1), tdb.engine.get_collection("test", "sessions").?.indexes.items.len);
|
try testing.expectEqual(@as(usize, 1), tdb.engine.get_collection("test", "sessions").?.indexes.items.len);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user