The comment claimed moving `update.validate` below `scan_matching` would
redden it. Ran the mutation: it does not -- the call still sits above the
zero-match branch, so it still runs. What reddens it is deleting the
standalone call and leaving the check to `apply`, which runs once per matched
document and so never at all when nothing matched. A mutation note that has
not been run is worth less than no note.
Scorecard 204 -> 218 pass, 87 -> 73 fail: the fourteen `arrayFilters` cases
across five files, which is the whole of what the two implementation commits
were expected to move and nothing else. Positional corpus 51/51.
The three divergences measured on the way are written into PLAN §6 rather
than left in commit messages: `$` with two predicates on one array that no
element satisfies together, an array filter with a top-level `$and`/`$or`,
and a literal index into a scalar element -- the last being the one place the
positional walk and the plain indexed path now answer differently, which is
worth a commit of its own and needs its own measurements first.
The design review gets an outcome note, since two of its guesses were wrong
and the corpus is where that was settled.
The engine landed last commit with nothing feeding it: `$[<identifier>]` had
no filters to bind and `$` had no query to resolve against, so both refused
correctly and uselessly. This is the plumbing.
`arrayFilters` is read per update statement on `update` and once on
`findAndModify`, which is where each command carries it. Only the *shape* is
checked here -- an array, of documents, TypeMismatch (14) with mongod's own
field names for either -- because which identifier a filter names, whether it
is spelled legally and whether the update ever uses it all need the update's
paths, and those belong to `update.validate`.
`validate` is called before `scan_matching`, not inside the per-document
loop, and that placement is load-bearing in both directions: an array filter
the update never uses is refused (9) even when the query matches nothing at
all, and an identifier nothing binds is refused (2) before a single document
is read. Both measured; a test pins each, with the mutation that reddens it
named in the comment.
Positional corpus 23 -> 51 of 51, the gate green.
Pinned crud scorecard 204 -> 218 pass, 87 -> 73 fail: the whole arrayFilters
cluster, which until two commits ago was answering ok: 1 having replaced the
array with a document keyed by the path segment's literal text.
222/222 unit tests in ReleaseFast and ReleaseSafe, 83/83 fuzz.
`$[]`, `$[<identifier>]` and `$` stop being refused and start being walked.
A path with a positional segment names a *set* of concrete paths rather than
one -- `y.$[].b` on a two-element array names `y.0.b` and `y.1.b` -- so
`resolve` expands it against the document at hand and every operator then
walks paths it already knew how to walk. Nothing below `resolve` knows a
positional segment exists, which is why `$set`, `$inc`, `$unset`, `$push` and
`$pull` all get it at once.
The static half -- which identifier binds to which array filter, whether a
segment may sit in first position, whether a filter went unused -- depends
only on the update and the filters, so it runs once in `validate` before any
document is touched. The document-dependent half is resolution itself: an
absent or non-array path is a refusal there, because array updates address
what is there rather than creating it, unlike `$set` on a plain path.
Codes and messages measured on mongod 8.3.7, not recalled. Sixteen shapes
were run; the ones that changed what this commit does:
- `$[]` in first position answers the *array filter identifier* message,
not the `$` one -- mongod treats the two spellings as one check.
- an array filter may have several top-level fields as long as they all
name the same identifier: `{i.b: 3, i.c: 1}` is legal, `{i.b: 3, j.b: 1}`
is not. So the check is on the name, not on the count.
- an identifier is `[a-z][a-zA-Z0-9]*`: `aB2` yes, `Ab`, `a_b`, `1x` no.
- `$rename` refuses a positional path on *either* end, with its own message
for each, so it keeps the plain split rather than resolving.
- `$unset` of an element leaves a null in its place rather than shortening
the array -- the same answer `$unset: {"y.0": ""}` already gave.
- a scalar element with more path below it is PathNotViable (28), not a
field to create. Without that check the walk would hand `y.1.b` to
`set_path` and it would replace the `7` at `y.1` with `{b: 9}` -- a
smaller copy of the destruction this whole walk replaced.
One divergence, measured and deliberate: `{"y.b": 3, "y.c": 2}` matches
`[{b: 3, c: 1}, {b: 1, c: 2}]` without either element satisfying both, and
`$` then picks element 1 on mongod and element 0 here. mongod's answer is an
artefact of which predicate last wrote its match position; guessing at it
would be worse than recording it. PLAN §6.
The command handlers still pass neither the query nor any array filters, so
over the wire this is `$[]` working and the other two refusing -- for the
right reason and with the right code, which they did not before. The plumbing
is the next commit.
221/221 unit tests in ReleaseFast and ReleaseSafe, 83/83 fuzz.
Positional corpus 15 -> 23 of 51.