tests/spec: a recorded corpus for the positional operators #6
20
PLAN.md
20
PLAN.md
@@ -467,7 +467,7 @@ answers, and the trade is only acceptable because the lie is removed first.
|
|||||||
| M1 | **Cursors + wire polish** | getMore / killCursors / batchSize; server-side cursor state with idle timeout; sessions plumbing (lsid accepted) as drivers send it; hello advertisement updates; **`moreToCome` on requests** (see the bug below); command-monitoring assertions in the spec runner | crud spec suite green; e2e green |
|
| M1 | **Cursors + wire polish** | getMore / killCursors / batchSize; server-side cursor state with idle timeout; sessions plumbing (lsid accepted) as drivers send it; hello advertisement updates; **`moreToCome` on requests** (see the bug below); command-monitoring assertions in the spec runner | crud spec suite green; e2e green |
|
||||||
| M2 | **The `aggregate` command surface** | `$out` and `$merge` (7 of the 13 failures), and refusing every pipeline construct the engine does not implement instead of answering `0` (amendment A6). The other 6 failures are blocked on M2.5, M4 and M8 — see `docs/M2_DESIGN_REVIEW.md` §7 | `aggregate-*.json`: 0 fail among the 7 reachable cases |
|
| M2 | **The `aggregate` command surface** | `$out` and `$merge` (7 of the 13 failures), and refusing every pipeline construct the engine does not implement instead of answering `0` (amendment A6). The other 6 failures are blocked on M2.5, M4 and M8 — see `docs/M2_DESIGN_REVIEW.md` §7 | `aggregate-*.json`: 0 fail among the 7 reachable cases |
|
||||||
| M2.5 | **The aggregation engine** | expression evaluator, per-stage document iterator, the accumulators, `$unwind`; `$lookup`/`$facet` explicitly out of the first cut (amendment A6) | a purpose-built stage corpus, every expectation measured against mongod |
|
| M2.5 | **The aggregation engine** | expression evaluator, per-stage document iterator, the accumulators, `$unwind`; `$lookup`/`$facet` explicitly out of the first cut (amendment A6) | a purpose-built stage corpus, every expectation measured against mongod |
|
||||||
| M3 | **Update operators + index types** | `distinct` (**done**); then $setOnInsert, $addToSet, $mul, $min/$max, $pop, $pullAll, $currentDate, `arrayFilters`, pipeline updates; partial + hashed indexes | remaining crud coverage; e2e3/e2e4 green |
|
| M3 | **Update operators + index types** | `distinct` (**done**); positional paths refused rather than destructive (**done**); then `$`/`$[]`/`$[<ident>]` implemented, $setOnInsert, $addToSet, $mul, $min/$max, $pop, $pullAll, $currentDate, pipeline updates; partial + hashed indexes | `tests/spec/positional/` 0 fail (51 cases, recorded from mongod — the named gate could not see this work, see below); remaining crud coverage; e2e3/e2e4 green |
|
||||||
| M4 | **Sessions + transactions** | logical sessions, snapshot isolation on the mmap engine, write concern at commit | sessions + transactions spec suites green |
|
| M4 | **Sessions + transactions** | logical sessions, snapshot isolation on the mmap engine, write concern at commit | sessions + transactions spec suites green |
|
||||||
| M5 | **Change streams** | change feed + resume tokens (likely log-seq based), getMore integration | change-streams spec suite green |
|
| M5 | **Change streams** | change feed + resume tokens (likely log-seq based), getMore integration | change-streams spec suite green |
|
||||||
| M6 | **Admin/ops commands** | dbStats, collStats, serverStatus, ping, buildInfo, listDatabases filters, dropDatabase durability (log it) | mongosh UX smoke; e2e green |
|
| M6 | **Admin/ops commands** | dbStats, collStats, serverStatus, ping, buildInfo, listDatabases filters, dropDatabase durability (log it) | mongosh UX smoke; e2e green |
|
||||||
@@ -1082,6 +1082,24 @@ has to be its own commit with its own re-recorded scorecard.
|
|||||||
is not excluded by the reasoning above; it holds no collection lock, so it
|
is not excluded by the reasoning above; it holds no collection lock, so it
|
||||||
is not this crash, but the two drops disagree about which lock protects the
|
is not this crash, but the two drops disagree about which lock protects the
|
||||||
namespace and that wants one answer.
|
namespace and that wants one answer.
|
||||||
|
- **M3's gate is `tests/spec/positional/`, not the one named in §3.** The row
|
||||||
|
said "remaining crud coverage; e2e3/e2e4 green". `e2e3` and `e2e4` contain
|
||||||
|
zero positional paths, and the pinned crud corpus covers `$[<identifier>]`
|
||||||
|
only — no `$[]` case, no bare `$` case anywhere in it. Both would have been
|
||||||
|
green through the worst version of the array-destroying bug. So M3 brings
|
||||||
|
its own corpus, built exactly like M2.5's: 51 cases, inputs authored in
|
||||||
|
`sources/`, every expectation recorded from mongod 8.3.7, run through the
|
||||||
|
shared runner with `--suite-dir`.
|
||||||
|
|
||||||
|
It stands at 15 pass / 36 fail against the refusal, which is the intended
|
||||||
|
shape — `expressions.json` was 1/26 before the evaluator and is green now.
|
||||||
|
Recording it settled a dozen things, and the first contradicts what the
|
||||||
|
design review assumed: `y.$[i].c.$[i].d` reusing one identifier at two
|
||||||
|
levels is **accepted**, not a duplicate-identifier error. Also: `$[]` over
|
||||||
|
an empty array is a no-op; `$[]` over an array with a non-document element
|
||||||
|
is error 28 where every other path failure is 2; a positional segment never
|
||||||
|
creates, so a missing or non-array path is an error where `$set: {'a.b': 1}`
|
||||||
|
would construct; and an upsert gets no special case.
|
||||||
- **M3 update operators** — open. `distinct` landed first because it was a
|
- **M3 update operators** — open. `distinct` landed first because it was a
|
||||||
whole missing command with no dependencies, and measuring it turned up three
|
whole missing command with no dependencies, and measuring it turned up three
|
||||||
things worth keeping, none of which are `distinct`'s to fix:
|
things worth keeping, none of which are `distinct`'s to fix:
|
||||||
|
|||||||
119
tests/spec/positional/README.md
Normal file
119
tests/spec/positional/README.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# The positional-update corpus
|
||||||
|
|
||||||
|
MongoDB has three ways to say "descend into this array" in an update path:
|
||||||
|
|
||||||
|
| | since | pinned crud corpus |
|
||||||
|
|---|---|---|
|
||||||
|
| `$[<identifier>]` with `arrayFilters` | 3.6 | 4 files, 14 cases |
|
||||||
|
| `$[]` all-positional | 3.6 | **nothing** |
|
||||||
|
| `$` positional | ancient | **nothing** |
|
||||||
|
|
||||||
|
All three shared one code path in this engine, and that path overwrote the
|
||||||
|
array it was supposed to walk — `y: [{b: 3}, {b: 1}]` became
|
||||||
|
`y: {"$[i]": {"b": 2}}`, every element discarded, `ok: 1`. Only the
|
||||||
|
`$[<identifier>]` third of it was externally visible, so a fix measured
|
||||||
|
against the pinned corpus alone would have gone green while two of the three
|
||||||
|
still destroyed data. That is `docs/M3_ARRAYFILTERS_DESIGN_REVIEW.md` §3, and
|
||||||
|
this directory is its consequence.
|
||||||
|
|
||||||
|
## The one rule
|
||||||
|
|
||||||
|
**Inputs are authored here; expectations are measured against a real mongod.**
|
||||||
|
|
||||||
|
The same rule as `tests/spec/aggregate/`, for the same reason: a corpus we
|
||||||
|
write end to end can encode our own bugs as expectations and will then agree
|
||||||
|
with us forever. It is not a hypothetical risk here — the design review's own
|
||||||
|
guesses about `$[]`, about missing paths and about upserts were all wrong
|
||||||
|
before mongod was asked.
|
||||||
|
|
||||||
|
`sources/*.json` holds documents and operations and nothing else. `record.js`
|
||||||
|
asks mongod 8.3.7 what each one answers.
|
||||||
|
|
||||||
|
## Running it
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node tests/spec/run.js --suite-dir tests/spec/positional
|
||||||
|
```
|
||||||
|
|
||||||
|
The same runner as the crud corpus, pointed elsewhere — the entity model, the
|
||||||
|
matchers, `outcome` verification and the skip accounting come for free.
|
||||||
|
`--scorecard` is refused with `--suite-dir`: `tests/spec/scorecard.txt` is the
|
||||||
|
crud corpus's record and the milestones are compared against it.
|
||||||
|
|
||||||
|
## Re-recording
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mongod --port 27099 --dbpath /tmp/mongo-corpus &
|
||||||
|
node tests/spec/positional/record.js --mongod-port 27099
|
||||||
|
```
|
||||||
|
|
||||||
|
Writes `<name>.json` for every `sources/<name>.json`. The generated files are
|
||||||
|
committed: they *are* the corpus, and regenerating them is how a disagreement
|
||||||
|
with mongod gets re-measured rather than argued about.
|
||||||
|
|
||||||
|
Three things to know when adding cases:
|
||||||
|
|
||||||
|
- **Every case records its `outcome`, including the refusals.** A refusal that
|
||||||
|
left the document mangled looks identical to a clean one in `expectError`
|
||||||
|
alone, and a mangled document is the entire reason this corpus exists.
|
||||||
|
- **Errors record the code, not the message.** Message text is mongod's to
|
||||||
|
change between releases. Several of these messages also embed a
|
||||||
|
shell-syntax rendering of the offending element, which no formatter in this
|
||||||
|
server produces.
|
||||||
|
- **A case needing its own documents reseeds through operations**
|
||||||
|
(`deleteMany` + `insertMany`), not `initialData`: the unified format's
|
||||||
|
`initialData` is per file and the runner seeds it once per test. This keeps
|
||||||
|
one file per operator rather than one file per document shape.
|
||||||
|
|
||||||
|
Every construct here is 3.6 or older, so nothing depends on a server newer
|
||||||
|
than the 4.4 this one reports. A case that did should be left out rather than
|
||||||
|
annotated — recording it from mongod 8.x and judging it against a 4.4 answer
|
||||||
|
measures the version gap, not the engine.
|
||||||
|
|
||||||
|
## Where it stands
|
||||||
|
|
||||||
|
Recorded against mongod 8.3.7, run against the server at the positional
|
||||||
|
refusal:
|
||||||
|
|
||||||
|
```
|
||||||
|
filtered.json 8 pass 19 fail 0 skip
|
||||||
|
all-positional.json 3 pass 9 fail 0 skip
|
||||||
|
first-positional.json 4 pass 8 fail 0 skip
|
||||||
|
```
|
||||||
|
|
||||||
|
Red by construction and by design. The 15 that pass are the refusals where
|
||||||
|
this server's code already matches mongod's; of the 36 that fail, 31 are the
|
||||||
|
constructs themselves answering "not implemented by this server", and 5 are
|
||||||
|
refusals whose code differs — four of them the `arrayFilters` validation this
|
||||||
|
server cannot do yet because it never parses the option.
|
||||||
|
|
||||||
|
That is the intended shape. `tests/spec/aggregate/expressions.json` was
|
||||||
|
recorded at 1 pass / 26 fail before the evaluator existed and is green now;
|
||||||
|
this is the same gate at the same stage.
|
||||||
|
|
||||||
|
## What recording it settled
|
||||||
|
|
||||||
|
None of this is guessable, and the first row contradicts what the review
|
||||||
|
assumed:
|
||||||
|
|
||||||
|
| | mongod |
|
||||||
|
|---|---|
|
||||||
|
| `y.$[i].c.$[i].d`, one identifier reused at two levels | **accepted** — not a duplicate-identifier error |
|
||||||
|
| `$[]` over an empty array | no-op, `modifiedCount: 0` |
|
||||||
|
| `$[]` where one element is not a document | error **28**, where the other path failures are 2 |
|
||||||
|
| `$[]` or `$[i]` where the path is missing, or is not an array | error 2 — a positional segment never *creates* |
|
||||||
|
| an upsert that would insert | error 2; upsert gets no special case |
|
||||||
|
| `$` when the query never touched the array | error 2 |
|
||||||
|
| `$` where several elements match | only the first is written |
|
||||||
|
| `$`, `$[]` or `$[i]` in first position | error 2 |
|
||||||
|
| `$` twice in one path | error 2 |
|
||||||
|
| `arrayFilters` alongside a replacement | ignored, no error |
|
||||||
|
| an array filter the update never uses | error 9 |
|
||||||
|
| a filter with no top-level field, two of them, or a duplicate identifier | error 9 |
|
||||||
|
|
||||||
|
The 2-versus-9 split is *nearly* "9 is the `arrayFilters` array judged on its
|
||||||
|
own, 2 is anything needing the update or the stored document" — and it is not
|
||||||
|
a clean rule: an identifier that is not a lowercase alphanumeric name carries
|
||||||
|
the same `Error parsing array filter` prefix as the 9s and is a 2. Recorded as
|
||||||
|
measured rather than tidied, because deriving the codes from the rule would
|
||||||
|
get that row wrong.
|
||||||
860
tests/spec/positional/all-positional.json
Normal file
860
tests/spec/positional/all-positional.json
Normal file
@@ -0,0 +1,860 @@
|
|||||||
|
{
|
||||||
|
"description": "all-positional",
|
||||||
|
"schemaVersion": "1.4",
|
||||||
|
"createEntities": [
|
||||||
|
{
|
||||||
|
"client": {
|
||||||
|
"id": "client0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"database": {
|
||||||
|
"id": "database0",
|
||||||
|
"client": "client0",
|
||||||
|
"databaseName": "positional-corpus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collection": {
|
||||||
|
"id": "collection0",
|
||||||
|
"database": "database0",
|
||||||
|
"collectionName": "coll"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"initialData": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"description": "every element of the array is written",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"_id": 1
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$[].b": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "nested $[] is a cross-product over both levels",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"_id": 3
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$[].c.$[].d": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"d": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] over an empty array",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"_id": 2
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$[].b": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 0,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$inc through $[] adds to every element",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"_id": 1
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$inc": {
|
||||||
|
"y.$[].b": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 11
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] as the leaf over an array of scalars",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "deleteMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "insertMany",
|
||||||
|
"arguments": {
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$[]": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$unset through $[] removes a field from every element",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "deleteMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "insertMany",
|
||||||
|
"arguments": {
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 1,
|
||||||
|
"e": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 2,
|
||||||
|
"e": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": {
|
||||||
|
"$unset": {
|
||||||
|
"y.$[].e": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] where an element is not a document",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "deleteMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "insertMany",
|
||||||
|
"arguments": {
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
},
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$[].b": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectError": {
|
||||||
|
"isError": true,
|
||||||
|
"errorCode": 28
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
},
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "updateMany with $[] across every document",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"_id": {
|
||||||
|
"$in": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$[].b": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 2,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] where the path does not exist",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "deleteMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "insertMany",
|
||||||
|
"arguments": {
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"z": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$[].b": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectError": {
|
||||||
|
"isError": true,
|
||||||
|
"errorCode": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"z": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] where the path is not an array",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "deleteMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "insertMany",
|
||||||
|
"arguments": {
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$[].b": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectError": {
|
||||||
|
"isError": true,
|
||||||
|
"errorCode": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] in first position is refused",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"_id": 1
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"$[]": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectError": {
|
||||||
|
"isError": true,
|
||||||
|
"errorCode": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] combined with an identifier in one path",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"_id": 3
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$[].c.$[j].d": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"arrayFilters": [
|
||||||
|
{
|
||||||
|
"j.d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"d": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2000
tests/spec/positional/filtered.json
Normal file
2000
tests/spec/positional/filtered.json
Normal file
File diff suppressed because it is too large
Load Diff
850
tests/spec/positional/first-positional.json
Normal file
850
tests/spec/positional/first-positional.json
Normal file
@@ -0,0 +1,850 @@
|
|||||||
|
{
|
||||||
|
"description": "first-positional",
|
||||||
|
"schemaVersion": "1.4",
|
||||||
|
"createEntities": [
|
||||||
|
{
|
||||||
|
"client": {
|
||||||
|
"id": "client0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"database": {
|
||||||
|
"id": "database0",
|
||||||
|
"client": "client0",
|
||||||
|
"databaseName": "positional-corpus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collection": {
|
||||||
|
"id": "collection0",
|
||||||
|
"database": "database0",
|
||||||
|
"collectionName": "coll"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"initialData": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 5,
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"description": "$ writes the element the filter matched",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y.b": 3
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$.b": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 5,
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ writes only the first match when several elements qualify",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "deleteMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "insertMany",
|
||||||
|
"arguments": {
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y.b": 1
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$.b": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ when the filter never touched the array is refused",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"_id": 1
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$.b": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectError": {
|
||||||
|
"isError": true,
|
||||||
|
"errorCode": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 5,
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$inc through $",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y.b": 3
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$inc": {
|
||||||
|
"y.$.b": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 5,
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ as the leaf replaces the matched element",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y.b": 3
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$": {
|
||||||
|
"replaced": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"replaced": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 5,
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ reaching a field below the matched element",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y.b": 5
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$.c": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 5,
|
||||||
|
"c": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ with the filter matching on a different field of the element",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "deleteMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "insertMany",
|
||||||
|
"arguments": {
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"k": "a",
|
||||||
|
"v": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "b",
|
||||||
|
"v": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y.k": "b"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$.v": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"k": "a",
|
||||||
|
"v": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "b",
|
||||||
|
"v": 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "updateMany with $ writes each document's own match",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y.b": 1
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$.b": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 2,
|
||||||
|
"modifiedCount": 2,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 7
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 7
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 5,
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ over an array of scalars",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "deleteMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "insertMany",
|
||||||
|
"arguments": {
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y": 2
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectResult": {
|
||||||
|
"matchedCount": 1,
|
||||||
|
"modifiedCount": 1,
|
||||||
|
"upsertedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
1,
|
||||||
|
9,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ in first position is refused",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y.b": 3
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"$": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectError": {
|
||||||
|
"isError": true,
|
||||||
|
"errorCode": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 5,
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ twice in one path is refused",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y.b": 5
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$.c.$.d": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expectError": {
|
||||||
|
"isError": true,
|
||||||
|
"errorCode": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 2,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"b": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": 3,
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"b": 5,
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ on an upsert that inserts",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "deleteMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "collection0",
|
||||||
|
"name": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {
|
||||||
|
"y.b": 3
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"$set": {
|
||||||
|
"y.$.b": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"upsert": true
|
||||||
|
},
|
||||||
|
"expectError": {
|
||||||
|
"isError": true,
|
||||||
|
"errorCode": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcome": [
|
||||||
|
{
|
||||||
|
"collectionName": "coll",
|
||||||
|
"databaseName": "positional-corpus",
|
||||||
|
"documents": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
200
tests/spec/positional/record.js
Normal file
200
tests/spec/positional/record.js
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
// Record a positional-update corpus by asking a real mongod what the answer is.
|
||||||
|
//
|
||||||
|
// The pinned crud suite tests `$[<identifier>]` in four files and does not
|
||||||
|
// contain a single `$[]` or bare `$` case -- see PLAN amendment on M3 and
|
||||||
|
// `docs/M3_ARRAYFILTERS_DESIGN_REVIEW.md` §3. That gap is not academic: all
|
||||||
|
// three spellings shared one code path, and while it destroyed the array it
|
||||||
|
// walked, only the `$[<identifier>]` third of it was externally visible.
|
||||||
|
//
|
||||||
|
// Same discipline as `tests/spec/aggregate/`: *inputs* are authored in
|
||||||
|
// `sources/`, *expectations* are measured here. A corpus we write end to end
|
||||||
|
// is a corpus that can encode our own bugs as expectations and then agree with
|
||||||
|
// us forever -- and the review's own guesses about `$[]`, missing paths and
|
||||||
|
// upserts were all wrong before mongod was asked.
|
||||||
|
//
|
||||||
|
// 1. author `sources/<name>.json`: documents, and a list of operations
|
||||||
|
// 2. run this against a real mongod
|
||||||
|
// 3. it writes `<name>.json` in the unified format, expectations filled in
|
||||||
|
// 4. `node tests/spec/run.js --suite-dir tests/spec/positional` runs it
|
||||||
|
//
|
||||||
|
// node tests/spec/positional/record.js --mongod-port 27099
|
||||||
|
//
|
||||||
|
// Options:
|
||||||
|
// --mongod-port <n> a running mongod to measure against (default 27099)
|
||||||
|
// --only <name> record just one source file
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
// The same pinned driver `run.js` uses, resolved the same way: there is one
|
||||||
|
// lockfile in this repo and it lives with the e2e suites.
|
||||||
|
const { MongoClient } = require(path.join(__dirname, '..', '..', 'e2e', 'node_modules', 'mongodb'));
|
||||||
|
|
||||||
|
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 = 'positional-corpus';
|
||||||
|
const COLL = 'coll';
|
||||||
|
|
||||||
|
// Every construct here is 3.6 or older (`$` is ancient, `$[]` and
|
||||||
|
// `$[<id>]` are 3.6), so nothing in this corpus depends on a server newer
|
||||||
|
// than the 4.4 this one reports. A case that did should be left out rather
|
||||||
|
// than annotated -- recording it from mongod 8.x and judging it against a 4.4
|
||||||
|
// answer measures the version gap, not the engine.
|
||||||
|
const SCHEMA_VERSION = '1.4';
|
||||||
|
|
||||||
|
/// Which fields of a driver result are recorded, per operation.
|
||||||
|
///
|
||||||
|
/// The whole result object is not recorded: it carries `upsertedId: null` and
|
||||||
|
/// driver-side types that do not survive a JSON round trip, and the pinned
|
||||||
|
/// corpus asserts exactly these three for an update. `findOneAndUpdate`
|
||||||
|
/// returns the document itself.
|
||||||
|
const UPDATE_KEYS = ['matchedCount', 'modifiedCount', 'upsertedCount'];
|
||||||
|
|
||||||
|
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 <dir>\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);
|
||||||
|
fs.writeFileSync(path.join(__dirname, `${name}.json`), JSON.stringify(out, null, 2) + '\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) {
|
||||||
|
// A case may bring its own documents: the shapes that make a missing
|
||||||
|
// path or a non-array path meaningful are not the shapes that make a
|
||||||
|
// filtered update meaningful.
|
||||||
|
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*
|
||||||
|
// rather than `initialData`, because the unified format's
|
||||||
|
// `initialData` is per file and the runner seeds it once per test.
|
||||||
|
// Setup-as-operations is what the format offers for this, and it keeps
|
||||||
|
// one file per operator instead of one file per document shape.
|
||||||
|
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 = 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 a corpus that pinned it would fail for the
|
||||||
|
// wrong reason. Several of these messages embed a shell-syntax
|
||||||
|
// rendering of the offending element, which no formatter here
|
||||||
|
// produces.
|
||||||
|
delete op.expectResult;
|
||||||
|
op.expectError = { isError: true, errorCode: e.code };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recorded for *every* case, including the refusals, and that is the
|
||||||
|
// point of the file. A refusal that left the document mangled would
|
||||||
|
// look identical to a clean one in `expectError` alone -- and a
|
||||||
|
// mangled document is exactly the bug this corpus exists to catch.
|
||||||
|
const after = await coll.find({}, { sort: { _id: 1 } }).toArray();
|
||||||
|
|
||||||
|
tests.push({
|
||||||
|
description: c.description,
|
||||||
|
operations: [...setup, op],
|
||||||
|
outcome: [{ collectionName: COLL, databaseName: DB_NAME, documents: after }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await coll.drop().catch(() => {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
description: name,
|
||||||
|
schemaVersion: SCHEMA_VERSION,
|
||||||
|
// Recorded, not authored. Regenerate with tests/spec/positional/record.js.
|
||||||
|
createEntities: [
|
||||||
|
{ client: { id: 'client0' } },
|
||||||
|
{ database: { id: 'database0', client: 'client0', databaseName: DB_NAME } },
|
||||||
|
{ collection: { id: 'collection0', database: 'database0', collectionName: COLL } },
|
||||||
|
],
|
||||||
|
// Reseeded by the runner before every test. Cases needing a different
|
||||||
|
// shape override it with setup operations, above.
|
||||||
|
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);
|
||||||
|
});
|
||||||
118
tests/spec/positional/sources/all-positional.json
Normal file
118
tests/spec/positional/sources/all-positional.json
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"_comment": [
|
||||||
|
"Inputs only. Expectations are measured -- see record.js.",
|
||||||
|
"`$[]`, the all-positional operator. The pinned crud corpus contains no",
|
||||||
|
"`$[]` case at all, which is half of why this directory exists: before",
|
||||||
|
"the refusal landed, `$[]` overwrote the array it was meant to walk and",
|
||||||
|
"nothing external would have noticed."
|
||||||
|
],
|
||||||
|
"documents": [
|
||||||
|
{ "_id": 1, "y": [{ "b": 3 }, { "b": 1 }] },
|
||||||
|
{ "_id": 2, "y": [] },
|
||||||
|
{ "_id": 3, "y": [{ "c": [{ "d": 1 }, { "d": 2 }] }, { "c": [{ "d": 3 }] }] }
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"description": "every element of the array is written",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 1 },
|
||||||
|
"update": { "$set": { "y.$[].b": 9 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "nested $[] is a cross-product over both levels",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 3 },
|
||||||
|
"update": { "$set": { "y.$[].c.$[].d": 0 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] over an empty array",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 2 },
|
||||||
|
"update": { "$set": { "y.$[].b": 9 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$inc through $[] adds to every element",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 1 },
|
||||||
|
"update": { "$inc": { "y.$[].b": 10 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] as the leaf over an array of scalars",
|
||||||
|
"documents": [{ "_id": 1, "y": [1, 2, 3] }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[]": 0 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$unset through $[] removes a field from every element",
|
||||||
|
"documents": [{ "_id": 1, "y": [{ "b": 1, "e": 1 }, { "b": 2, "e": 2 }] }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$unset": { "y.$[].e": "" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] where an element is not a document",
|
||||||
|
"documents": [{ "_id": 1, "y": [{ "b": 1 }, 7] }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[].b": 9 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "updateMany with $[] across every document",
|
||||||
|
"operation": "updateMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": { "$in": [1, 2] } },
|
||||||
|
"update": { "$set": { "y.$[].b": 9 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] where the path does not exist",
|
||||||
|
"documents": [{ "_id": 1, "z": 1 }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[].b": 9 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] where the path is not an array",
|
||||||
|
"documents": [{ "_id": 1, "y": 5 }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[].b": 9 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] in first position is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 1 },
|
||||||
|
"update": { "$set": { "$[]": 9 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$[] combined with an identifier in one path",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 3 },
|
||||||
|
"update": { "$set": { "y.$[].c.$[j].d": 0 } },
|
||||||
|
"arrayFilters": [{ "j.d": 2 }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
267
tests/spec/positional/sources/filtered.json
Normal file
267
tests/spec/positional/sources/filtered.json
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
{
|
||||||
|
"_comment": [
|
||||||
|
"Inputs only. Expectations are measured -- see record.js.",
|
||||||
|
"`$[<identifier>]` and its arrayFilters. The documents are the pinned",
|
||||||
|
"corpus's own (updateOne-arrayFilters.json) so the two agree on the data",
|
||||||
|
"and a disagreement between them is about behaviour, never about setup.",
|
||||||
|
"Cases that need a different shape carry their own `documents`."
|
||||||
|
],
|
||||||
|
"documents": [
|
||||||
|
{ "_id": 1, "y": [{ "b": 3 }, { "b": 1 }] },
|
||||||
|
{ "_id": 2, "y": [{ "b": 0 }, { "b": 1 }] },
|
||||||
|
{ "_id": 3, "y": [{ "b": 5, "c": [{ "d": 2 }, { "d": 1 }] }] }
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"description": "one element matches the filter",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "no element matches the filter",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 4 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "every element of the array matches",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": { "$gte": 0 } }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "updateMany where no document has a matching element",
|
||||||
|
"operation": "updateMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 4 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "updateMany where one document has a matching element",
|
||||||
|
"operation": "updateMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "updateMany where several documents match",
|
||||||
|
"operation": "updateMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 1 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "two identifiers down a nested path",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 3 },
|
||||||
|
"update": { "$set": { "y.$[i].c.$[j].d": 0 } },
|
||||||
|
"arrayFilters": [{ "i.b": 5 }, { "j.d": 2 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "the outer identifier matches nothing, so the inner never runs",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 3 },
|
||||||
|
"update": { "$set": { "y.$[i].c.$[j].d": 0 } },
|
||||||
|
"arrayFilters": [{ "i.b": 99 }, { "j.d": 2 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "findOneAndUpdate returns the document and reports the array",
|
||||||
|
"operation": "findOneAndUpdate",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 1 },
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }],
|
||||||
|
"returnDocument": "after"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$inc through an identifier adds to the element",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 1 },
|
||||||
|
"update": { "$inc": { "y.$[i].b": 10 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$unset through an identifier removes the element's field",
|
||||||
|
"documents": [{ "_id": 1, "y": [{ "b": 3, "e": 9 }, { "b": 1 }] }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$unset": { "y.$[i].e": "" } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an identifier as the leaf replaces the whole element",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 1 },
|
||||||
|
"update": { "$set": { "y.$[i]": { "replaced": true } } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "a filter reaching a nested field of the element",
|
||||||
|
"documents": [{ "_id": 1, "y": [{ "b": { "q": 1 } }, { "b": { "q": 2 } }] }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b.q": 0 } },
|
||||||
|
"arrayFilters": [{ "i.b.q": 2 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an array of scalars, filtered on the identifier itself",
|
||||||
|
"documents": [{ "_id": 1, "y": [1, 2, 3, 2] }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i]": 9 } },
|
||||||
|
"arrayFilters": [{ "i": 2 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an identifier used twice in one path",
|
||||||
|
"documents": [{ "_id": 1, "y": [{ "c": [{ "d": 1 }, { "d": 2 }] }] }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].c.$[i].d": 0 } },
|
||||||
|
"arrayFilters": [{ "i.d": 1 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an identifier naming no array filter is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[k].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an identifier with no arrayFilters at all is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an array filter the update never uses is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "two array filters with the same identifier are refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }, { "i.b": 1 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an array filter with no top-level field is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an array filter with two top-level fields is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3, "j.b": 1 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an identifier that is not a lowercase alphanumeric name is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[1x].b": 2 } },
|
||||||
|
"arrayFilters": [{ "1x.b": 3 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an identifier in first position is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "$[i]": 2 } },
|
||||||
|
"arrayFilters": [{ "i": 1 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "the path's array does not exist",
|
||||||
|
"documents": [{ "_id": 1, "z": 1 }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "the path names something that is not an array",
|
||||||
|
"documents": [{ "_id": 1, "y": 5 }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": {},
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an upsert gets no special case",
|
||||||
|
"documents": [],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "k": 1 },
|
||||||
|
"update": { "$set": { "y.$[i].b": 2 } },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }],
|
||||||
|
"upsert": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "arrayFilters alongside a replacement",
|
||||||
|
"operation": "replaceOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 1 },
|
||||||
|
"replacement": { "z": 1 },
|
||||||
|
"arrayFilters": [{ "i.b": 3 }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
118
tests/spec/positional/sources/first-positional.json
Normal file
118
tests/spec/positional/sources/first-positional.json
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"_comment": [
|
||||||
|
"Inputs only. Expectations are measured -- see record.js.",
|
||||||
|
"`$`, the original positional operator, which writes the *first* element",
|
||||||
|
"the query matched. It is the one of the three that needs something the",
|
||||||
|
"document alone does not hold -- which element the filter matched -- so",
|
||||||
|
"every case here pairs a filter with an update and the pairing is the",
|
||||||
|
"point. The pinned crud corpus has no `$` case anywhere."
|
||||||
|
],
|
||||||
|
"documents": [
|
||||||
|
{ "_id": 1, "y": [{ "b": 3 }, { "b": 1 }] },
|
||||||
|
{ "_id": 2, "y": [{ "b": 0 }, { "b": 1 }] },
|
||||||
|
{ "_id": 3, "y": [{ "b": 5, "c": [{ "d": 2 }] }] }
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"description": "$ writes the element the filter matched",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y.b": 3 },
|
||||||
|
"update": { "$set": { "y.$.b": 7 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ writes only the first match when several elements qualify",
|
||||||
|
"documents": [{ "_id": 1, "y": [{ "b": 1 }, { "b": 1 }, { "b": 2 }] }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y.b": 1 },
|
||||||
|
"update": { "$set": { "y.$.b": 7 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ when the filter never touched the array is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "_id": 1 },
|
||||||
|
"update": { "$set": { "y.$.b": 7 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$inc through $",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y.b": 3 },
|
||||||
|
"update": { "$inc": { "y.$.b": 10 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ as the leaf replaces the matched element",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y.b": 3 },
|
||||||
|
"update": { "$set": { "y.$": { "replaced": true } } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ reaching a field below the matched element",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y.b": 5 },
|
||||||
|
"update": { "$set": { "y.$.c": [] } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ with the filter matching on a different field of the element",
|
||||||
|
"documents": [{ "_id": 1, "y": [{ "k": "a", "v": 1 }, { "k": "b", "v": 2 }] }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y.k": "b" },
|
||||||
|
"update": { "$set": { "y.$.v": 9 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "updateMany with $ writes each document's own match",
|
||||||
|
"operation": "updateMany",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y.b": 1 },
|
||||||
|
"update": { "$set": { "y.$.b": 7 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ over an array of scalars",
|
||||||
|
"documents": [{ "_id": 1, "y": [1, 2, 3] }],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y": 2 },
|
||||||
|
"update": { "$set": { "y.$": 9 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ in first position is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y.b": 3 },
|
||||||
|
"update": { "$set": { "$": 7 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ twice in one path is refused",
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y.b": 5 },
|
||||||
|
"update": { "$set": { "y.$.c.$.d": 0 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "$ on an upsert that inserts",
|
||||||
|
"documents": [],
|
||||||
|
"operation": "updateOne",
|
||||||
|
"arguments": {
|
||||||
|
"filter": { "y.b": 3 },
|
||||||
|
"update": { "$set": { "y.$.b": 7 } },
|
||||||
|
"upsert": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -44,7 +44,9 @@ function opt(name, dflt) {
|
|||||||
}
|
}
|
||||||
const VERBOSE = !!opt('verbose', false);
|
const VERBOSE = !!opt('verbose', false);
|
||||||
// The corpus. Defaults to the pinned crud suite; `--suite-dir` points the same
|
// The corpus. Defaults to the pinned crud suite; `--suite-dir` points the same
|
||||||
// runner at another one, which is how `tests/spec/aggregate/` is run. Sharing
|
// 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
|
||||||
// the runner rather than writing a second one is the point: the entity model,
|
// 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
|
// the matchers, the skip accounting and `expectEvents` all come for free, and a
|
||||||
// second runner would drift from this one exactly where it mattered.
|
// second runner would drift from this one exactly where it mattered.
|
||||||
|
|||||||
Reference in New Issue
Block a user