Files
MultiforaDB/tests/spec/indexes/README.md
A.Shakhmatov 879a6bb07b 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.
2026-08-10 22:29:08 +03:00

80 lines
3.3 KiB
Markdown

# The index corpus
M3's last row: partial and hashed indexes. `docs/M3_INDEX_TYPES_DESIGN_REVIEW.md`
measured both against mongod 8.3.7 and found they were not the same kind of
gap — hashed was honestly refused, `partialFilterExpression` was accepted and
ignored, and a *unique* partial index therefore refused inserts mongod
accepts.
It also found that **no test in this repository covered that row, in any
suite**. The pinned corpus is crud and aggregate; `tests/e2e/e2e5.js` and
`e2e6.js` test indexes and write neither a partial nor a hashed spec. So this
directory exists for the same reason `tests/spec/positional/` and
`tests/spec/operators/` do.
## The one rule
**Inputs are authored here; expectations are measured against a real mongod.**
```
tests/spec/indexes/
sources/*.json documents + operations, authored
record.js runs them against mongod, writes the expectations
*.json generated, unified format, do not hand-edit
```
```sh
mongod --port 27099 --dbpath <dir>
node tests/spec/indexes/record.js --mongod-port 27099
node tests/spec/run.js --suite-dir tests/spec/indexes
```
Unlike the other two corpora, a case here is a **sequence**: create an index,
insert against it, read back, list it. An index outlives a `deleteMany`, and
every case is about which indexes exist, so the recorder drops the collection
between cases and walks each case's operations in order — stopping at the
first that throws, which is what a client would see.
## Where it stands
Recorded against mongod 8.3.7, run before any of it was implemented:
```
hashed.json 0 pass 18 fail 0 skip
partial.json 3 pass 21 fail 0 skip
```
Red by construction. The three that pass are the reads a partial index does
not change: this server builds an index over every document, so a query still
finds everything, which is the whole reason the review called the partial gap
smaller than the `arrayFilters` one.
## What recording it settled
Two of the review's own guesses were wrong, which is why it was recorded
rather than reasoned:
| | mongod |
|---|---|
| `$in` in a partial filter | **allowed** — the review listed it with `$ne` and `$regex` |
| same key, different filter, no explicit name | **IndexKeySpecsConflict (86)**, not the 67 the review assumed |
And what it confirmed:
| | mongod |
|---|---|
| a unique partial index | constrains only the documents its filter selects — two `{a: 1, t: false}` are fine, two `{a: 9, t: true}` are E11000 |
| a document *leaving* the filter | frees the value it held for another document to take |
| a document *entering* the filter | must take a value nothing inside it holds, or the update is E11000 |
| `$regex`, `$ne` in a filter | 67 |
| a filter that is not a document | TypeMismatch (14) |
| `sparse` + `partialFilterExpression` | 67 — may not be combined |
| `expireAfterSeconds` + a filter | allowed; `listIndexes` reports the filter *before* the expiry |
| an empty filter | allowed, and reported |
| two hashed components | 31303 |
| `unique` on a hashed index | 16764 |
| an array at a hashed path | 16766, **at insert time**, not at creation |
| a key direction that is not 1, -1 or `"hashed"` | 67 |
| a hashed index beside an ascending one on the same field | both exist |
| a range query or a sort over a hashed field | still correct — the planner declines the index rather than misusing it |