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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -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 this crash, but the two drops disagree about which lock protects the
|
||||
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
|
||||
whole missing command with no dependencies, and measuring it turned up three
|
||||
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);
|
||||
// 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 matchers, the skip accounting and `expectEvents` all come for free, and a
|
||||
// second runner would drift from this one exactly where it mattered.
|
||||
|
||||
Reference in New Issue
Block a user