M3: the positional operators and arrayFilters #7
Reference in New Issue
Block a user
Delete Branch "m3-positional"
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?
The positional operators, against the gate recorded for them last PR.
$[],$[<identifier>]and$stop being refused and start being walked, andarrayFiltersreaches the update that names it.The shape
A path with a positional segment names a set of concrete paths rather than
one:
y.$[].bon a two-element array namesy.0.bandy.1.b.resolveexpands it against the document at hand and every operator then walks paths it
already knew how to walk. Nothing below
resolveknows a positional segmentexists, which is why
$set,$inc,$unset,$pushand$pullall get it atonce rather than one at a time.
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 the scan.That placement is load-bearing in both directions and a test pins each:
matches nothing at all;
Measured, not recalled
56 shapes run against mongod 8.3.7. Three changed what the code does:
$[]in first position answers the array filter identifier message, notthe
$one -- mongod treats the two spellings as one check.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 of fields.
$renamerefuses a positional path on either end, with its own message foreach, so it keeps the plain split rather than resolving.
Also settled: an identifier is
[a-z][a-zA-Z0-9]*(aB2yes,Ab/a_b/1xno);
$unsetof an element leaves a null rather than shortening the array; ascalar element with more path below it is PathNotViable (28), which without the
check would have had the walk replace the
7aty.1with{b: 9}-- asmaller copy of the destruction this work replaced.
Numbers
tests/spec/positional/The scorecard delta is the fourteen
arrayFilterscases across five files andnothing else -- which is what a corpus recorded from mongod is for.
zig build fuzz83/83; aggregation corpus 70/0; the full e2e matrix andcrash-fuzzgreen.Divergences, recorded rather than papered over
Three answers still differ from mongod, all in PLAN §6:
$with two predicates on one array.{"y.b": 3, "y.c": 2}matches[{b: 3, c: 1}, {b: 1, c: 2}]without either element satisfying both, and thedocument still matched. mongod writes element 1, this writes element 0.
mongod's answer is an artefact of which predicate last wrote its match
position -- reversing the two predicates still gives 1 -- so there is no rule
here to copy, only a behaviour to record.
$and/$or. mongod accepts it and findsthe identifier inside; this refuses with 9.
$set: {"y.0.b": 1}ony: [null]is PathNotViable on mongod; here it creates. The positional walk refuses it,
the plain indexed path does not. One rule reached two ways, and only one way
agrees -- worth its own commit, and it needs its own measurements first,
because null-padding past the end of an array is a legal creation.
Mutations
Five run. Four redden as their comments promise. The fifth did not: the comment
claimed moving
update.validatebelowscan_matchingwould break the test, andit does not -- the call still sits above the zero-match branch. What breaks it
is deleting the standalone call. Fixed in
1482df8; a mutation note nobody ranis worth less than no note.
`$[]`, `$[<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.