update: $addToSet, $pop and $pullAll

`$addToSet`'s identity is `bson.compare` equality, which is already exactly
mongod's: an int32 `2` and a double `2.0` are one value, and `{a: 1, b: 2}`
and `{b: 2, a: 1}` are two, because `compare_docs` walks the pairs
positionally and tie-breaks on the key. Candidates are checked against the
array as it grows, so a `$each` holding the same value twice adds it once.

`$pullAll` is `$pull`'s neighbour and its opposite: `$pull` takes a predicate,
`$pullAll` takes values compared whole. `{$pull: {t: {a: 1}}}` removes
elements *having* `a: 1`; `{$pullAll: {t: [{a: 1}]}}` removes elements that
*are* `{a: 1}`. Sharing the comparison would have been the natural mistake and
is what the test mutates to check.

`$pop` is the small one, and its refusals are the measured part: an empty
array and an absent field are no-ops, an argument that is not 1 or -1 is
FailedToParse (9), and a non-array field is TypeMismatch (14) -- where the
identical mistake under `$addToSet` and `$pullAll` is BadValue (2). Three
codes for one shape, none of them derivable from the others. `$each` that is
not an array is 14 under `$addToSet` and 2 under `$push`, measured on both.

array-ops.json 2/26 -> 26/26. 234/234 unit tests.
This commit is contained in:
A.Shakhmatov
2026-08-10 21:11:33 +03:00
parent c2f682717b
commit 066b32617e
2 changed files with 322 additions and 0 deletions

View File

@@ -4272,6 +4272,42 @@ fn update_refusal(reply: *wire.Reply, err: anyerror, diag: update.Diagnostic) !v
.{ if (std.mem.eql(u8, diag.segment, "$inc")) "increment" else "multiply", diag.path },
),
),
error.NotAnArrayField => return bad_value(reply, try std.fmt.allocPrint(
arena,
"Cannot apply {s} to non-array field. Field named '{s}' has non-array type",
.{ diag.segment, diag.path },
)),
error.NotAnArrayPathElement => return reply.put_error(
@intFromEnum(ErrorCode.type_mismatch),
"TypeMismatch",
try std.fmt.allocPrint(
arena,
"Path '{s}' contains an element of non-array type",
.{diag.path},
),
),
// mongod has two sentences here -- "$pop expects 1 or -1, found: 2"
// and "Expected a number in: t: \"x\"" -- both code 9, and both about
// an argument that is not one of the two values `$pop` takes. One
// sentence covering both says the same thing without rendering the
// operand, which no formatter here does.
error.BadPopArgument => return failed_to_parse(reply, try std.fmt.allocPrint(
arena,
"$pop expects 1 or -1, at field '{s}'",
.{diag.path},
)),
error.PullAllNeedsArray => return bad_value(reply, try std.fmt.allocPrint(
arena,
"$pullAll requires an array argument but was given a {s}",
.{diag.other},
)),
// `$push` and `$addToSet` disagree about the code for the identical
// mistake: 2 and 14. Measured on both, and not derivable from either.
error.BadEach => if (std.mem.eql(u8, diag.segment, "$addToSet")) return reply.put_error(
@intFromEnum(ErrorCode.type_mismatch),
"TypeMismatch",
"The argument to $each in $addToSet must be an array",
) else return bad_value(reply, "The argument to $each in $push must be an array"),
error.PathNotViable => return reply.put_error(
@intFromEnum(ErrorCode.path_not_viable),
"PathNotViable",