M3: design review of the index types, and the partialFilterExpression refusal #10
Reference in New Issue
Block a user
Delete Branch "m3-index-types"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
M3's last row, opened the way M2 and
arrayFilterswere opened: measurefirst, write the review, and land the refusal the measurement demands.
The two halves are not the same kind of gap
PLAN §3 names them together. They are not.
Hashed is honestly missing.
createIndex({a: "hashed"})is refused. Wrongcode -- 2 where mongod says 67, and it has four more specific ones besides --
but the right answer. The client is told no, and queries keep working because
there is simply no index.
Partial was accepted and ignored.
createIndexreported success, theindex was built over every document rather than the filtered subset, and
listIndexesdid not mention the option.The row that made it a wrong answer rather than a missing feature
An over-inclusive index still answers reads correctly -- it holds a superset,
never a subset -- so this was not the array-destroying class of bug. That is
worth saying plainly.
uniqueis where it stopped being harmless:A legal insert refused. Unique-within-a-subset -- unique email among
active accounts, unique external id among synced rows -- is the whole point of
the option, so every use of it hit this.
Nothing in the repository covered that row. The pinned suite is crud and
aggregate; neither
e2e5.jsnore2e6.jswrites a partial or hashed spec.What is in this PR
docs/M3_INDEX_TYPES_DESIGN_REVIEW.md-- both features' rules measuredagainst mongod 8.3.7: which predicates a partial filter may hold, why
sparseandpartialFilterExpressionmay not be combined (67), the fourhashed refusals and the one that fires at insert time rather than at
creation (16766), and the planner rule that decides the whole design --
Same for hashed, mirrored: equality predicates only, everything else scans.
The hash value itself is a private encoding choice -- nothing a client can
observe depends on it -- which is exactly why sorts may not use the tree.
The refusal.
partialFilterExpressionis now CannotCreateIndex (67) atcreation, on the same judgement
cmd_updatealready states about an updatespec's
sort: "ignoring the field would be the worst of the three possibleanswers." The two alternatives were to keep enforcing
uniqueover thewrong set, or to echo the option back from
listIndexeswhile nothonouring it, which is a larger lie than saying no.
Two rows added to the existing index-spec refusal test, with the mutation
that reddens them written beside them.
Verification
249/249 unit tests in ReleaseFast and ReleaseSafe, 83/83 fuzz, operators
125/0, positional 51/0, aggregation 70/0, pinned crud 228/63/196 unmoved, the
full e2e matrix including
e2e5/e2e6andcrash-fuzzgreen.Next, in the review's order
Record
tests/spec/indexes/; then partial indexes, maintained andunique-enforcing with the planner declining to read from them; then hashed,equality-only; then the implication test that lets a partial index serve a
read. Both need one catalog field, and
write_index_catalog's flags byte hasspare bits, so
catalog_versionstays 1 -- the same argument the free listused for its own format change.
M3's last row names them together. They are not the same kind of gap, and the measurement is what says so. **Hashed is honestly missing**: `createIndex({a: "hashed"})` is refused. Wrong code -- 2 where mongod says 67, and it has four more specific ones besides -- but the right answer, and queries keep working because there is simply no index. **Partial is accepted and ignored.** `createIndex` reports success, `listIndexes` does not mention `partialFilterExpression`, and the index is built over every document rather than the ones the filter selects. An over-inclusive index still answers reads correctly, which is worth saying plainly: it holds a superset, never a subset. `unique` is where that stops being true -- 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 -- a legal insert refused. Unique-within-a-subset is the whole point of the option, and every use of it is an insert this server rejects. Nothing in the repository covers that row: the pinned suite is crud and aggregate, and neither `e2e5.js` nor `e2e6.js` writes a partial or hashed spec. So this needs its own recorded corpus, like the positional operators and the update operators before it. The review measures both feature's rules -- which predicates a partial filter may hold, why `sparse` and `partialFilterExpression` may not be combined, the four hashed refusals and the one that fires at insert time rather than at creation -- and argues the planner rule that matters: 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. Recommended order: refuse `partialFilterExpression`, record the corpus, then implement partial, then hashed, then the implication test. Both need one catalog field each, and `write_index_catalog`'s flags byte has spare bits, so `catalog_version` stays 1 -- the same argument the free list used.`createIndex({a: 1}, {partialFilterExpression: ...})` answered success, built the index over every document, and left the option out of `listIndexes`. An over-inclusive index still answers reads correctly -- it holds a superset, never a subset -- so this was not the array-destroying class of bug. `unique` is where it stopped being harmless. Measured on mongod 8.3.7: 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, so neither collides ours: E11000 duplicate key error, dup key: {a: 1} A legal insert refused. Unique-within-a-subset -- unique email among active accounts, unique external id among synced rows -- is the whole point of the option, so every use of it hit this. Refused at creation, which is the same judgement `cmd_update` already makes about an update spec's `sort`: "ignoring the field would be the worst of the three possible answers." The two alternatives here were to keep enforcing `unique` over the wrong set, or to echo the option back from `listIndexes` while not honouring it, which is a larger lie than saying no. CannotCreateIndex (67), joining the five existing rows of the same test with the mutation that reddens them written beside it. The implementation is the rest of M3's last row and comes next, against a corpus that does not exist yet -- nothing in this repository covered the row above, in any suite. 249/249 unit tests, pinned crud corpus unmoved at 228/63/196.