diff --git a/tests/spec/operators/README.md b/tests/spec/operators/README.md new file mode 100644 index 0000000..7caae44 --- /dev/null +++ b/tests/spec/operators/README.md @@ -0,0 +1,103 @@ +# The update-operator corpus + +PLAN §3 lists eight update operators for M3 — `$setOnInsert`, `$addToSet`, +`$mul`, `$min`, `$max`, `$pop`, `$pullAll`, `$currentDate` — and the pinned +crud corpus says almost nothing about any of them. A probe running the +identical update against mongod 8.3.7 and this server found: + +- all eight answering `bad update`, code 2, one message for every question; +- `$push`'s `$slice`, `$position` and `$sort` **silently ignored**. + `{$each: [3, 4], $slice: -3}` appended both values, sliced nothing, and + answered `ok: 1` with `modifiedCount: 1`. + +The second is the reason this directory exists rather than a list of TODOs. +A missing operator is an error the client can see; a modifier that is parsed, +accepted and then dropped is the same class of wrong answer the positional +operators were — the client asked for one thing and was told it got it. + +## The one rule + +**Inputs are authored here; expectations are measured against a real mongod.** + +``` +tests/spec/operators/ + sources/*.json documents + operations, authored + record.js runs them against mongod, writes the expectations + *.json generated, unified format, do not hand-edit +``` + +```sh +mongod --port 27099 --dbpath +node tests/spec/operators/record.js --mongod-port 27099 +node tests/spec/run.js --suite-dir tests/spec/operators +``` + +Same discipline as `tests/spec/positional/` and `tests/spec/aggregate/`, and +for the same reason: a corpus written end to end here can encode our own bugs +as expectations and then agree with us forever. + +## What cannot be recorded as a value + +Two things in this corpus are not predictable, and both are replaced by a +`$$type` assertion rather than left out: + +- a `$currentDate` field is whatever the clock said. The case names it in + `volatile`, per case, so a corpus that one day wants to pin a real stored + date still can. +- an upsert that inserts gets a generated ObjectId. That one is automatic: + no source file authors an ObjectId, so the rule is unambiguous. + +Everything else about the document — which fields exist, in what order, +holding what — is still compared exactly. A case that dropped a field still +fails. + +Files are written with canonical extended JSON (`relaxed: false`), which the +runner already parses that way. It is verbose and it is exact: `$mul` +overflowing an int32 produces an int64, and a corpus that wrote `4000000000` +as a bare number would not have said so. + +## Where it stands + +Recorded against mongod 8.3.7, run against the server before any of it was +implemented: + +``` +array-ops.json 2 pass 24 fail 0 skip +current-date.json 3 pass 8 fail 0 skip +modifiers.json 7 pass 7 fail 0 skip +numeric.json 0 pass 21 fail 0 skip +push-modifiers.json 6 pass 15 fail 0 skip +set-on-insert.json 0 pass 9 fail 0 skip +``` + +Red by construction. The 18 that pass are the shapes this server already +answers the way mongod does — mostly refusals that happen to agree, plus the +`$push` cases with no modifier on them. + +## What recording it settled + +None of this is guessable, and several rows contradict the obvious reading: + +| | mongod | +|---|---| +| `$mul` of a missing field | writes **0**, not the operand | +| `$mul` of a non-numeric field, or by one | TypeMismatch (14) | +| `$min`/`$max` across types | compares in BSON canonical order, so `$min: {s: 5}` on `s: "b"` writes 5 | +| `$min`/`$max` of a missing field | always writes | +| two operators writing one field | **ConflictingUpdateOperators (40)** — `$min`+`$max`, `$set`+`$inc`, `$setOnInsert`+`$set` | +| `$addToSet` of a document | compares whole, **field order included**: `{a:1,b:2}` and `{b:2,a:1}` are two values | +| `$addToSet` of `2` and `2.0` | one value | +| `$pop` of an empty or missing field | no-op, not an error | +| `$pop` with an argument that is not ±1 | FailedToParse (9); on a non-array field, TypeMismatch (14) | +| `$pullAll` with a non-array argument | BadValue (2) | +| `$push` modifiers without `$each` | **not modifiers at all** — `{$slice: 1}` is pushed as a value | +| `$push` modifier order | insert at `$position`, then `$sort` the whole array, then `$slice` | +| `$position` negative | counted from the end | +| `$currentDate` with `false` | still writes a date; the boolean's value is ignored | +| `$currentDate` with anything but a bool or `{$type: date\|timestamp}` | BadValue (2) | +| `$setOnInsert` writing `_id` on an insert | **allowed**, unlike `$set` | +| an unknown modifier | FailedToParse (9), not BadValue | + +One case was authored and then removed: `{b: 1, $set: {c: 1}}` is rejected by +the driver before it reaches a server, so there is no server answer to record +and the case would have asserted nothing. diff --git a/tests/spec/operators/array-ops.json b/tests/spec/operators/array-ops.json new file mode 100644 index 0000000..7692dba --- /dev/null +++ b/tests/spec/operators/array-ops.json @@ -0,0 +1 @@ +{"description":"array-ops","schemaVersion":"1.4","createEntities":[{"client":{"id":"client0"}},{"database":{"id":"database0","client":"client0","databaseName":"operator-corpus"}},{"collection":{"id":"collection0","database":"database0","collectionName":"coll"}}],"initialData":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}],"tests":[{"description":"$addToSet appends a value the array does not hold","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$addToSet":{"t":{"$numberInt":"4"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"},{"$numberInt":"4"}]}]}]},{"description":"$addToSet on a value already there writes nothing","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$addToSet":{"t":{"$numberInt":"2"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$addToSet on a missing field creates the array","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$addToSet":{"gone":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}],"gone":[{"$numberInt":"1"}]}]}]},{"description":"$addToSet $each adds only what is missing","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$addToSet":{"t":{"$each":[{"$numberInt":"2"},{"$numberInt":"4"},{"$numberInt":"5"}]}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"},{"$numberInt":"4"},{"$numberInt":"5"}]}]}]},{"description":"$addToSet $each with duplicates inside it","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$addToSet":{"t":{"$each":[{"$numberInt":"7"},{"$numberInt":"7"}]}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"},{"$numberInt":"7"}]}]}]},{"description":"$addToSet compares documents whole","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"a":{"$numberInt":"1"}}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$addToSet":{"t":{"a":{"$numberInt":"1"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"a":{"$numberInt":"1"}}]}]}]},{"description":"$addToSet distinguishes documents by field order","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"a":{"$numberInt":"1"},"b":{"$numberInt":"2"}}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$addToSet":{"t":{"b":{"$numberInt":"2"},"a":{"$numberInt":"1"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"a":{"$numberInt":"1"},"b":{"$numberInt":"2"}},{"b":{"$numberInt":"2"},"a":{"$numberInt":"1"}}]}]}]},{"description":"$addToSet treats an int and an equal double as one value","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$addToSet":{"t":{"$numberInt":"2"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$addToSet on a non-array field","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":{"$numberInt":"5"}}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$addToSet":{"t":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":{"$numberInt":"5"}}]}]},{"description":"$pop removes the last element","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pop":{"t":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"$pop with -1 removes the first","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pop":{"t":{"$numberInt":"-1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$pop of an empty array writes nothing","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pop":{"t":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[]}]}]},{"description":"$pop of a missing field writes nothing","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pop":{"gone":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$pop of a one-element array leaves an empty one","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"9"}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pop":{"t":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[]}]}]},{"description":"$pop with an argument that is neither 1 nor -1","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pop":{"t":{"$numberInt":"2"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"9"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$pop with 1.0, which is 1","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pop":{"t":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"$pop of a non-array field","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":{"$numberInt":"5"}}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pop":{"t":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"14"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":{"$numberInt":"5"}}]}]},{"description":"$pullAll removes every copy of every listed value","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"},{"$numberInt":"2"}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pullAll":{"t":[{"$numberInt":"2"}]}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"3"}]}]}]},{"description":"$pullAll with several values","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pullAll":{"t":[{"$numberInt":"1"},{"$numberInt":"3"}]}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"2"}]}]}]},{"description":"$pullAll matching nothing writes nothing","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pullAll":{"t":[{"$numberInt":"9"}]}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$pullAll compares documents whole, not by predicate","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"a":{"$numberInt":"1"}},{"a":{"$numberInt":"2"}}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pullAll":{"t":[{"a":{"$numberInt":"1"}}]}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"a":{"$numberInt":"2"}}]}]}]},{"description":"$pullAll on a missing field writes nothing","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pullAll":{"gone":[{"$numberInt":"1"}]}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$pullAll with an argument that is not an array","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pullAll":{"t":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$pullAll emptying the array","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pullAll":{"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[]}]}]},{"description":"$addToSet through a positional segment","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"y":[{"t":[{"$numberInt":"1"}]},{"t":[{"$numberInt":"2"}]}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$addToSet":{"y.$[].t":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"y":[{"t":[{"$numberInt":"1"}]},{"t":[{"$numberInt":"2"},{"$numberInt":"1"}]}]}]}]},{"description":"$pop down a dotted path","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"n":{"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pop":{"n.t":{"$numberInt":"-1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"n":{"t":[{"$numberInt":"2"}]}}]}]}]} diff --git a/tests/spec/operators/current-date.json b/tests/spec/operators/current-date.json new file mode 100644 index 0000000..b987ba7 --- /dev/null +++ b/tests/spec/operators/current-date.json @@ -0,0 +1 @@ +{"description":"current-date","schemaVersion":"1.4","createEntities":[{"client":{"id":"client0"}},{"database":{"id":"database0","client":"client0","databaseName":"operator-corpus"}},{"collection":{"id":"collection0","database":"database0","collectionName":"coll"}}],"initialData":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"}}]}],"tests":[{"description":"$currentDate with true writes a date","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$currentDate":{"d":true}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"d":{"$$type":"date"}}]}]},{"description":"$currentDate with an explicit date type","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$currentDate":{"d":{"$type":"date"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"d":{"$$type":"date"}}]}]},{"description":"$currentDate with the timestamp type","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$currentDate":{"d":{"$type":"timestamp"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"d":{"$$type":"timestamp"}}]}]},{"description":"$currentDate overwrites a field that is already there","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$currentDate":{"a":true}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$$type":"date"}}]}]},{"description":"$currentDate down a dotted path","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$currentDate":{"n.d":true}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"n":{"d":{"$$type":"date"}}}]}]},{"description":"$currentDate beside another operator","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$currentDate":{"d":true},"$inc":{"a":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"2"},"d":{"$$type":"date"}}]}]},{"description":"$currentDate with false, which is still a date","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$currentDate":{"d":false}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"d":{"$$type":"date"}}]}]},{"description":"$currentDate with an unknown type name","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$currentDate":{"d":{"$type":"nope"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"}}]}]},{"description":"$currentDate with a document that is not $type","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$currentDate":{"d":{"a":{"$numberInt":"1"}}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"}}]}]},{"description":"$currentDate with a number","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$currentDate":{"d":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"}}]}]},{"description":"$currentDate on an upsert that inserts","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"updateOne","arguments":{"filter":{"k":{"$numberInt":"1"}},"update":{"$currentDate":{"d":true}},"upsert":true},"expectResult":{"matchedCount":{"$numberInt":"0"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"1"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$$type":"objectId"},"k":{"$numberInt":"1"},"d":{"$$type":"date"}}]}]}]} diff --git a/tests/spec/operators/modifiers.json b/tests/spec/operators/modifiers.json new file mode 100644 index 0000000..17fae6b --- /dev/null +++ b/tests/spec/operators/modifiers.json @@ -0,0 +1 @@ +{"description":"modifiers","schemaVersion":"1.4","createEntities":[{"client":{"id":"client0"}},{"database":{"id":"database0","client":"client0","databaseName":"operator-corpus"}},{"collection":{"id":"collection0","database":"database0","collectionName":"coll"}}],"initialData":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}],"tests":[{"description":"an unknown modifier","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$bogus":{"a":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"9"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"an unknown modifier beside a known one","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$set":{"b":{"$numberInt":"1"}},"$bogus":{"a":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"9"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"a known modifier whose argument is not a document","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$set":{"$numberInt":"1"}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"9"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"an empty argument to a known modifier","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$set":{}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"a data field after an operator","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$set":{"c":{"$numberInt":"1"}},"b":{"$numberInt":"1"}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"9"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"two operators writing the same field","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$set":{"a":{"$numberInt":"2"}},"$inc":{"a":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"40"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"$inc on a non-numeric field","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":"x"}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$inc":{"a":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"14"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":"x"}]}]},{"description":"$inc by a non-numeric operand","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$inc":{"a":"x"}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"14"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"$push onto a non-array field","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":{"$numberInt":"5"}}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":{"$numberInt":"5"}}]}]},{"description":"$pull on a non-array field","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":{"$numberInt":"5"}}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$pull":{"t":{"$numberInt":"1"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":{"$numberInt":"5"}}]}]},{"description":"$unset of a field that is not there","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$unset":{"gone":""}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"$rename onto a field that already exists","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$rename":{"a":"t"}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":{"$numberInt":"1"}}]}]},{"description":"$rename of a field that is not there","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$rename":{"gone":"b"}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"an update that writes nothing still reports a match","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$set":{"a":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]}]} diff --git a/tests/spec/operators/numeric.json b/tests/spec/operators/numeric.json new file mode 100644 index 0000000..81796be --- /dev/null +++ b/tests/spec/operators/numeric.json @@ -0,0 +1 @@ +{"description":"numeric","schemaVersion":"1.4","createEntities":[{"client":{"id":"client0"}},{"database":{"id":"database0","client":"client0","databaseName":"operator-corpus"}},{"collection":{"id":"collection0","database":"database0","collectionName":"coll"}}],"initialData":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"}}]}],"tests":[{"description":"$mul multiplies an int by an int","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$mul":{"a":{"$numberInt":"2"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"10"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$mul of a missing field","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$mul":{"gone":{"$numberInt":"5"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"},"gone":{"$numberInt":"0"}}]}]},{"description":"$mul mixes int and double","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$mul":{"d":{"$numberInt":"2"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberInt":"5"}}]}]},{"description":"$mul by zero","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$mul":{"a":{"$numberInt":"0"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"0"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$mul overflowing an int32","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"2000000000"}}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$mul":{"a":{"$numberInt":"2"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberLong":"4000000000"}}]}]},{"description":"$mul of a non-numeric field","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$mul":{"s":{"$numberInt":"2"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"14"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$mul by a non-numeric operand","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$mul":{"a":"x"}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"14"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$mul down a dotted path","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"n":{"a":{"$numberInt":"3"}}}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$mul":{"n.a":{"$numberInt":"4"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"n":{"a":{"$numberInt":"12"}}}]}]},{"description":"$min writes when the operand is lower","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$min":{"a":{"$numberInt":"3"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"3"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$min leaves the field when the operand is higher","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$min":{"a":{"$numberInt":"7"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$min of a missing field always writes","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$min":{"gone":{"$numberInt":"7"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"},"gone":{"$numberInt":"7"}}]}]},{"description":"$min against a field of another type","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$min":{"s":{"$numberInt":"5"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":{"$numberInt":"5"},"d":{"$numberDouble":"2.5"}}]}]},{"description":"$min compares an int and an equal double","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$min":{"a":{"$numberInt":"5"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$max writes when the operand is higher","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$max":{"a":{"$numberInt":"7"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"7"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$max leaves the field when the operand is lower","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$max":{"a":{"$numberInt":"3"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$max of a missing field always writes","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$max":{"gone":{"$numberInt":"7"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"},"gone":{"$numberInt":"7"}}]}]},{"description":"$max against a field of another type","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$max":{"s":{"$numberInt":"5"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$max against a null field","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"a":null}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$max":{"a":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"1"}}]}]},{"description":"$min and $max on the same field in one update","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$min":{"a":{"$numberInt":"3"}},"$max":{"a":{"$numberInt":"9"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"40"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"a":{"$numberInt":"5"},"s":"b","d":{"$numberDouble":"2.5"}}]}]},{"description":"$min through an array index","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"5"},{"$numberInt":"5"}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$min":{"t.0":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"5"}]}]}]},{"description":"$mul through a positional segment","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"a":{"$numberInt":"2"}},{"a":{"$numberInt":"3"}}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$mul":{"t.$[].a":{"$numberInt":"10"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"a":{"$numberInt":"20"}},{"a":{"$numberInt":"30"}}]}]}]}]} diff --git a/tests/spec/operators/push-modifiers.json b/tests/spec/operators/push-modifiers.json new file mode 100644 index 0000000..d61e933 --- /dev/null +++ b/tests/spec/operators/push-modifiers.json @@ -0,0 +1 @@ +{"description":"push-modifiers","schemaVersion":"1.4","createEntities":[{"client":{"id":"client0"}},{"database":{"id":"database0","client":"client0","databaseName":"operator-corpus"}},{"collection":{"id":"collection0","database":"database0","collectionName":"coll"}}],"initialData":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}],"tests":[{"description":"$slice keeps the last n","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"3"},{"$numberInt":"4"}],"$slice":{"$numberInt":"-3"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"2"},{"$numberInt":"3"},{"$numberInt":"4"}]}]}]},{"description":"$slice keeps the first n","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"3"},{"$numberInt":"4"}],"$slice":{"$numberInt":"3"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$slice of zero empties the array","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"3"}],"$slice":{"$numberInt":"0"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[]}]}]},{"description":"$slice larger than the array keeps all of it","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"3"}],"$slice":{"$numberInt":"10"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$slice with an empty $each truncates without adding","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[],"$slice":{"$numberInt":"1"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"}]}]}]},{"description":"$position inserts at the front","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"9"}],"$position":{"$numberInt":"0"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"9"},{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"$position inserts in the middle","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"9"}],"$position":{"$numberInt":"1"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"9"},{"$numberInt":"2"}]}]}]},{"description":"$position past the end appends","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"9"}],"$position":{"$numberInt":"99"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"9"}]}]}]},{"description":"$position counted from the end","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"9"}],"$position":{"$numberInt":"-1"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"9"},{"$numberInt":"2"}]}]}]},{"description":"$sort ascending over scalars","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"3"},{"$numberInt":"1"}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"2"}],"$sort":{"$numberInt":"1"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$numberInt":"3"}]}]}]},{"description":"$sort descending over scalars","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"3"},{"$numberInt":"1"}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"2"}],"$sort":{"$numberInt":"-1"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"3"},{"$numberInt":"2"},{"$numberInt":"1"}]}]}]},{"description":"$sort on a field of the elements","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"a":{"$numberInt":"3"}},{"a":{"$numberInt":"1"}}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"a":{"$numberInt":"2"}}],"$sort":{"a":{"$numberInt":"1"}}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"a":{"$numberInt":"1"}},{"a":{"$numberInt":"2"}},{"a":{"$numberInt":"3"}}]}]}]},{"description":"$sort and $slice together","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"insertMany","arguments":{"documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"3"},{"$numberInt":"1"}]}]}},{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"2"}],"$sort":{"$numberInt":"1"},"$slice":{"$numberInt":"2"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"$position and $slice together","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"9"}],"$position":{"$numberInt":"0"},"$slice":{"$numberInt":"2"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"9"},{"$numberInt":"1"}]}]}]},{"description":"$slice without $each","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$slice":{"$numberInt":"1"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"$slice":{"$numberInt":"1"}}]}]}]},{"description":"$each that is not an array","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":{"$numberInt":"3"}}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"$slice that is not a number","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"3"}],"$slice":"x"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"$position that is not a number","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"3"}],"$position":"x"}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"an unknown modifier beside $each","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"$each":[{"$numberInt":"3"}],"$bogus":{"$numberInt":"1"}}}}},"expectError":{"isError":true,"errorCode":{"$numberInt":"2"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]},{"description":"a document operand that is not modifiers at all is a value","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"t":{"a":{"$numberInt":"1"}}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"},{"a":{"$numberInt":"1"}}]}]}]},{"description":"$push $each onto a missing field","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{},"update":{"$push":{"gone":{"$each":[{"$numberInt":"1"},{"$numberInt":"2"}]}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"t":[{"$numberInt":"1"},{"$numberInt":"2"}],"gone":[{"$numberInt":"1"},{"$numberInt":"2"}]}]}]}]} diff --git a/tests/spec/operators/record.js b/tests/spec/operators/record.js new file mode 100644 index 0000000..d151a5f --- /dev/null +++ b/tests/spec/operators/record.js @@ -0,0 +1,222 @@ +// Record an update-operator corpus by asking a real mongod what the answer is. +// +// Eight operators PLAN §3 lists for M3 -- `$setOnInsert`, `$addToSet`, `$mul`, +// `$min`, `$max`, `$pop`, `$pullAll`, `$currentDate` -- plus `$push`'s +// `$slice`/`$position`/`$sort` modifiers. The pinned crud corpus contains +// almost nothing about any of them, and a probe against both servers found +// every one of the eight answering `bad update` here, and the three `$push` +// modifiers *silently ignored*: `{$each: [3, 4], $slice: -3}` appended without +// slicing and answered ok: 1. Same discipline as `tests/spec/positional/` and +// `tests/spec/aggregate/`: inputs are authored in `sources/`, expectations are +// measured here. +// +// node tests/spec/operators/record.js --mongod-port 27099 +// +// Options: +// --mongod-port a running mongod to measure against (default 27099) +// --only record just one source file +const fs = require('fs'); +const path = require('path'); +// The same pinned driver `run.js` uses, resolved the same way. +const DRIVER = path.join(__dirname, '..', '..', 'e2e', 'node_modules', 'mongodb'); +const { MongoClient } = require(DRIVER); +const { EJSON, ObjectId, Timestamp } = require(path.join(DRIVER, 'lib', 'bson.js')); + +const argv = process.argv.slice(2); +function opt(name, dflt) { + const i = argv.indexOf('--' + name); + if (i < 0) return dflt; + const v = argv[i + 1]; + return v === undefined || v.startsWith('--') ? true : v; +} + +const PORT = parseInt(opt('mongod-port', '27099'), 10); +const ONLY = opt('only', null); +const SRC_DIR = path.join(__dirname, 'sources'); +const DB_NAME = 'operator-corpus'; +const COLL = 'coll'; + +// Every construct here predates 4.0, so nothing depends on a server newer than +// the 4.4 this one reports. +const SCHEMA_VERSION = '1.4'; + +const UPDATE_KEYS = ['matchedCount', 'modifiedCount', 'upsertedCount']; + +/// Values nobody can predict, replaced by an assertion about their type. +/// +/// Two kinds, and both are the corpus staying honest rather than the corpus +/// looking away: a `$currentDate` field is whatever the clock said, and an +/// upsert that inserts gets a generated ObjectId. Everything else about the +/// document -- which fields exist, in what order, holding what -- is still +/// compared exactly, so a case that lost a field still fails. +/// +/// The ObjectId rule is automatic because it is unambiguous: no source file +/// authors one. A date is not: `volatile` names those per case, so a corpus +/// that one day wants to pin a real stored date still can. +function maskGenerated(value) { + if (value instanceof ObjectId) return { $$type: 'objectId' }; + if (Array.isArray(value)) return value.map(maskGenerated); + if (value && typeof value === 'object' && value.constructor === Object) { + const out = {}; + for (const [k, v] of Object.entries(value)) out[k] = maskGenerated(v); + return out; + } + return value; +} + +function maskVolatile(docs, paths) { + for (const p of paths) { + for (const doc of docs) { + const segs = p.split('.'); + let cur = doc; + for (const s of segs.slice(0, -1)) cur = cur === undefined ? undefined : cur[s]; + const last = segs[segs.length - 1]; + if (cur === undefined || !(last in cur)) { + throw new Error(`volatile path '${p}' is not in the recorded document ` + + `${JSON.stringify(doc)} -- the case did not write what it said it would`); + } + const v = cur[last]; + if (v instanceof Date) cur[last] = { $$type: 'date' }; + else if (v instanceof Timestamp) cur[last] = { $$type: 'timestamp' }; + else throw new Error(`volatile path '${p}' holds ${v}, which is neither a date nor a timestamp`); + } + } +} + +async function main() { + if (!fs.existsSync(SRC_DIR)) { + console.error(`missing ${SRC_DIR}`); + process.exit(2); + } + const client = new MongoClient(`mongodb://127.0.0.1:${PORT}`, { serverSelectionTimeoutMS: 3000 }); + try { + await client.connect(); + } catch (e) { + console.error(`no mongod on :${PORT} -- start one first:\n` + + ` mongod --port ${PORT} --dbpath \n${e.message}`); + process.exit(2); + } + const build = await client.db('admin').command({ buildInfo: 1 }); + console.log(`recording against mongod ${build.version} on :${PORT}`); + + const sources = fs.readdirSync(SRC_DIR).filter((f) => f.endsWith('.json')) + .filter((f) => !ONLY || f === ONLY || f === ONLY + '.json') + .sort(); + if (!sources.length) { + console.error('no source files'); + process.exit(2); + } + + for (const file of sources) { + const src = JSON.parse(fs.readFileSync(path.join(SRC_DIR, file), 'utf8')); + const name = path.basename(file, '.json'); + const out = await record(client, name, src); + const text = EJSON.stringify(out, { relaxed: false, indent: 2 }); + fs.writeFileSync(path.join(__dirname, `${name}.json`), text + '\n'); + const errs = out.tests.filter((t) => t.operations[t.operations.length - 1].expectError).length; + console.log(` ${name}: ${out.tests.length} cases, ${errs} of them errors`); + } + await client.close(); + console.log('RECORDED'); +} + +async function record(client, name, src) { + const coll = client.db(DB_NAME).collection(COLL); + const tests = []; + + for (const c of src.cases) { + const documents = c.documents === undefined ? src.documents : c.documents; + await coll.drop().catch(() => {}); + if (documents.length) await coll.insertMany(structuredClone(documents)); + + // A case that brings its own documents reseeds through *operations*: + // the unified format's `initialData` is per file, and the runner seeds + // it once per test. + const setup = []; + if (c.documents !== undefined) { + setup.push({ object: 'collection0', name: 'deleteMany', arguments: { filter: {} } }); + if (documents.length) { + setup.push({ + object: 'collection0', + name: 'insertMany', + arguments: { documents: structuredClone(documents) }, + }); + } + } + const op = { + object: 'collection0', + name: c.operation, + arguments: structuredClone(c.arguments), + }; + try { + op.expectResult = maskGenerated(await invoke(coll, c.operation, structuredClone(c.arguments))); + } catch (e) { + // The code, not the message: message text is mongod's to change + // between releases, and several of these embed a rendering of the + // offending BSON that no formatter here produces. + delete op.expectResult; + op.expectError = { isError: true, errorCode: e.code }; + } + + // Recorded for every case including the refusals: a refusal that left + // the document half-written would look identical to a clean one in + // `expectError` alone. + const after = await coll.find({}, { sort: { _id: 1 } }).toArray(); + if (c.volatile) maskVolatile(after, c.volatile); + + tests.push({ + description: c.description, + operations: [...setup, op], + outcome: [{ + collectionName: COLL, + databaseName: DB_NAME, + documents: after.map(maskGenerated), + }], + }); + } + await coll.drop().catch(() => {}); + + return { + description: name, + schemaVersion: SCHEMA_VERSION, + // Recorded, not authored. Regenerate with tests/spec/operators/record.js. + createEntities: [ + { client: { id: 'client0' } }, + { database: { id: 'database0', client: 'client0', databaseName: DB_NAME } }, + { collection: { id: 'collection0', database: 'database0', collectionName: COLL } }, + ], + initialData: [{ collectionName: COLL, databaseName: DB_NAME, documents: src.documents }], + tests, + }; +} + +async function invoke(coll, name, args) { + const { filter, update, replacement, ...rest } = args; + switch (name) { + case 'updateOne': + return pick(await coll.updateOne(filter, update, rest), UPDATE_KEYS); + case 'updateMany': + return pick(await coll.updateMany(filter, update, rest), UPDATE_KEYS); + case 'replaceOne': + return pick(await coll.replaceOne(filter, replacement, rest), UPDATE_KEYS); + case 'findOneAndUpdate': { + const r = await coll.findOneAndUpdate(filter, update, rest); + // Driver 5+ returns the document itself; 4.x wrapped it in + // `{value}`. run.js tolerates both the same way. + return r && typeof r === 'object' && 'value' in r && 'ok' in r ? r.value : r; + } + default: + throw new Error(`record.js does not know the operation '${name}'`); + } +} + +function pick(result, keys) { + const o = {}; + for (const k of keys) if (result[k] !== undefined) o[k] = result[k]; + return o; +} + +main().catch((e) => { + console.error('RECORD_FAIL', e); + process.exit(1); +}); diff --git a/tests/spec/operators/set-on-insert.json b/tests/spec/operators/set-on-insert.json new file mode 100644 index 0000000..0a65ee1 --- /dev/null +++ b/tests/spec/operators/set-on-insert.json @@ -0,0 +1 @@ +{"description":"set-on-insert","schemaVersion":"1.4","createEntities":[{"client":{"id":"client0"}},{"database":{"id":"database0","client":"client0","databaseName":"operator-corpus"}},{"collection":{"id":"collection0","database":"database0","collectionName":"coll"}}],"initialData":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"k":{"$numberInt":"1"},"a":{"$numberInt":"100"}}]}],"tests":[{"description":"$setOnInsert writes when the upsert inserts","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"updateOne","arguments":{"filter":{"k":{"$numberInt":"1"}},"update":{"$setOnInsert":{"a":{"$numberInt":"1"}},"$set":{"b":{"$numberInt":"2"}}},"upsert":true},"expectResult":{"matchedCount":{"$numberInt":"0"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"1"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$$type":"objectId"},"k":{"$numberInt":"1"},"a":{"$numberInt":"1"},"b":{"$numberInt":"2"}}]}]},{"description":"$setOnInsert is ignored when the upsert updates","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{"k":{"$numberInt":"1"}},"update":{"$setOnInsert":{"a":{"$numberInt":"1"}},"$set":{"b":{"$numberInt":"2"}}},"upsert":true},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"1"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"k":{"$numberInt":"1"},"a":{"$numberInt":"100"},"b":{"$numberInt":"2"}}]}]},{"description":"$setOnInsert alone on a plain update is a no-op","operations":[{"object":"collection0","name":"updateOne","arguments":{"filter":{"k":{"$numberInt":"1"}},"update":{"$setOnInsert":{"a":{"$numberInt":"1"}}}},"expectResult":{"matchedCount":{"$numberInt":"1"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"0"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"1"},"k":{"$numberInt":"1"},"a":{"$numberInt":"100"}}]}]},{"description":"$setOnInsert alone on an insert is the whole document","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"updateOne","arguments":{"filter":{"k":{"$numberInt":"7"}},"update":{"$setOnInsert":{"a":{"$numberInt":"1"}}},"upsert":true},"expectResult":{"matchedCount":{"$numberInt":"0"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"1"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$$type":"objectId"},"k":{"$numberInt":"7"},"a":{"$numberInt":"1"}}]}]},{"description":"$setOnInsert writes a dotted path on insert","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"updateOne","arguments":{"filter":{"k":{"$numberInt":"1"}},"update":{"$setOnInsert":{"x.y":{"$numberInt":"5"}}},"upsert":true},"expectResult":{"matchedCount":{"$numberInt":"0"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"1"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$$type":"objectId"},"k":{"$numberInt":"1"},"x":{"y":{"$numberInt":"5"}}}]}]},{"description":"$setOnInsert may not touch _id","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"updateOne","arguments":{"filter":{"k":{"$numberInt":"1"}},"update":{"$setOnInsert":{"_id":{"$numberInt":"9"}}},"upsert":true},"expectResult":{"matchedCount":{"$numberInt":"0"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"1"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$numberInt":"9"},"k":{"$numberInt":"1"}}]}]},{"description":"$setOnInsert and $set naming the same field on an insert","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"updateOne","arguments":{"filter":{"k":{"$numberInt":"1"}},"update":{"$setOnInsert":{"a":{"$numberInt":"1"}},"$set":{"a":{"$numberInt":"2"}}},"upsert":true},"expectError":{"isError":true,"errorCode":{"$numberInt":"40"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[]}]},{"description":"findOneAndUpdate reports the document $setOnInsert built","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"findOneAndUpdate","arguments":{"filter":{"k":{"$numberInt":"3"}},"update":{"$setOnInsert":{"a":{"$numberInt":"1"}}},"upsert":true,"returnDocument":"after"},"expectResult":{"_id":{"$$type":"objectId"},"k":{"$numberInt":"3"},"a":{"$numberInt":"1"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$$type":"objectId"},"k":{"$numberInt":"3"},"a":{"$numberInt":"1"}}]}]},{"description":"updateMany with $setOnInsert on an upsert that inserts","operations":[{"object":"collection0","name":"deleteMany","arguments":{"filter":{}}},{"object":"collection0","name":"updateMany","arguments":{"filter":{"k":{"$numberInt":"1"}},"update":{"$setOnInsert":{"a":{"$numberInt":"1"}}},"upsert":true},"expectResult":{"matchedCount":{"$numberInt":"0"},"modifiedCount":{"$numberInt":"0"},"upsertedCount":{"$numberInt":"1"}}}],"outcome":[{"collectionName":"coll","databaseName":"operator-corpus","documents":[{"_id":{"$$type":"objectId"},"k":{"$numberInt":"1"},"a":{"$numberInt":"1"}}]}]}]} diff --git a/tests/spec/operators/sources/array-ops.json b/tests/spec/operators/sources/array-ops.json new file mode 100644 index 0000000..49f2a15 --- /dev/null +++ b/tests/spec/operators/sources/array-ops.json @@ -0,0 +1,153 @@ +{ + "_comment": [ + "Inputs only. Expectations are measured -- see record.js.", + "`$addToSet`, `$pop` and `$pullAll`: the three array operators that are", + "neither `$push` nor `$pull`. Each has a shape that decides whether it", + "wrote at all -- a set that already held the value, a pop of an empty", + "array, a pullAll matching nothing -- and `modifiedCount` is the only", + "place that shows, which is why every case records an outcome too." + ], + "documents": [{ "_id": 1, "t": [1, 2, 3] }], + "cases": [ + { + "description": "$addToSet appends a value the array does not hold", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$addToSet": { "t": 4 } } } + }, + { + "description": "$addToSet on a value already there writes nothing", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$addToSet": { "t": 2 } } } + }, + { + "description": "$addToSet on a missing field creates the array", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$addToSet": { "gone": 1 } } } + }, + { + "description": "$addToSet $each adds only what is missing", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$addToSet": { "t": { "$each": [2, 4, 5] } } } } + }, + { + "description": "$addToSet $each with duplicates inside it", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$addToSet": { "t": { "$each": [7, 7] } } } } + }, + { + "description": "$addToSet compares documents whole", + "documents": [{ "_id": 1, "t": [{ "a": 1 }] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$addToSet": { "t": { "a": 1 } } } } + }, + { + "description": "$addToSet distinguishes documents by field order", + "documents": [{ "_id": 1, "t": [{ "a": 1, "b": 2 }] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$addToSet": { "t": { "b": 2, "a": 1 } } } } + }, + { + "description": "$addToSet treats an int and an equal double as one value", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$addToSet": { "t": 2.0 } } } + }, + { + "description": "$addToSet on a non-array field", + "documents": [{ "_id": 1, "t": 5 }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$addToSet": { "t": 1 } } } + }, + { + "description": "$pop removes the last element", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pop": { "t": 1 } } } + }, + { + "description": "$pop with -1 removes the first", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pop": { "t": -1 } } } + }, + { + "description": "$pop of an empty array writes nothing", + "documents": [{ "_id": 1, "t": [] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pop": { "t": 1 } } } + }, + { + "description": "$pop of a missing field writes nothing", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pop": { "gone": 1 } } } + }, + { + "description": "$pop of a one-element array leaves an empty one", + "documents": [{ "_id": 1, "t": [9] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pop": { "t": 1 } } } + }, + { + "description": "$pop with an argument that is neither 1 nor -1", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pop": { "t": 2 } } } + }, + { + "description": "$pop with 1.0, which is 1", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pop": { "t": 1.0 } } } + }, + { + "description": "$pop of a non-array field", + "documents": [{ "_id": 1, "t": 5 }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pop": { "t": 1 } } } + }, + { + "description": "$pullAll removes every copy of every listed value", + "documents": [{ "_id": 1, "t": [1, 2, 3, 2] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pullAll": { "t": [2] } } } + }, + { + "description": "$pullAll with several values", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pullAll": { "t": [1, 3] } } } + }, + { + "description": "$pullAll matching nothing writes nothing", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pullAll": { "t": [9] } } } + }, + { + "description": "$pullAll compares documents whole, not by predicate", + "documents": [{ "_id": 1, "t": [{ "a": 1 }, { "a": 2 }] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pullAll": { "t": [{ "a": 1 }] } } } + }, + { + "description": "$pullAll on a missing field writes nothing", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pullAll": { "gone": [1] } } } + }, + { + "description": "$pullAll with an argument that is not an array", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pullAll": { "t": 1 } } } + }, + { + "description": "$pullAll emptying the array", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pullAll": { "t": [1, 2, 3] } } } + }, + { + "description": "$addToSet through a positional segment", + "documents": [{ "_id": 1, "y": [{ "t": [1] }, { "t": [2] }] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$addToSet": { "y.$[].t": 1 } } } + }, + { + "description": "$pop down a dotted path", + "documents": [{ "_id": 1, "n": { "t": [1, 2] } }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pop": { "n.t": -1 } } } + } + ] +} diff --git a/tests/spec/operators/sources/current-date.json b/tests/spec/operators/sources/current-date.json new file mode 100644 index 0000000..d50fd5b --- /dev/null +++ b/tests/spec/operators/sources/current-date.json @@ -0,0 +1,81 @@ +{ + "_comment": [ + "Inputs only. Expectations are measured -- see record.js.", + "`$currentDate`, the one update operator whose result cannot be recorded", + "as a value: it is whatever the clock said. `volatile` names the fields", + "whose recorded value is replaced by a `$$type` assertion, so the case", + "still pins the type, the position and every other field of the document", + "-- everything except the number nobody can predict." + ], + "documents": [{ "_id": 1, "a": 1 }], + "cases": [ + { + "description": "$currentDate with true writes a date", + "volatile": ["d"], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$currentDate": { "d": true } } } + }, + { + "description": "$currentDate with an explicit date type", + "volatile": ["d"], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$currentDate": { "d": { "$type": "date" } } } } + }, + { + "description": "$currentDate with the timestamp type", + "volatile": ["d"], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$currentDate": { "d": { "$type": "timestamp" } } } } + }, + { + "description": "$currentDate overwrites a field that is already there", + "volatile": ["a"], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$currentDate": { "a": true } } } + }, + { + "description": "$currentDate down a dotted path", + "volatile": ["n.d"], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$currentDate": { "n.d": true } } } + }, + { + "description": "$currentDate beside another operator", + "volatile": ["d"], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$currentDate": { "d": true }, "$inc": { "a": 1 } } } + }, + { + "description": "$currentDate with false, which is still a date", + "volatile": ["d"], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$currentDate": { "d": false } } } + }, + { + "description": "$currentDate with an unknown type name", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$currentDate": { "d": { "$type": "nope" } } } } + }, + { + "description": "$currentDate with a document that is not $type", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$currentDate": { "d": { "a": 1 } } } } + }, + { + "description": "$currentDate with a number", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$currentDate": { "d": 1 } } } + }, + { + "description": "$currentDate on an upsert that inserts", + "documents": [], + "volatile": ["d"], + "operation": "updateOne", + "arguments": { + "filter": { "k": 1 }, + "update": { "$currentDate": { "d": true } }, + "upsert": true + } + } + ] +} diff --git a/tests/spec/operators/sources/modifiers.json b/tests/spec/operators/sources/modifiers.json new file mode 100644 index 0000000..cd17fec --- /dev/null +++ b/tests/spec/operators/sources/modifiers.json @@ -0,0 +1,88 @@ +{ + "_comment": [ + "Inputs only. Expectations are measured -- see record.js.", + "The operator table itself, rather than any one operator: what an update", + "document may be, and what happens to a name that is not in the table.", + "This server answers `bad update` with code 2 to every one of these, which", + "is one message covering several different questions.", + "A `{b: 1, $set: {...}}` case was authored and then removed: the driver", + "rejects it before it reaches a server, so there is no server answer to", + "record and a case with no `errorCode` would assert nothing." + ], + "documents": [{ "_id": 1, "a": 1, "t": [1, 2] }], + "cases": [ + { + "description": "an unknown modifier", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$bogus": { "a": 1 } } } + }, + { + "description": "an unknown modifier beside a known one", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$set": { "b": 1 }, "$bogus": { "a": 1 } } } + }, + { + "description": "a known modifier whose argument is not a document", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$set": 1 } } + }, + { + "description": "an empty argument to a known modifier", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$set": {} } } + }, + { + "description": "a data field after an operator", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$set": { "c": 1 }, "b": 1 } } + }, + { + "description": "two operators writing the same field", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$set": { "a": 2 }, "$inc": { "a": 1 } } } + }, + { + "description": "$inc on a non-numeric field", + "documents": [{ "_id": 1, "a": "x" }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$inc": { "a": 1 } } } + }, + { + "description": "$inc by a non-numeric operand", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$inc": { "a": "x" } } } + }, + { + "description": "$push onto a non-array field", + "documents": [{ "_id": 1, "t": 5 }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": 1 } } } + }, + { + "description": "$pull on a non-array field", + "documents": [{ "_id": 1, "t": 5 }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$pull": { "t": 1 } } } + }, + { + "description": "$unset of a field that is not there", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$unset": { "gone": "" } } } + }, + { + "description": "$rename onto a field that already exists", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$rename": { "a": "t" } } } + }, + { + "description": "$rename of a field that is not there", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$rename": { "gone": "b" } } } + }, + { + "description": "an update that writes nothing still reports a match", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$set": { "a": 1 } } } + } + ] +} diff --git a/tests/spec/operators/sources/numeric.json b/tests/spec/operators/sources/numeric.json new file mode 100644 index 0000000..f53c4fb --- /dev/null +++ b/tests/spec/operators/sources/numeric.json @@ -0,0 +1,122 @@ +{ + "_comment": [ + "Inputs only. Expectations are measured -- see record.js.", + "`$mul`, `$min` and `$max`. The two comparison operators are not numeric", + "at all -- they compare in BSON canonical order, which is why a string", + "field and a number operand have a defined answer -- and the cases here", + "are shaped to say which of the three rules each one follows." + ], + "documents": [{ "_id": 1, "a": 5, "s": "b", "d": 2.5 }], + "cases": [ + { + "description": "$mul multiplies an int by an int", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$mul": { "a": 2 } } } + }, + { + "description": "$mul of a missing field", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$mul": { "gone": 5 } } } + }, + { + "description": "$mul mixes int and double", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$mul": { "d": 2 } } } + }, + { + "description": "$mul by zero", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$mul": { "a": 0 } } } + }, + { + "description": "$mul overflowing an int32", + "documents": [{ "_id": 1, "a": 2000000000 }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$mul": { "a": 2 } } } + }, + { + "description": "$mul of a non-numeric field", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$mul": { "s": 2 } } } + }, + { + "description": "$mul by a non-numeric operand", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$mul": { "a": "x" } } } + }, + { + "description": "$mul down a dotted path", + "documents": [{ "_id": 1, "n": { "a": 3 } }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$mul": { "n.a": 4 } } } + }, + { + "description": "$min writes when the operand is lower", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$min": { "a": 3 } } } + }, + { + "description": "$min leaves the field when the operand is higher", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$min": { "a": 7 } } } + }, + { + "description": "$min of a missing field always writes", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$min": { "gone": 7 } } } + }, + { + "description": "$min against a field of another type", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$min": { "s": 5 } } } + }, + { + "description": "$min compares an int and an equal double", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$min": { "a": 5.0 } } } + }, + { + "description": "$max writes when the operand is higher", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$max": { "a": 7 } } } + }, + { + "description": "$max leaves the field when the operand is lower", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$max": { "a": 3 } } } + }, + { + "description": "$max of a missing field always writes", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$max": { "gone": 7 } } } + }, + { + "description": "$max against a field of another type", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$max": { "s": 5 } } } + }, + { + "description": "$max against a null field", + "documents": [{ "_id": 1, "a": null }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$max": { "a": 1 } } } + }, + { + "description": "$min and $max on the same field in one update", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$min": { "a": 3 }, "$max": { "a": 9 } } } + }, + { + "description": "$min through an array index", + "documents": [{ "_id": 1, "t": [5, 5] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$min": { "t.0": 1 } } } + }, + { + "description": "$mul through a positional segment", + "documents": [{ "_id": 1, "t": [{ "a": 2 }, { "a": 3 }] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$mul": { "t.$[].a": 10 } } } + } + ] +} diff --git a/tests/spec/operators/sources/push-modifiers.json b/tests/spec/operators/sources/push-modifiers.json new file mode 100644 index 0000000..3ae75d2 --- /dev/null +++ b/tests/spec/operators/sources/push-modifiers.json @@ -0,0 +1,122 @@ +{ + "_comment": [ + "Inputs only. Expectations are measured -- see record.js.", + "`$push`'s modifiers: `$each`, `$slice`, `$position`, `$sort`. This server", + "already implements `$push` and `$each` and *silently ignores* the other", + "three -- `{$each: [3, 4], $slice: -3}` appended and did not slice, and", + "answered ok: 1. That is the same class of wrong answer the positional", + "operators were: the client asked for one thing and was told it got it." + ], + "documents": [{ "_id": 1, "t": [1, 2] }], + "cases": [ + { + "description": "$slice keeps the last n", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [3, 4], "$slice": -3 } } } } + }, + { + "description": "$slice keeps the first n", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [3, 4], "$slice": 3 } } } } + }, + { + "description": "$slice of zero empties the array", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [3], "$slice": 0 } } } } + }, + { + "description": "$slice larger than the array keeps all of it", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [3], "$slice": 10 } } } } + }, + { + "description": "$slice with an empty $each truncates without adding", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [], "$slice": 1 } } } } + }, + { + "description": "$position inserts at the front", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [9], "$position": 0 } } } } + }, + { + "description": "$position inserts in the middle", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [9], "$position": 1 } } } } + }, + { + "description": "$position past the end appends", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [9], "$position": 99 } } } } + }, + { + "description": "$position counted from the end", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [9], "$position": -1 } } } } + }, + { + "description": "$sort ascending over scalars", + "documents": [{ "_id": 1, "t": [3, 1] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [2], "$sort": 1 } } } } + }, + { + "description": "$sort descending over scalars", + "documents": [{ "_id": 1, "t": [3, 1] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [2], "$sort": -1 } } } } + }, + { + "description": "$sort on a field of the elements", + "documents": [{ "_id": 1, "t": [{ "a": 3 }, { "a": 1 }] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [{ "a": 2 }], "$sort": { "a": 1 } } } } } + }, + { + "description": "$sort and $slice together", + "documents": [{ "_id": 1, "t": [3, 1] }], + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [2], "$sort": 1, "$slice": 2 } } } } + }, + { + "description": "$position and $slice together", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [9], "$position": 0, "$slice": 2 } } } } + }, + { + "description": "$slice without $each", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$slice": 1 } } } } + }, + { + "description": "$each that is not an array", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": 3 } } } } + }, + { + "description": "$slice that is not a number", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [3], "$slice": "x" } } } } + }, + { + "description": "$position that is not a number", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [3], "$position": "x" } } } } + }, + { + "description": "an unknown modifier beside $each", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "$each": [3], "$bogus": 1 } } } } + }, + { + "description": "a document operand that is not modifiers at all is a value", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "t": { "a": 1 } } } } + }, + { + "description": "$push $each onto a missing field", + "operation": "updateOne", + "arguments": { "filter": {}, "update": { "$push": { "gone": { "$each": [1, 2] } } } } + } + ] +} diff --git a/tests/spec/operators/sources/set-on-insert.json b/tests/spec/operators/sources/set-on-insert.json new file mode 100644 index 0000000..040f450 --- /dev/null +++ b/tests/spec/operators/sources/set-on-insert.json @@ -0,0 +1,99 @@ +{ + "_comment": [ + "Inputs only. Expectations are measured -- see record.js.", + "`$setOnInsert`, the only update operator whose meaning depends on which", + "branch of an upsert ran. Every case here is therefore a pair: the same", + "update against a document that exists and against one that does not." + ], + "documents": [{ "_id": 1, "k": 1, "a": 100 }], + "cases": [ + { + "description": "$setOnInsert writes when the upsert inserts", + "documents": [], + "operation": "updateOne", + "arguments": { + "filter": { "k": 1 }, + "update": { "$setOnInsert": { "a": 1 }, "$set": { "b": 2 } }, + "upsert": true + } + }, + { + "description": "$setOnInsert is ignored when the upsert updates", + "operation": "updateOne", + "arguments": { + "filter": { "k": 1 }, + "update": { "$setOnInsert": { "a": 1 }, "$set": { "b": 2 } }, + "upsert": true + } + }, + { + "description": "$setOnInsert alone on a plain update is a no-op", + "operation": "updateOne", + "arguments": { + "filter": { "k": 1 }, + "update": { "$setOnInsert": { "a": 1 } } + } + }, + { + "description": "$setOnInsert alone on an insert is the whole document", + "documents": [], + "operation": "updateOne", + "arguments": { + "filter": { "k": 7 }, + "update": { "$setOnInsert": { "a": 1 } }, + "upsert": true + } + }, + { + "description": "$setOnInsert writes a dotted path on insert", + "documents": [], + "operation": "updateOne", + "arguments": { + "filter": { "k": 1 }, + "update": { "$setOnInsert": { "x.y": 5 } }, + "upsert": true + } + }, + { + "description": "$setOnInsert may not touch _id", + "documents": [], + "operation": "updateOne", + "arguments": { + "filter": { "k": 1 }, + "update": { "$setOnInsert": { "_id": 9 } }, + "upsert": true + } + }, + { + "description": "$setOnInsert and $set naming the same field on an insert", + "documents": [], + "operation": "updateOne", + "arguments": { + "filter": { "k": 1 }, + "update": { "$setOnInsert": { "a": 1 }, "$set": { "a": 2 } }, + "upsert": true + } + }, + { + "description": "findOneAndUpdate reports the document $setOnInsert built", + "documents": [], + "operation": "findOneAndUpdate", + "arguments": { + "filter": { "k": 3 }, + "update": { "$setOnInsert": { "a": 1 } }, + "upsert": true, + "returnDocument": "after" + } + }, + { + "description": "updateMany with $setOnInsert on an upsert that inserts", + "documents": [], + "operation": "updateMany", + "arguments": { + "filter": { "k": 1 }, + "update": { "$setOnInsert": { "a": 1 } }, + "upsert": true + } + } + ] +} diff --git a/tests/spec/run.js b/tests/spec/run.js index 60e3b38..11c5143 100644 --- a/tests/spec/run.js +++ b/tests/spec/run.js @@ -44,9 +44,9 @@ function opt(name, dflt) { } const VERBOSE = !!opt('verbose', false); // The corpus. Defaults to the pinned crud suite; `--suite-dir` points the same -// runner at another one, which is how `tests/spec/aggregate/` and -// `tests/spec/positional/` are run -- both exist because the pinned suite has -// a hole where a whole feature should be. Sharing +// runner at another one, which is how `tests/spec/aggregate/`, +// `tests/spec/positional/` and `tests/spec/operators/` are run -- each exists +// because the pinned suite has a hole where a whole feature should be. Sharing // the runner rather than writing a second one is the point: the entity model, // the matchers, the skip accounting and `expectEvents` all come for free, and a // second runner would drift from this one exactly where it mattered.