tests/spec: a recorded corpus for the positional operators
M3's gate as named -- "remaining crud coverage; e2e3/e2e4 green" -- cannot
see this work. e2e3 and e2e4 contain zero positional paths, and the pinned
crud corpus covers `$[<identifier>]` only: no `$[]` case, no bare `$` case
anywhere in it. Both stayed green through a bug that replaced an array with
`{"$[i]": {...}}` and answered ok: 1. So M3 brings its own corpus, built the
way M2.5's was: inputs authored in `sources/`, every expectation recorded
from mongod 8.3.7, run through the shared runner with `--suite-dir`.
51 cases across the three spellings. It stands at 15 pass / 36 fail against
the refusal, which is the intended shape -- `expressions.json` was recorded
at 1 pass / 26 fail before the evaluator and is green now. The 15 that pass
are refusals where this server's code already matches; of the 36, 31 are the
constructs answering "not implemented" and 5 are refusals whose code
differs, four of them arrayFilters validation this server cannot do because
it never parses the option.
Every case records its `outcome`, refusals included. That is deliberate and
it is the whole point of the file: a refusal that left the document mangled
is indistinguishable from a clean one in `expectError` alone, and a mangled
document is what this corpus exists to catch.
What recording it settled, none of it guessable, the first contradicting
what the design review assumed:
- `y.$[i].c.$[i].d`, one identifier reused at two levels, is **accepted**
-- not a duplicate-identifier error
- `$[]` over an empty array is a no-op with modifiedCount 0
- `$[]` over an array with a non-document element is error 28, where every
other path failure here is 2
- a positional segment never creates: a missing or non-array path is an
error, where `$set: {'a.b': 1}` would construct one
- an upsert gets no special case -- it fails for the same reason
- `$` writes only the first matching element, and is refused when the query
never touched the array
- any of the three in first position is refused, as is `$` twice in a path
- `arrayFilters` alongside a replacement is ignored rather than refused
A case needing its own documents reseeds through operations rather than
`initialData`, because the format's `initialData` is per file and the runner
seeds it once per test. That keeps one file per operator instead of one file
per document shape.
crud scorecard unchanged at 204/87, aggregation corpus still 70/0.
This commit was merged in pull request #6.
This commit is contained in:
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user