tests/spec: a recorded corpus for partial and hashed indexes
M3's last row, and the first of the four corpora here whose subject nothing in
the repository tested at all: the pinned suite is crud and aggregate, and
`e2e5.js`/`e2e6.js` write neither a partial nor a hashed spec.
42 cases in two files, recorded red at 3/39. The three that pass are the reads
a partial index does not change -- this server indexes every document, so a
query still finds everything, which is exactly why the review called this a
smaller fire than `arrayFilters`.
A case here is a *sequence* rather than one operation: create an index, insert
against it, read back, list it. So the recorder walks a case's operations in
order and stops at the first that throws, which is what a client would see,
and drops the collection between cases because an index outlives a
`deleteMany`.
Two of the design review's own guesses were wrong, which is the argument for
recording rather than reasoning:
- **`$in` in a partial filter is allowed.** The review grouped it with `$ne`
and `$regex`, which are 67.
- **Same key, different filter, no explicit name is IndexKeySpecsConflict
(86)**, not the 67 the review assumed.
What it confirmed, and what the implementation now has to satisfy: a unique
partial index constrains only the documents its filter selects; a document
*leaving* the filter frees the value it held; a document *entering* it must
take a value nothing inside holds, or the update is E11000. `sparse` and
`partialFilterExpression` may not be combined (67). `expireAfterSeconds` and a
filter may, and `listIndexes` reports the filter before the expiry. Two hashed
components is 31303, `unique` on a hashed index is 16764, and an array at a
hashed path is 16766 *at insert time* rather than at creation.
This commit is contained in:
146
tests/spec/indexes/sources/hashed.json
Normal file
146
tests/spec/indexes/sources/hashed.json
Normal file
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"_comment": [
|
||||
"Inputs only. Expectations are measured -- see record.js.",
|
||||
"Hashed indexes. The tree is ordered by a hash, so it answers equality and",
|
||||
"nothing else; every read case here exists to pin that a range query and a",
|
||||
"sort still come back *correct*, because the planner declining to use the",
|
||||
"index is the only thing that makes them so."
|
||||
],
|
||||
"documents": [
|
||||
{ "_id": 1, "a": 1, "s": "x" },
|
||||
{ "_id": 2, "a": 5, "s": "y" },
|
||||
{ "_id": 3, "s": "z" },
|
||||
{ "_id": 4, "a": 5, "s": "x" }
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"description": "a hashed index is created and names itself for the plugin",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "an equality query answers every match",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "find", "arguments": { "filter": { "a": 5 }, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "equality against a value nothing holds",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "find", "arguments": { "filter": { "a": 99 }, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "equality against a missing field",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "find", "arguments": { "filter": { "a": null }, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a range query still answers every match",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "find", "arguments": { "filter": { "a": { "$gte": 5 } }, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a sort on the hashed field is by value, not by hash",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "find", "arguments": { "filter": {}, "sort": { "a": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "equality across numeric types",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "find", "arguments": { "filter": { "a": 5.0 }, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "inserting after the index exists",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "insertOne", "arguments": { "document": { "_id": 5, "a": 5 } } },
|
||||
{ "name": "find", "arguments": { "filter": { "a": 5 }, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "deleting through a hashed index",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "deleteMany", "arguments": { "filter": { "a": 5 } } },
|
||||
{ "name": "find", "arguments": { "filter": {}, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a hashed component beside a range one",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed", "s": 1 } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "two hashed components are refused",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed", "s": "hashed" } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a unique hashed index is refused",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" }, "unique": true } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "an array at the hashed path is refused, at insert time",
|
||||
"documents": [],
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "insertOne", "arguments": { "document": { "_id": 1, "a": [1, 2] } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a key direction that is neither 1, -1 nor hashed",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "bogus" } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a sparse hashed index",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" }, "sparse": true } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "the same hashed index twice is idempotent",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a hashed index beside an ascending one on the same field",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 } } },
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a hashed index is dropped by name",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": "hashed" } } },
|
||||
{ "name": "dropIndex", "arguments": { "name": "a_hashed" } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
193
tests/spec/indexes/sources/partial.json
Normal file
193
tests/spec/indexes/sources/partial.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"_comment": [
|
||||
"Inputs only. Expectations are measured -- see record.js.",
|
||||
"`partialFilterExpression`: which filters may be written, what the index",
|
||||
"then reports about itself, and the one behaviour that made ignoring the",
|
||||
"option a wrong answer rather than a missing feature -- a unique partial",
|
||||
"index constrains only the documents its filter selects."
|
||||
],
|
||||
"documents": [
|
||||
{ "_id": 1, "a": 1, "s": "x" },
|
||||
{ "_id": 2, "a": 5, "s": "y" },
|
||||
{ "_id": 3, "s": "z" },
|
||||
{ "_id": 4, "a": 5, "s": "x" }
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"description": "a partial index is created and reports its filter",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gte": 5 } } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a query the filter covers still answers every match",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gte": 5 } } } },
|
||||
{ "name": "find", "arguments": { "filter": { "a": { "$gte": 5 } }, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a query the filter excludes still answers every match",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gte": 5 } } } },
|
||||
{ "name": "find", "arguments": { "filter": { "a": 1 }, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a query straddling the filter still answers every match",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gte": 5 } } } },
|
||||
{ "name": "find", "arguments": { "filter": { "a": { "$gte": 1 } }, "sort": { "_id": 1 } } },
|
||||
{ "name": "countDocuments", "arguments": { "filter": {} } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a unique partial index accepts duplicates its filter excludes",
|
||||
"documents": [],
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "unique": true, "partialFilterExpression": { "t": true } } },
|
||||
{ "name": "insertMany", "arguments": { "documents": [{ "_id": 1, "a": 1, "t": false }, { "_id": 2, "a": 1, "t": false }] } },
|
||||
{ "name": "find", "arguments": { "filter": {}, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a unique partial index refuses duplicates its filter selects",
|
||||
"documents": [],
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "unique": true, "partialFilterExpression": { "t": true } } },
|
||||
{ "name": "insertMany", "arguments": { "documents": [{ "_id": 1, "a": 9, "t": true }, { "_id": 2, "a": 9, "t": true }] } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a document leaving the filter frees the value it held",
|
||||
"documents": [],
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "unique": true, "partialFilterExpression": { "t": true } } },
|
||||
{ "name": "insertMany", "arguments": { "documents": [{ "_id": 1, "a": 9, "t": true }] } },
|
||||
{ "name": "updateOne", "arguments": { "filter": { "_id": 1 }, "update": { "$set": { "t": false } } } },
|
||||
{ "name": "insertOne", "arguments": { "document": { "_id": 2, "a": 9, "t": true } } },
|
||||
{ "name": "find", "arguments": { "filter": {}, "sort": { "_id": 1 } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a document entering the filter takes the value it names",
|
||||
"documents": [],
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "unique": true, "partialFilterExpression": { "t": true } } },
|
||||
{ "name": "insertMany", "arguments": { "documents": [{ "_id": 1, "a": 9, "t": true }, { "_id": 2, "a": 9, "t": false }] } },
|
||||
{ "name": "updateOne", "arguments": { "filter": { "_id": 2 }, "update": { "$set": { "t": true } } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a filter on $exists",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$exists": true } } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a filter with two predicates",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gt": 0 }, "s": "x" } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a filter with an explicit $and",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "$and": [{ "a": { "$gt": 0 } }] } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a filter on a field the index does not name",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "s": "x" } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a filter using $regex is refused",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "s": { "$regex": "x" } } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a filter using $ne is refused",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$ne": 1 } } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a filter using $in is refused",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$in": [1, 2] } } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a filter that is not a document is refused",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": 1 } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "an empty filter",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": {} } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "sparse and a partial filter may not be combined",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "sparse": true, "partialFilterExpression": { "a": { "$gt": 0 } } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a partial filter with a TTL",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "expireAfterSeconds": 100, "partialFilterExpression": { "a": { "$gt": 0 } } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a partial index on a compound key",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1, "s": 1 }, "partialFilterExpression": { "a": { "$gt": 0 } } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "two indexes on one key differing only in their filter",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "name": "one", "partialFilterExpression": { "a": { "$gt": 0 } } } },
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "name": "two", "partialFilterExpression": { "a": { "$gt": 4 } } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "the same index twice is idempotent",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gt": 0 } } } },
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gt": 0 } } } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "the same name with a different filter conflicts",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gt": 0 } } } },
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "partialFilterExpression": { "a": { "$gt": 4 } } } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "a partial index is dropped by name",
|
||||
"ops": [
|
||||
{ "name": "createIndex", "arguments": { "keys": { "a": 1 }, "name": "pa", "partialFilterExpression": { "a": { "$gt": 0 } } } },
|
||||
{ "name": "dropIndex", "arguments": { "name": "pa" } },
|
||||
{ "name": "listIndexes", "arguments": {} }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user