indexwright 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -2
- package/README.md +5 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/key.d.ts +30 -2
- package/dist/key.d.ts.map +1 -1
- package/dist/key.js +58 -0
- package/dist/key.js.map +1 -1
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +55 -2
- package/dist/parse.js.map +1 -1
- package/dist/types.d.ts +64 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +9 -15
- package/SPEC.md +0 -683
package/CHANGELOG.md
CHANGED
|
@@ -5,16 +5,63 @@ All notable changes to this project are documented here. The format follows
|
|
|
5
5
|
versioning. Pre-1.0, rule additions and message changes may land in minor releases; the `json`
|
|
6
6
|
output shape is the stable contract.
|
|
7
7
|
|
|
8
|
+
## [0.3.0] — 2026-09-21
|
|
9
|
+
|
|
10
|
+
The other half of the file. `fieldOverrides` — single-field index configuration — was typed
|
|
11
|
+
`unknown[]` and read by nothing; it is now validated to the same depth as `indexes` and reduced to
|
|
12
|
+
a canonical form, so that `@indexwright/record` 0.7.0 can reconcile a declaration against a live
|
|
13
|
+
field listing on the one key the linter will report on. No rule reads it yet, and nothing about the
|
|
14
|
+
linter's output or exit codes changes. The package also moves inside the repository, to
|
|
15
|
+
`packages/indexwright`, without moving anything about what is published.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`fieldOverrides` is validated and has a canonical form** (issues #53 and #54). The file's other
|
|
20
|
+
half was typed `unknown[]` and read by nothing. It is now held to the same depth as `indexes` —
|
|
21
|
+
an entry needs `collectionGroup`, `fieldPath`, and an `indexes` array (empty for an exemption),
|
|
22
|
+
each index exactly one of `order`, `arrayConfig`, and `vectorConfig` (and a `queryScope`, read as
|
|
23
|
+
`COLLECTION` when omitted, as the Firebase CLI does), and `ttl` a boolean when present — and `analyseOverrides` reduces each to an `AnalysedOverride` keyed as
|
|
24
|
+
`<collectionGroup>::<fieldPath>::<queryScope>:<direction>|…`, sorted, with entries alike in scope and
|
|
25
|
+
direction collapsed, as the set it is. No rule reads it yet; the form exists so that `@indexwright/record` can reconcile a
|
|
26
|
+
declaration against a live field listing on the same key the linter will report on, rather than
|
|
27
|
+
on a second model. A file whose `fieldOverrides` cannot be read that way is now malformed, where
|
|
28
|
+
it previously passed unexamined. `fieldDirection` accepts any object carrying the three configs,
|
|
29
|
+
which every `IndexField` still is.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `IndexDocument.fieldOverrides` is typed `FieldOverride[]`, no longer `unknown[]`. A consumer
|
|
34
|
+
that built one by hand with entries of another shape stops compiling.
|
|
35
|
+
- The package now lives at `packages/indexwright` rather than at the repository root, so that
|
|
36
|
+
`@indexwright/record` can depend on it from the working tree instead of from a published release.
|
|
37
|
+
Nothing about the published package moves: same name, same bin, same `exports`, same
|
|
38
|
+
dependency-free guarantee.
|
|
39
|
+
- `SPEC.md` is no longer shipped inside the tarball. It specifies both packages, so it belongs to
|
|
40
|
+
the repository rather than to either one, and `files` cannot reach above a package directory. The
|
|
41
|
+
alternative — copying it in at pack time — runs through `prepublishOnly`, which npm skips entirely
|
|
42
|
+
under `ignore-scripts=true`; that would make the verified tarball and the published one differ in
|
|
43
|
+
exactly the way `verify-package` exists to catch. The spec is linked from the README and is in the
|
|
44
|
+
repository.
|
|
45
|
+
|
|
46
|
+
### Notes
|
|
47
|
+
|
|
48
|
+
- **The override key carries no marker telling it from an index key.** `posts::tags::COLLECTION:…`
|
|
49
|
+
and `posts::COLLECTION::tags:…` are the same shape read by different rules, and a reader that
|
|
50
|
+
does not know which listing a key came from cannot tell them apart by the string alone. Decided
|
|
51
|
+
and left as it is for this release: no rule reports an override key yet and the `json` output
|
|
52
|
+
does not carry one, so adding a marker later would change nothing that is a contract today, while
|
|
53
|
+
adding one now would fix a spelling nothing has needed.
|
|
54
|
+
|
|
8
55
|
## [0.2.0] — 2026-08-10
|
|
9
56
|
|
|
10
57
|
### Added
|
|
11
58
|
|
|
12
|
-
- A query corpus format, specified in [SPEC.md](SPEC.md) §7 and versioned by its own
|
|
59
|
+
- A query corpus format, specified in [SPEC.md](https://github.com/uny/indexwright/blob/main/SPEC.md) §7 and versioned by its own
|
|
13
60
|
`corpusVersion` rather than by any package's release number. It is the contract between capture
|
|
14
61
|
and the coverage check, and the first indexwright artefact meant to be committed and reviewed in
|
|
15
62
|
a diff alongside `firestore.indexes.json`.
|
|
16
63
|
- `@indexwright/record`, a separate package that writes one. See
|
|
17
|
-
[its changelog](packages/record/CHANGELOG.md).
|
|
64
|
+
[its changelog](https://github.com/uny/indexwright/blob/main/packages/record/CHANGELOG.md).
|
|
18
65
|
- `indexwright record` now says that the verb ships in `@indexwright/record` and how to run it,
|
|
19
66
|
instead of reporting an unknown command. The cost of splitting the family into two packages is a
|
|
20
67
|
second package to discover, and "unknown command" reads as "indexwright cannot do this".
|
|
@@ -53,6 +100,7 @@ First release. Static analysis of `firestore.indexes.json`; no network access, n
|
|
|
53
100
|
`__name__` to the same resource.
|
|
54
101
|
- A provisional JavaScript API, so the rules can run without spawning a process.
|
|
55
102
|
|
|
103
|
+
[0.3.0]: https://github.com/uny/indexwright/releases/tag/v0.3.0
|
|
56
104
|
[0.2.0]: https://github.com/uny/indexwright/releases/tag/v0.2.0
|
|
57
105
|
[0.1.1]: https://github.com/uny/indexwright/releases/tag/v0.1.1
|
|
58
106
|
[0.1.0]: https://github.com/uny/indexwright/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ measured your own noise level.
|
|
|
88
88
|
| `quota-headroom` | The declared index count is close to the per-database limit. |
|
|
89
89
|
|
|
90
90
|
Each rule's rationale, its known false positives, and the exact detection condition are in
|
|
91
|
-
[SPEC.md](SPEC.md) §5.
|
|
91
|
+
[SPEC.md](https://github.com/uny/indexwright/blob/main/SPEC.md) §5.
|
|
92
92
|
|
|
93
93
|
### A note on `__name__`
|
|
94
94
|
|
|
@@ -147,7 +147,7 @@ console.log(result.summary.byRule);
|
|
|
147
147
|
no Firestore connection. Answering *is this index needed* takes the queries, which is a separate
|
|
148
148
|
package:
|
|
149
149
|
|
|
150
|
-
- **v0.2 — query capture, shipped.** [`@indexwright/record`](packages/record) runs a test suite
|
|
150
|
+
- **v0.2 — query capture, shipped.** [`@indexwright/record`](https://github.com/uny/indexwright/tree/main/packages/record) runs a test suite
|
|
151
151
|
with `FIRESTORE_EMULATOR_HOST` pointed at a pass-through proxy and records the `StructuredQuery`
|
|
152
152
|
shapes it observes as a corpus. This is language- and framework-independent because it operates
|
|
153
153
|
on the wire protocol rather than on source code.
|
|
@@ -158,7 +158,7 @@ package:
|
|
|
158
158
|
Coverage is bounded by what actually exercises the proxy. A query no test issues is not observed,
|
|
159
159
|
and absence of observation is not evidence that an index is unused. That limit is inherent; the
|
|
160
160
|
narrower ones that are not — the Firebase Web SDK's transport, and snapshot listeners — are
|
|
161
|
-
[named in the spec](SPEC.md) and counted in the corpus rather than passed over.
|
|
161
|
+
[named in the spec](https://github.com/uny/indexwright/blob/main/SPEC.md) and counted in the corpus rather than passed over.
|
|
162
162
|
|
|
163
163
|
## Toward 1.0
|
|
164
164
|
|
|
@@ -170,8 +170,8 @@ Until then the version stays below 1.0 and the rules stay provisional.
|
|
|
170
170
|
|
|
171
171
|
## Development
|
|
172
172
|
|
|
173
|
-
This is an npm workspace holding two packages
|
|
174
|
-
[`@indexwright/record`](packages/record)
|
|
173
|
+
This is an npm workspace holding two packages under `packages/`: `indexwright` and
|
|
174
|
+
[`@indexwright/record`](https://github.com/uny/indexwright/tree/main/packages/record). Both scripts cover both.
|
|
175
175
|
|
|
176
176
|
```bash
|
|
177
177
|
npm install
|
package/dist/index.d.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
export { formatGithub, type GithubOutput } from './format/github.js';
|
|
9
9
|
export { formatJson } from './format/json.js';
|
|
10
10
|
export { formatText } from './format/text.js';
|
|
11
|
-
export { analyse, canonicalFields, fieldDirection, implicitNameDirection, indexKey, NAME_FIELD, } from './key.js';
|
|
11
|
+
export { analyse, analyseOverrides, canonicalFields, canonicalSingleFieldIndexes, fieldDirection, formatSingleFieldIndex, implicitNameDirection, indexKey, NAME_FIELD, overrideKey, type IndexConfig, } from './key.js';
|
|
12
12
|
export { DEFAULT_QUOTA, DEFAULT_QUOTA_THRESHOLD, lintDocuments, lintFiles, lintTexts, type DocumentInput, type LintOptions, type TextInput, } from './lint.js';
|
|
13
13
|
export { MalformedInputError, parseDocument, validateDocument } from './parse.js';
|
|
14
14
|
export { getRule, isRuleId, rules } from './rules/index.js';
|
|
15
15
|
export { RULE_IDS } from './types.js';
|
|
16
|
-
export type { AnalysedIndex, CanonicalField, CompositeIndex, Finding, IndexDocument, IndexField, LintError, LintResult, LintSummary, OutputFormat, Rule, RuleContext, RuleId, RuleOptions, } from './types.js';
|
|
16
|
+
export type { AnalysedIndex, AnalysedOverride, CanonicalField, CanonicalSingleFieldIndex, CompositeIndex, FieldOverride, Finding, IndexDocument, IndexField, LintError, LintResult, LintSummary, OutputFormat, Rule, RuleContext, RuleId, RuleOptions, SingleFieldIndex, } from './types.js';
|
|
17
17
|
export { VERSION } from './version.js';
|
|
18
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACL,OAAO,EACP,eAAe,EACf,cAAc,EACd,qBAAqB,EACrB,QAAQ,EACR,UAAU,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,eAAe,EACf,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,QAAQ,EACR,UAAU,EACV,WAAW,EACX,KAAK,WAAW,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,aAAa,EACb,SAAS,EACT,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,SAAS,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,yBAAyB,EACzB,cAAc,EACd,aAAa,EACb,OAAO,EACP,aAAa,EACb,UAAU,EACV,SAAS,EACT,UAAU,EACV,WAAW,EACX,YAAY,EACZ,IAAI,EACJ,WAAW,EACX,MAAM,EACN,WAAW,EACX,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export { formatGithub } from './format/github.js';
|
|
9
9
|
export { formatJson } from './format/json.js';
|
|
10
10
|
export { formatText } from './format/text.js';
|
|
11
|
-
export { analyse, canonicalFields, fieldDirection, implicitNameDirection, indexKey, NAME_FIELD, } from './key.js';
|
|
11
|
+
export { analyse, analyseOverrides, canonicalFields, canonicalSingleFieldIndexes, fieldDirection, formatSingleFieldIndex, implicitNameDirection, indexKey, NAME_FIELD, overrideKey, } from './key.js';
|
|
12
12
|
export { DEFAULT_QUOTA, DEFAULT_QUOTA_THRESHOLD, lintDocuments, lintFiles, lintTexts, } from './lint.js';
|
|
13
13
|
export { MalformedInputError, parseDocument, validateDocument } from './parse.js';
|
|
14
14
|
export { getRule, isRuleId, rules } from './rules/index.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACL,OAAO,EACP,eAAe,EACf,cAAc,EACd,qBAAqB,EACrB,QAAQ,EACR,UAAU,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,eAAe,EACf,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,QAAQ,EACR,UAAU,EACV,WAAW,GAEZ,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,aAAa,EACb,SAAS,EACT,SAAS,GAIV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAqBtC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/key.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import type { AnalysedIndex, CanonicalField, IndexDocument, IndexField } from './types.js';
|
|
1
|
+
import type { AnalysedIndex, AnalysedOverride, CanonicalField, CanonicalSingleFieldIndex, IndexDocument, IndexField, SingleFieldIndex } from './types.js';
|
|
2
2
|
export declare const NAME_FIELD = "__name__";
|
|
3
|
+
/** The three configs a field can carry, which a composite index's field and a single-field index share. */
|
|
4
|
+
export type IndexConfig = Pick<IndexField, 'order' | 'arrayConfig' | 'vectorConfig'>;
|
|
3
5
|
/**
|
|
4
6
|
* The direction a field contributes to the canonical key.
|
|
5
7
|
*
|
|
6
8
|
* `parse.ts` guarantees exactly one of `order`, `arrayConfig`, and `vectorConfig` is present, so
|
|
7
9
|
* the fallback is unreachable for validated input.
|
|
8
10
|
*/
|
|
9
|
-
export declare function fieldDirection(field:
|
|
11
|
+
export declare function fieldDirection(field: IndexConfig): string;
|
|
10
12
|
/**
|
|
11
13
|
* The direction Firestore would give the document key it appends to this field list.
|
|
12
14
|
*
|
|
@@ -32,4 +34,30 @@ export declare function formatField(field: CanonicalField): string;
|
|
|
32
34
|
export declare function indexKey(collectionGroup: string, queryScope: string, fields: readonly CanonicalField[]): string;
|
|
33
35
|
/** Precompute the canonical form once so every rule shares one interpretation of the document. */
|
|
34
36
|
export declare function analyse(document: IndexDocument): AnalysedIndex[];
|
|
37
|
+
export declare function formatSingleFieldIndex(index: CanonicalSingleFieldIndex): string;
|
|
38
|
+
/**
|
|
39
|
+
* The canonical key of a field override (SPEC §5, *Canonical override key*).
|
|
40
|
+
*
|
|
41
|
+
* The same three-part shape as `indexKey`, with the field path where the query scope sits and the
|
|
42
|
+
* scope inside each entry — a single-field index names one field, so what varies per entry is the
|
|
43
|
+
* scope. `indexes` must already be in canonical order; the key is a rendering, not a sort.
|
|
44
|
+
*/
|
|
45
|
+
export declare function overrideKey(collectionGroup: string, fieldPath: string, indexes: readonly CanonicalSingleFieldIndex[]): string;
|
|
46
|
+
/**
|
|
47
|
+
* Reduce an override's declared set to canonical form.
|
|
48
|
+
*
|
|
49
|
+
* A composite index's fields are a sequence and their order is part of the key; an override's
|
|
50
|
+
* indexes are a set, and Firestore holds at most one single-field index per (scope, direction) of
|
|
51
|
+
* a field. So the entries are sorted, and entries that agree on scope and direction are collapsed
|
|
52
|
+
* to one: two spellings of one configuration reach the same key, while two configurations that
|
|
53
|
+
* differ in any member cannot. The collapse reads only what the key reads — two entries alike in
|
|
54
|
+
* scope and direction but differing in a key the form does not see (`density`, `unique`) collapse
|
|
55
|
+
* too, and `source.indexes` keeps both for a consumer that refuses what the key cannot express.
|
|
56
|
+
*/
|
|
57
|
+
export declare function canonicalSingleFieldIndexes(indexes: readonly SingleFieldIndex[]): CanonicalSingleFieldIndex[];
|
|
58
|
+
/**
|
|
59
|
+
* The counterpart of `analyse` for the document's other half. An absent `fieldOverrides` is an
|
|
60
|
+
* empty set, which is what a hand-written file without the key means.
|
|
61
|
+
*/
|
|
62
|
+
export declare function analyseOverrides(document: IndexDocument): AnalysedOverride[];
|
|
35
63
|
//# sourceMappingURL=key.d.ts.map
|
package/dist/key.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,yBAAyB,EAGzB,aAAa,EACb,UAAU,EACV,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,UAAU,aAAa,CAAC;AAErC,2GAA2G;AAC3G,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,cAAc,CAAC,CAAC;AAErF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAQzD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAM3E;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,+FAA+F;IAC/F,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,eAAe,CAS9E;AAMD,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAEzD;AAED,wBAAgB,QAAQ,CACtB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,cAAc,EAAE,GAChC,MAAM,CAER;AAED,kGAAkG;AAClG,wBAAgB,OAAO,CAAC,QAAQ,EAAE,aAAa,GAAG,aAAa,EAAE,CAEhE;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,yBAAyB,GAAG,MAAM,CAE/E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,yBAAyB,EAAE,GAC5C,MAAM,CAER;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GACnC,yBAAyB,EAAE,CAe7B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAI5E"}
|
package/dist/key.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compareStrings } from './collections.js';
|
|
1
2
|
export const NAME_FIELD = '__name__';
|
|
2
3
|
/**
|
|
3
4
|
* The direction a field contributes to the canonical key.
|
|
@@ -61,6 +62,63 @@ export function indexKey(collectionGroup, queryScope, fields) {
|
|
|
61
62
|
export function analyse(document) {
|
|
62
63
|
return document.indexes.map((source, position) => analyseIndex(source, position));
|
|
63
64
|
}
|
|
65
|
+
export function formatSingleFieldIndex(index) {
|
|
66
|
+
return `${index.queryScope}:${index.direction}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The canonical key of a field override (SPEC §5, *Canonical override key*).
|
|
70
|
+
*
|
|
71
|
+
* The same three-part shape as `indexKey`, with the field path where the query scope sits and the
|
|
72
|
+
* scope inside each entry — a single-field index names one field, so what varies per entry is the
|
|
73
|
+
* scope. `indexes` must already be in canonical order; the key is a rendering, not a sort.
|
|
74
|
+
*/
|
|
75
|
+
export function overrideKey(collectionGroup, fieldPath, indexes) {
|
|
76
|
+
return `${collectionGroup}::${fieldPath}::${indexes.map(formatSingleFieldIndex).join('|')}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Reduce an override's declared set to canonical form.
|
|
80
|
+
*
|
|
81
|
+
* A composite index's fields are a sequence and their order is part of the key; an override's
|
|
82
|
+
* indexes are a set, and Firestore holds at most one single-field index per (scope, direction) of
|
|
83
|
+
* a field. So the entries are sorted, and entries that agree on scope and direction are collapsed
|
|
84
|
+
* to one: two spellings of one configuration reach the same key, while two configurations that
|
|
85
|
+
* differ in any member cannot. The collapse reads only what the key reads — two entries alike in
|
|
86
|
+
* scope and direction but differing in a key the form does not see (`density`, `unique`) collapse
|
|
87
|
+
* too, and `source.indexes` keeps both for a consumer that refuses what the key cannot express.
|
|
88
|
+
*/
|
|
89
|
+
export function canonicalSingleFieldIndexes(indexes) {
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
const canonical = [];
|
|
92
|
+
for (const index of indexes) {
|
|
93
|
+
const entry = { queryScope: index.queryScope, direction: fieldDirection(index) };
|
|
94
|
+
// Not the rendered `scope:direction`, which is not injective — nothing forbids `:` in either
|
|
95
|
+
// part. `JSON.stringify` over the pair is, without an assumption about the vocabulary.
|
|
96
|
+
const identity = JSON.stringify([entry.queryScope, entry.direction]);
|
|
97
|
+
if (seen.has(identity))
|
|
98
|
+
continue;
|
|
99
|
+
seen.add(identity);
|
|
100
|
+
canonical.push(entry);
|
|
101
|
+
}
|
|
102
|
+
return canonical.sort((a, b) => compareStrings(a.queryScope, b.queryScope) || compareStrings(a.direction, b.direction));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The counterpart of `analyse` for the document's other half. An absent `fieldOverrides` is an
|
|
106
|
+
* empty set, which is what a hand-written file without the key means.
|
|
107
|
+
*/
|
|
108
|
+
export function analyseOverrides(document) {
|
|
109
|
+
return (document.fieldOverrides ?? []).map((source, position) => analyseOverride(source, position));
|
|
110
|
+
}
|
|
111
|
+
function analyseOverride(source, position) {
|
|
112
|
+
const indexes = canonicalSingleFieldIndexes(source.indexes);
|
|
113
|
+
return {
|
|
114
|
+
source,
|
|
115
|
+
position,
|
|
116
|
+
collectionGroup: source.collectionGroup,
|
|
117
|
+
fieldPath: source.fieldPath,
|
|
118
|
+
indexes,
|
|
119
|
+
key: overrideKey(source.collectionGroup, source.fieldPath, indexes),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
64
122
|
function analyseIndex(source, position) {
|
|
65
123
|
const { fields, redundantNameDirection } = canonicalFields(source.fields);
|
|
66
124
|
return {
|
package/dist/key.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"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"}
|
package/dist/parse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,aAAa,EAGd,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,SAAkB,IAAI,yBAAyB;CAChD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAUzD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CA4B5D"}
|
package/dist/parse.js
CHANGED
|
@@ -29,7 +29,19 @@ export function validateDocument(raw) {
|
|
|
29
29
|
throw new MalformedInputError('"indexes" must be an array');
|
|
30
30
|
}
|
|
31
31
|
const validated = indexes.map((entry, i) => validateIndex(entry, `indexes[${i}]`));
|
|
32
|
-
|
|
32
|
+
// Optional, because a hand-written file usually has none and the Firebase CLI omits the key when
|
|
33
|
+
// an export has none. Present, it is validated to the same depth as `indexes`: an override this
|
|
34
|
+
// tool cannot read is one it would otherwise pass through *as* read, and the consumer that
|
|
35
|
+
// reconciles it against a live listing would then vouch for a field it never examined (#53).
|
|
36
|
+
const overrides = raw['fieldOverrides'];
|
|
37
|
+
if (overrides === undefined) {
|
|
38
|
+
return { ...raw, indexes: validated };
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(overrides)) {
|
|
41
|
+
throw new MalformedInputError('"fieldOverrides" must be an array');
|
|
42
|
+
}
|
|
43
|
+
const validatedOverrides = overrides.map((entry, i) => validateOverride(entry, `fieldOverrides[${i}]`));
|
|
44
|
+
return { ...raw, indexes: validated, fieldOverrides: validatedOverrides };
|
|
33
45
|
}
|
|
34
46
|
function validateIndex(raw, path) {
|
|
35
47
|
if (!isObject(raw)) {
|
|
@@ -55,6 +67,48 @@ function validateField(raw, path) {
|
|
|
55
67
|
throw new MalformedInputError(`${path}: must be an object`);
|
|
56
68
|
}
|
|
57
69
|
const fieldPath = requireString(raw['fieldPath'], `${path}: "fieldPath"`);
|
|
70
|
+
validateConfig(raw, path);
|
|
71
|
+
return { ...raw, fieldPath };
|
|
72
|
+
}
|
|
73
|
+
function validateOverride(raw, path) {
|
|
74
|
+
if (!isObject(raw)) {
|
|
75
|
+
throw new MalformedInputError(`${path}: must be an object`);
|
|
76
|
+
}
|
|
77
|
+
const collectionGroup = requireString(raw['collectionGroup'], `${path}: "collectionGroup"`);
|
|
78
|
+
const fieldPath = requireString(raw['fieldPath'], `${path}: "fieldPath"`);
|
|
79
|
+
// Empty is not the error it is for a composite index: an override declaring no indexes is an
|
|
80
|
+
// exemption, which is a configuration Firestore holds and an export writes out.
|
|
81
|
+
const indexes = raw['indexes'];
|
|
82
|
+
if (!Array.isArray(indexes)) {
|
|
83
|
+
throw new MalformedInputError(`${path}: "indexes" must be an array`);
|
|
84
|
+
}
|
|
85
|
+
const validatedIndexes = indexes.map((entry, i) => validateSingleFieldIndex(entry, `${path}.indexes[${i}]`));
|
|
86
|
+
// Checked for type only. Whether the field has a TTL policy is not something a canonical form
|
|
87
|
+
// reads, but a value that is not a boolean is not a TTL declaration the Firebase CLI would write.
|
|
88
|
+
if (raw['ttl'] !== undefined && typeof raw['ttl'] !== 'boolean') {
|
|
89
|
+
throw new MalformedInputError(`${path}: "ttl" must be a boolean`);
|
|
90
|
+
}
|
|
91
|
+
return { ...raw, collectionGroup, fieldPath, indexes: validatedIndexes };
|
|
92
|
+
}
|
|
93
|
+
function validateSingleFieldIndex(raw, path) {
|
|
94
|
+
if (!isObject(raw)) {
|
|
95
|
+
throw new MalformedInputError(`${path}: must be an object`);
|
|
96
|
+
}
|
|
97
|
+
// Absent, the scope is `COLLECTION`: the Firebase CLI's validator checks `queryScope` only when
|
|
98
|
+
// it is present and its own exports always write it, so an omission is a hand-written file, and
|
|
99
|
+
// `COLLECTION` is the default the CLI itself supplies when it upgrades an old-format composite
|
|
100
|
+
// index. A present value is still held to a string.
|
|
101
|
+
const queryScope = raw['queryScope'] === undefined
|
|
102
|
+
? 'COLLECTION'
|
|
103
|
+
: requireString(raw['queryScope'], `${path}: "queryScope"`);
|
|
104
|
+
validateConfig(raw, path);
|
|
105
|
+
return { ...raw, queryScope };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The one-of check a composite index's field and a single-field index share: exactly one of
|
|
109
|
+
* `order`, `arrayConfig`, and `vectorConfig`, each of the right type.
|
|
110
|
+
*/
|
|
111
|
+
function validateConfig(raw, path) {
|
|
58
112
|
const configured = ['order', 'arrayConfig', 'vectorConfig'].filter((name) => raw[name] !== undefined);
|
|
59
113
|
if (configured.length === 0) {
|
|
60
114
|
throw new MalformedInputError(`${path}: needs one of "order", "arrayConfig", or "vectorConfig"`);
|
|
@@ -71,7 +125,6 @@ function validateField(raw, path) {
|
|
|
71
125
|
if (raw['vectorConfig'] !== undefined && !isObject(raw['vectorConfig'])) {
|
|
72
126
|
throw new MalformedInputError(`${path}: "vectorConfig" must be an object`);
|
|
73
127
|
}
|
|
74
|
-
return { ...raw, fieldPath };
|
|
75
128
|
}
|
|
76
129
|
function requireString(value, label) {
|
|
77
130
|
if (typeof value !== 'string') {
|
package/dist/parse.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -23,10 +23,46 @@ export interface CompositeIndex {
|
|
|
23
23
|
fields: IndexField[];
|
|
24
24
|
[key: string]: unknown;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* One entry of a field override's `indexes` array: a single-field index.
|
|
28
|
+
*
|
|
29
|
+
* The same three configs as `IndexField`, minus `fieldPath` — the override names the field once,
|
|
30
|
+
* above — plus the `queryScope` a composite index carries at the top level. A single-field index is
|
|
31
|
+
* one field, so the scope is the only thing that varies from entry to entry besides the direction.
|
|
32
|
+
*/
|
|
33
|
+
export interface SingleFieldIndex {
|
|
34
|
+
/**
|
|
35
|
+
* `COLLECTION` or `COLLECTION_GROUP` in practice; not validated against an enumeration. `parse.ts`
|
|
36
|
+
* fills in `COLLECTION` when the declaration omits it, as the Firebase CLI does.
|
|
37
|
+
*/
|
|
38
|
+
queryScope: string;
|
|
39
|
+
order?: string;
|
|
40
|
+
arrayConfig?: string;
|
|
41
|
+
vectorConfig?: Record<string, unknown>;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One entry of the document's `fieldOverrides` array.
|
|
46
|
+
*
|
|
47
|
+
* Firestore indexes every field of every document by default; an override replaces that default
|
|
48
|
+
* for one field of one collection group with the set declared in `indexes`. The set is the *whole*
|
|
49
|
+
* configuration the field ends up with, not a delta — an export materialises the defaults an
|
|
50
|
+
* override keeps — and an empty set is an exemption: the field is not indexed at all.
|
|
51
|
+
*
|
|
52
|
+
* `ttl` is carried and not analysed. It decides when a document is deleted, not which queries are
|
|
53
|
+
* served, so nothing that reads a canonical form has a use for it.
|
|
54
|
+
*/
|
|
55
|
+
export interface FieldOverride {
|
|
56
|
+
collectionGroup: string;
|
|
57
|
+
fieldPath: string;
|
|
58
|
+
indexes: SingleFieldIndex[];
|
|
59
|
+
ttl?: boolean;
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
}
|
|
26
62
|
/** A parsed and validated `firestore.indexes.json`. */
|
|
27
63
|
export interface IndexDocument {
|
|
28
64
|
indexes: CompositeIndex[];
|
|
29
|
-
fieldOverrides?:
|
|
65
|
+
fieldOverrides?: FieldOverride[];
|
|
30
66
|
[key: string]: unknown;
|
|
31
67
|
}
|
|
32
68
|
/** A field reduced to the two things the canonical key is built from. */
|
|
@@ -52,6 +88,33 @@ export interface AnalysedIndex {
|
|
|
52
88
|
*/
|
|
53
89
|
readonly redundantNameDirection: string | null;
|
|
54
90
|
}
|
|
91
|
+
/** A single-field index reduced to the two things its canonical form is built from. */
|
|
92
|
+
export interface CanonicalSingleFieldIndex {
|
|
93
|
+
queryScope: string;
|
|
94
|
+
/** `ASCENDING`, `DESCENDING`, `CONTAINS`, or `VECTOR(<dimension>)`. */
|
|
95
|
+
direction: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A field override with its canonical form precomputed, the counterpart of `AnalysedIndex`.
|
|
99
|
+
*
|
|
100
|
+
* Built by `analyseOverrides` and shared with `@indexwright/record`, which reconciles it against a
|
|
101
|
+
* live field listing; a rule reading `fieldOverrides` reads this rather than the source (issues #53
|
|
102
|
+
* and #54 ask for one model, not two).
|
|
103
|
+
*/
|
|
104
|
+
export interface AnalysedOverride {
|
|
105
|
+
/** The declaration as written, including any keys indexwright does not understand. */
|
|
106
|
+
readonly source: FieldOverride;
|
|
107
|
+
/** Position within the document's `fieldOverrides` array, for messages that need to point at one. */
|
|
108
|
+
readonly position: number;
|
|
109
|
+
readonly collectionGroup: string;
|
|
110
|
+
readonly fieldPath: string;
|
|
111
|
+
/**
|
|
112
|
+
* The declared set, in canonical order: sorted by query scope, then direction, with entries alike
|
|
113
|
+
* in both collapsed to one (`source.indexes` keeps them all). Empty for an exemption.
|
|
114
|
+
*/
|
|
115
|
+
readonly indexes: readonly CanonicalSingleFieldIndex[];
|
|
116
|
+
readonly key: string;
|
|
117
|
+
}
|
|
55
118
|
export declare const RULE_IDS: readonly ["scope-mismatch", "field-order-variant", "explicit-name-field", "quota-headroom"];
|
|
56
119
|
export type RuleId = (typeof RULE_IDS)[number];
|
|
57
120
|
/** A single warning. Never an error: see SPEC §8. */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,8CAA8C;AAC9C,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,mDAAmD;AACnD,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,4FAA4F;IAC5F,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,uDAAuD;AACvD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyHH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,gBAAgB;IAChB,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;CACR,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "indexwright",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Linter for Firestore composite index declarations (firestore.indexes.json)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"firestore",
|
|
@@ -10,18 +10,16 @@
|
|
|
10
10
|
"linter",
|
|
11
11
|
"static-analysis"
|
|
12
12
|
],
|
|
13
|
-
"homepage": "https://github.com/uny/indexwright#readme",
|
|
13
|
+
"homepage": "https://github.com/uny/indexwright/tree/main/packages/indexwright#readme",
|
|
14
14
|
"bugs": "https://github.com/uny/indexwright/issues",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/uny/indexwright.git"
|
|
17
|
+
"url": "git+https://github.com/uny/indexwright.git",
|
|
18
|
+
"directory": "packages/indexwright"
|
|
18
19
|
},
|
|
19
20
|
"license": "Apache-2.0",
|
|
20
21
|
"author": "Yuki Nagai",
|
|
21
22
|
"type": "module",
|
|
22
|
-
"workspaces": [
|
|
23
|
-
"packages/*"
|
|
24
|
-
],
|
|
25
23
|
"bin": {
|
|
26
24
|
"indexwright": "dist/cli.js"
|
|
27
25
|
},
|
|
@@ -36,22 +34,18 @@
|
|
|
36
34
|
"files": [
|
|
37
35
|
"dist",
|
|
38
36
|
"README.md",
|
|
39
|
-
"SPEC.md",
|
|
40
37
|
"CHANGELOG.md",
|
|
41
38
|
"LICENSE"
|
|
42
39
|
],
|
|
43
40
|
"engines": {
|
|
44
41
|
"node": ">=22"
|
|
45
42
|
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
46
|
"scripts": {
|
|
47
|
-
"build": "tsc --build --force
|
|
48
|
-
"typecheck": "tsc --noEmit
|
|
49
|
-
"test": "npm run build && node --test \"test/*.test.js\" \"packages/*/test/*.test.js\"",
|
|
50
|
-
"verify-package": "npm run build && node scripts/verify-package.mjs && node scripts/verify-record-package.mjs",
|
|
47
|
+
"build": "tsc --build --force",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
51
49
|
"prepublishOnly": "npm run build"
|
|
52
|
-
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@types/node": "^22.10.2",
|
|
55
|
-
"typescript": "^5.7.2"
|
|
56
50
|
}
|
|
57
51
|
}
|
package/SPEC.md
DELETED
|
@@ -1,683 +0,0 @@
|
|
|
1
|
-
# indexwright — Specification
|
|
2
|
-
|
|
3
|
-
Linter and query-coverage checker for Firestore composite indexes.
|
|
4
|
-
|
|
5
|
-
Status: **v0.2.0 (draft)**. Pre-1.0: rules and CLI surface may change between minor versions.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. Why this exists
|
|
10
|
-
|
|
11
|
-
Firestore composite indexes are declared as data (`firestore.indexes.json`) and applied as
|
|
12
|
-
whole-state. Three properties of the platform make that declaration hard to get right, and none of
|
|
13
|
-
them are addressed by existing tooling.
|
|
14
|
-
|
|
15
|
-
**The emulator does not enforce composite indexes.** The Firestore emulator "does not track
|
|
16
|
-
composite indexes and instead executes any valid query." A query that will fail in production with
|
|
17
|
-
`FAILED_PRECONDITION` passes locally. Strict indexing in the emulator is not a planned feature.
|
|
18
|
-
|
|
19
|
-
**Firestore exposes no per-index usage metrics.** Index entry reads are billed but do not appear in
|
|
20
|
-
the usage dashboard, and there is no index-level metric. You cannot ask the platform which of your
|
|
21
|
-
indexes are actually used, so you cannot safely identify unused ones.
|
|
22
|
-
|
|
23
|
-
**The index-matching rule is undocumented.** Google's documentation states that field ordering must
|
|
24
|
-
be specified, but does not define how field order maps to query shape — in particular whether the
|
|
25
|
-
relative order of equality and `array-contains` fields before the range fields affects matching.
|
|
26
|
-
This means index changes cannot be validated against documentation; only empirically.
|
|
27
|
-
|
|
28
|
-
The asymmetry is stark: security rules have an official test harness
|
|
29
|
-
(`@firebase/rules-unit-testing`) and can be verified in the emulator. **Composite indexes have no
|
|
30
|
-
equivalent.** A survey of GitHub and npm found no linter for `firestore.indexes.json`; the only
|
|
31
|
-
adjacent projects are three abandoned index *generators* (0–6 stars, last pushed 2021–2025).
|
|
32
|
-
|
|
33
|
-
Consequently, index declarations drift into states that are detectable by inspection but that
|
|
34
|
-
nothing inspects: inconsistent `queryScope` within a collection, near-duplicate indexes that differ
|
|
35
|
-
only in field order, artifacts of round-tripping through live exports, and silent approach to the
|
|
36
|
-
per-database index quota.
|
|
37
|
-
|
|
38
|
-
## 2. What this is not
|
|
39
|
-
|
|
40
|
-
**indexwright cannot determine whether an index is needed.** That question requires the queries,
|
|
41
|
-
which live in application code, not in the index declaration. No rule in this specification asserts
|
|
42
|
-
that an index is unnecessary, and no output should be read as authorising a deletion.
|
|
43
|
-
|
|
44
|
-
Deleting a composite index is not symmetric with adding one: an addition does not disturb existing
|
|
45
|
-
queries while it backfills, but a deletion breaks its queries the moment it takes effect, and
|
|
46
|
-
restoring it requires a fresh backfill. **A linter that only sees the declaration must never be the
|
|
47
|
-
basis for a deletion.**
|
|
48
|
-
|
|
49
|
-
What indexwright asserts is narrower and safer: *these declarations are structurally inconsistent
|
|
50
|
-
with each other, and a human should look.*
|
|
51
|
-
|
|
52
|
-
## 3. Scope
|
|
53
|
-
|
|
54
|
-
### v0.1.0
|
|
55
|
-
|
|
56
|
-
Static analysis of index declaration files. Four rules (§5). No network access, no credentials, no
|
|
57
|
-
Firestore connection.
|
|
58
|
-
|
|
59
|
-
### Planned
|
|
60
|
-
|
|
61
|
-
- **v0.2 — query capture.** The Firestore emulator speaks the public Firestore v1 gRPC API in
|
|
62
|
-
plaintext on a local port. An intercepting proxy can decode `RunQuery` requests and record the
|
|
63
|
-
observed `StructuredQuery` shapes, yielding a query corpus harvested from execution rather than
|
|
64
|
-
hand-written. This is independent of language and framework, because it operates on the wire
|
|
65
|
-
protocol rather than on source code — but not of transport: the emulator also serves the Firebase
|
|
66
|
-
Web SDK over WebChannel rather than gRPC, and a suite driven from a browser does not pass through
|
|
67
|
-
a gRPC proxy at all. v0.2 captures the gRPC transport, which is what server-side SDKs use. The
|
|
68
|
-
corpus format is specified in §7.
|
|
69
|
-
- **v0.3 — coverage check.** Replay a captured corpus against a throwaway Firestore database that
|
|
70
|
-
has the candidate index set applied, and report queries that fail with `FAILED_PRECONDITION`.
|
|
71
|
-
The oracle is Firestore itself; indexwright does not reimplement the undocumented matching rule.
|
|
72
|
-
|
|
73
|
-
The v0.2/v0.3 split is deliberate: capture is cheap and offline, while the coverage decision is
|
|
74
|
-
delegated to the platform. Reimplementing index matching would risk emitting false
|
|
75
|
-
`FAILED_PRECONDITION` verdicts and blocking development on a rule that is not published.
|
|
76
|
-
|
|
77
|
-
**Packaging of v0.2/v0.3.** `record` and `check` run inside an adopter's project, so whatever they
|
|
78
|
-
depend on lands in an adopter's tree, and the build-time carve-out of §8 does not reach them. They
|
|
79
|
-
ship as a separate package, `@indexwright/record`. **`indexwright` itself acquires no runtime
|
|
80
|
-
dependency, in any version.**
|
|
81
|
-
|
|
82
|
-
The cost being separated is `check`'s, not `record`'s. Replay executes captured shapes against a
|
|
83
|
-
real database and reads the status Firestore answers with, which means a Firestore client — the
|
|
84
|
-
thing §8 exists to keep out of a linter that runs on every push, in every CI job, in projects that
|
|
85
|
-
may never touch Firestore from a server. `check` runs where a project is already talking to
|
|
86
|
-
Firestore server-side and has already resolved that client transitively. The dependency is added
|
|
87
|
-
where it is very likely already resolved, and is absent where it would be new.
|
|
88
|
-
|
|
89
|
-
Capture turned out not to need one. An earlier draft of this section justified the split with a
|
|
90
|
-
gRPC stack for `record` too — `@grpc/grpc-js` and protobuf definitions for the Firestore v1 API, on
|
|
91
|
-
the grounds that hand-writing a decoder for a wire format owned by someone else is not a cost worth
|
|
92
|
-
paying. Neither half held. `@grpc/grpc-js` implements clients and servers for services known at
|
|
93
|
-
build time; a transparent proxy is `node:http2` and raw frames, and grpc-js has no part in it. And
|
|
94
|
-
§7 fixes a closed operator vocabulary and requires an unrecognised enum value to be counted rather
|
|
95
|
-
than named, so the enum table has to exist in-tree whichever library reads the bytes — what a
|
|
96
|
-
protobuf runtime would add on top of it is varint and length-delimited parsing over field numbers
|
|
97
|
-
that a released `.proto` cannot renumber. `@indexwright/record` v0.2 therefore declares no runtime
|
|
98
|
-
dependency either, and takes one when `check` arrives.
|
|
99
|
-
|
|
100
|
-
It does not depend on `indexwright` yet. Capture reads no index declarations, so the index model
|
|
101
|
-
and the `json` contract are `check`'s needs rather than `record`'s. Declaring the dependency early
|
|
102
|
-
would put a package in an adopter's tree that nothing imports.
|
|
103
|
-
|
|
104
|
-
The cost is a second package to discover. `indexwright record` in an installation that has only the
|
|
105
|
-
linter must say where the verb lives, not report an unknown command.
|
|
106
|
-
|
|
107
|
-
**Known limit of v0.2/v0.3:** coverage is bounded by what actually exercises the proxy. A query that
|
|
108
|
-
no test issues is not observed, and absence of observation is not evidence that an index is unused.
|
|
109
|
-
That much is inherent. Two further gaps are not, and are implementation gaps v0.2 ships with: a
|
|
110
|
-
suite driven through the Firebase Web SDK does not reach a gRPC proxy at all, and a query issued as
|
|
111
|
-
a snapshot listener travels by `Listen` rather than `RunQuery` and is counted rather than recorded
|
|
112
|
-
(§7). Both narrow what a corpus covers without narrowing what it appears to cover, which is why §7
|
|
113
|
-
counts them out loud.
|
|
114
|
-
|
|
115
|
-
## 4. CLI
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
indexwright lint <file...> [options]
|
|
119
|
-
|
|
120
|
-
Options:
|
|
121
|
-
--format <fmt> text (default) | json | github
|
|
122
|
-
--max-warnings <n> exit 1 if warnings exceed n (default: unlimited → always exit 0)
|
|
123
|
-
--rule <id> run only the given rule; repeatable
|
|
124
|
-
--disable <id> skip the given rule; repeatable
|
|
125
|
-
--quota <n> per-database composite index limit for R4 (default: 1000)
|
|
126
|
-
--quota-threshold <p> warn above this fraction of the limit (default: 0.8)
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Exit codes
|
|
130
|
-
|
|
131
|
-
| Code | Meaning |
|
|
132
|
-
|-----:|:--------|
|
|
133
|
-
| 0 | Completed. Warnings may have been emitted (default policy). |
|
|
134
|
-
| 1 | Warning count exceeded `--max-warnings`. |
|
|
135
|
-
| 2 | Usage error, unreadable file, or malformed input. |
|
|
136
|
-
|
|
137
|
-
**The default is exit 0 even with findings.** A linter whose rules have unmeasured false-positive
|
|
138
|
-
rates must not gate a pipeline by default. Adopters opt into enforcement with `--max-warnings`
|
|
139
|
-
once they have measured their own noise level.
|
|
140
|
-
|
|
141
|
-
### Input
|
|
142
|
-
|
|
143
|
-
Any file in the `firestore.indexes.json` shape:
|
|
144
|
-
|
|
145
|
-
```jsonc
|
|
146
|
-
{
|
|
147
|
-
"indexes": [
|
|
148
|
-
{
|
|
149
|
-
"collectionGroup": "…",
|
|
150
|
-
"queryScope": "COLLECTION" | "COLLECTION_GROUP",
|
|
151
|
-
"fields": [ { "fieldPath": "…", "order": "ASCENDING" | "DESCENDING" }
|
|
152
|
-
| { "fieldPath": "…", "arrayConfig": "CONTAINS" }
|
|
153
|
-
| { "fieldPath": "…", "vectorConfig": { … } } ],
|
|
154
|
-
"density": "…" // optional, passed through
|
|
155
|
-
}
|
|
156
|
-
],
|
|
157
|
-
"fieldOverrides": [ … ] // parsed, not analysed in v0.1.0
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Multiple files may be passed; each is analysed independently. Rules are not applied across files.
|
|
162
|
-
|
|
163
|
-
### Validation
|
|
164
|
-
|
|
165
|
-
A file is **malformed** when it is not valid JSON, when the top level is not an object, when
|
|
166
|
-
`indexes` is absent or is not an array, when an index lacks `collectionGroup`, `queryScope`, or a
|
|
167
|
-
non-empty `fields`, or when a field lacks `fieldPath` or declares none — or more than one — of
|
|
168
|
-
`order`, `arrayConfig`, and `vectorConfig`.
|
|
169
|
-
|
|
170
|
-
Nothing beyond that is refused. The *values* of `queryScope` and `order` are not checked against an
|
|
171
|
-
enumeration, unknown keys anywhere are ignored, and a declaration that repeats a `fieldPath` within
|
|
172
|
-
one index — which does occur in live exports — is carried through rather than rejected. Refusing to
|
|
173
|
-
analyse a file is the harshest outcome available to a tool that otherwise only warns, and it costs
|
|
174
|
-
the reader every other index in the file, so it is reserved for input that cannot be read as an
|
|
175
|
-
index declaration at all.
|
|
176
|
-
|
|
177
|
-
Malformed input does not abort the run. Each file is read and analysed on its own: files that parse
|
|
178
|
-
are linted and their findings reported, files that do not are reported as errors, and the process
|
|
179
|
-
exits 2 once every file has been handled. Reporting only the first bad file would hide findings in
|
|
180
|
-
the files that were fine.
|
|
181
|
-
|
|
182
|
-
## 5. Rules
|
|
183
|
-
|
|
184
|
-
Every rule emits **warnings**, never errors. Each finding carries: rule id, file, a canonical index
|
|
185
|
-
key, and a one-line reason. A finding that concerns the file as a whole rather than any particular
|
|
186
|
-
index carries a **null** key.
|
|
187
|
-
|
|
188
|
-
A rule whose subject is a *set* of indexes emits one grouped finding for that set, not one finding
|
|
189
|
-
per member. The finding's `key` is the lexicographically smallest member key and `related` holds the
|
|
190
|
-
remaining member keys, sorted ascending and de-duplicated. This keeps the warning count proportional
|
|
191
|
-
to the number of problems rather than to the number of indexes, which matters because
|
|
192
|
-
`--max-warnings` is counted in findings.
|
|
193
|
-
|
|
194
|
-
### Canonical index key
|
|
195
|
-
|
|
196
|
-
```
|
|
197
|
-
<collectionGroup>::<queryScope>::<fieldPath>:<direction>|<fieldPath>:<direction>|…
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
where `direction` is `ASCENDING`, `DESCENDING`, `CONTAINS`, or — for a `vectorConfig` field —
|
|
201
|
-
`VECTOR(<dimension>)`, written `VECTOR(?)` when no dimension is declared. A trailing implicit
|
|
202
|
-
`__name__` entry is stripped before forming the key, so that declarations that differ only in
|
|
203
|
-
whether the document key is written explicitly resolve to the same resource.
|
|
204
|
-
|
|
205
|
-
### The implicit `__name__` direction
|
|
206
|
-
|
|
207
|
-
Firestore appends the document key to every composite index, and its direction is not declared, so
|
|
208
|
-
stripping it requires knowing which direction it would have had. indexwright defines that direction
|
|
209
|
-
as **the `order` of the last preceding field that carries an `order`, or `ASCENDING` when no
|
|
210
|
-
preceding field carries one** — the latter arising when the index ends with an `arrayConfig` field.
|
|
211
|
-
|
|
212
|
-
This is not published behaviour. It is read off live exports, which render the key explicitly:
|
|
213
|
-
`[type ASC, createdAt DESC, __name__ DESC]`, `[public ASC, startAt ASC, __name__ ASC]`, and
|
|
214
|
-
`[isRecommended ASC, tags CONTAINS, __name__ ASC]` are all shapes an export produces.
|
|
215
|
-
|
|
216
|
-
The definition is used only to decide whether a written `__name__` is redundant, and the decision is
|
|
217
|
-
deliberately one-sided: a trailing `__name__` is stripped **only** when its direction equals the
|
|
218
|
-
value above, and one that differs — `[totalNbUses DESC, __name__ ASC]` occurs in real exports — is
|
|
219
|
-
treated as meaningful and kept. A wrong definition can therefore fail to merge two spellings of one
|
|
220
|
-
index, but can never merge two indexes that are actually distinct.
|
|
221
|
-
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
### R1 · `scope-mismatch`
|
|
225
|
-
|
|
226
|
-
**Detects.** Within one `collectionGroup`, more than one distinct `queryScope` is declared. One
|
|
227
|
-
finding is emitted per affected `collectionGroup`, reporting how many indexes each scope holds.
|
|
228
|
-
|
|
229
|
-
The scope with the fewest indexes is named as the minority, and the finding's keys are that scope's
|
|
230
|
-
indexes. When no scope holds a strict minority — an even split — the finding says so instead of
|
|
231
|
-
naming one, and reports the keys of the lexicographically first scope. It still fires: the detection
|
|
232
|
-
condition is that the scopes disagree, and an even split is a disagreement. Suppressing it would
|
|
233
|
-
narrow the rule to a shape the rule does not claim.
|
|
234
|
-
|
|
235
|
-
**Rationale.** A `COLLECTION`-scoped index does not serve a collection-group query, and vice versa.
|
|
236
|
-
When every other index on a collection uses one scope and a newly added one uses the other, the new
|
|
237
|
-
declaration is frequently a mistake that will surface only as a production `FAILED_PRECONDITION`.
|
|
238
|
-
|
|
239
|
-
**False positives.** Legitimate when an application queries the same collection both as a single
|
|
240
|
-
collection and as a collection group. This is why the rule warns rather than fails.
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
### R2 · `field-order-variant`
|
|
245
|
-
|
|
246
|
-
**Detects.** Two or more indexes that share `collectionGroup`, `queryScope`, and the same *set* of
|
|
247
|
-
`fieldPath:direction` pairs, but declare them in different orders.
|
|
248
|
-
|
|
249
|
-
"Same set" means the same *multiset*: a `fieldPath` that repeats within one index is unusual but not
|
|
250
|
-
rejected (§4), and comparing multisets keeps the grouping well defined when it does. The comparison
|
|
251
|
-
runs on the canonicalised field list, so an index that writes `__name__` explicitly groups with one
|
|
252
|
-
that does not. Two indexes that are byte-identical do not constitute different orders and do not
|
|
253
|
-
fire this rule.
|
|
254
|
-
|
|
255
|
-
**Rationale.** Firestore treats a different field order as a different index. Each variant consumes
|
|
256
|
-
write amplification, storage, and quota independently. Variants proliferate easily: a query change
|
|
257
|
-
that reorders fields adds a new index without removing the old one, and round-tripping through a
|
|
258
|
-
live export can reintroduce an ordering that was intended to be replaced.
|
|
259
|
-
|
|
260
|
-
**False positives.** Legitimate when distinct queries genuinely require distinct orderings — for
|
|
261
|
-
example, two queries that order by the same two fields in opposite directions. The finding asks for
|
|
262
|
-
that justification to be recorded, not for one variant to be removed.
|
|
263
|
-
|
|
264
|
-
**Output.** All members of the variant group are listed together, so the reader can judge the set
|
|
265
|
-
rather than one member at a time.
|
|
266
|
-
|
|
267
|
-
---
|
|
268
|
-
|
|
269
|
-
### R3 · `explicit-name-field`
|
|
270
|
-
|
|
271
|
-
**Detects.** An index whose **last** `fields` entry is an explicit `__name__` carrying the implicit
|
|
272
|
-
default direction (§5, *The implicit `__name__` direction*). A `__name__` written anywhere other
|
|
273
|
-
than last, or written last with a direction other than the default, is not flagged.
|
|
274
|
-
|
|
275
|
-
**Rationale.** Firestore appends the document key implicitly; live exports render it explicitly.
|
|
276
|
-
An explicit `__name__` in a hand-maintained declaration is therefore a signature of a value that
|
|
277
|
-
round-tripped through an export rather than being authored directly. It does not change the
|
|
278
|
-
resource identity, but it makes the file inconsistent and can mask genuine duplicates from
|
|
279
|
-
naive text comparison.
|
|
280
|
-
|
|
281
|
-
**False positives.** Explicit `__name__` with a direction that differs from the implicit default is
|
|
282
|
-
meaningful and should not be flagged. The rule only fires on a trailing `__name__` matching the
|
|
283
|
-
implicit default.
|
|
284
|
-
|
|
285
|
-
---
|
|
286
|
-
|
|
287
|
-
### R4 · `quota-headroom`
|
|
288
|
-
|
|
289
|
-
**Detects.** The number of composite indexes in the file exceeds `--quota-threshold` of `--quota`.
|
|
290
|
-
|
|
291
|
-
**Rationale.** Composite indexes are capped per database. The limit is reached gradually and
|
|
292
|
-
silently; the first symptom is a failed index creation at deploy time, which is a poor moment to
|
|
293
|
-
discover it. Reporting headroom continuously makes the trend visible.
|
|
294
|
-
|
|
295
|
-
The comparison is a strict `count > quota × threshold`, so the defaults (1000, 0.8) fire at 801.
|
|
296
|
-
|
|
297
|
-
**False positives.** None in principle; the threshold is configurable because appropriate headroom
|
|
298
|
-
depends on the rate of index growth.
|
|
299
|
-
|
|
300
|
-
**Output.** The finding is about the file, not about any one index, so its `key` is `null` and its
|
|
301
|
-
`related` is empty. One finding per file at most.
|
|
302
|
-
|
|
303
|
-
## 6. Output formats
|
|
304
|
-
|
|
305
|
-
**`text`** — human-readable, grouped by rule, intended for a terminal.
|
|
306
|
-
|
|
307
|
-
**`json`** — a stable machine-readable shape:
|
|
308
|
-
|
|
309
|
-
```jsonc
|
|
310
|
-
{
|
|
311
|
-
"version": "0.1.0",
|
|
312
|
-
"files": ["…"],
|
|
313
|
-
"summary": { "warnings": 0, "errors": 0, "byRule": { "scope-mismatch": 0, … } },
|
|
314
|
-
"findings": [
|
|
315
|
-
{
|
|
316
|
-
"rule": "scope-mismatch",
|
|
317
|
-
"file": "…",
|
|
318
|
-
"key": "…", // null for a finding about the file as a whole
|
|
319
|
-
"message": "…",
|
|
320
|
-
"related": ["…"] // other keys in the same finding group; [] when there are none
|
|
321
|
-
}
|
|
322
|
-
],
|
|
323
|
-
"errors": [
|
|
324
|
-
{ "file": "…", "message": "…" } // files that could not be read or parsed
|
|
325
|
-
]
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
`files` lists every file the run was given, whether or not it parsed, sorted by path. `byRule` holds
|
|
330
|
-
one entry for every rule that ran after `--rule` and `--disable` were applied, including rules that
|
|
331
|
-
found nothing; a rule that did not run is absent rather than zero. `findings` is sorted by file,
|
|
332
|
-
then by rule in the order of §5, then by key with a null key sorting first. `related` is sorted
|
|
333
|
-
ascending. `errors` is sorted by file. Every field is always present: `related` is `[]` rather than
|
|
334
|
-
omitted, and `errors` is `[]` on a clean run.
|
|
335
|
-
|
|
336
|
-
**`github`** — GitHub Actions workflow commands (`::warning file=…::`) plus a Markdown summary
|
|
337
|
-
suitable for `$GITHUB_STEP_SUMMARY`.
|
|
338
|
-
|
|
339
|
-
## 7. Query corpus (v0.2)
|
|
340
|
-
|
|
341
|
-
`record` writes a **query corpus**: the set of distinct query shapes observed on the emulator's wire
|
|
342
|
-
during a run, conventionally `firestore.queries.json`. The corpus is the contract between capture
|
|
343
|
-
(v0.2) and the coverage check (v0.3), and it is meant to be committed alongside
|
|
344
|
-
`firestore.indexes.json` and reviewed in a diff like any other declaration.
|
|
345
|
-
|
|
346
|
-
A query enters the corpus when its request is observed, whatever the server answers next. One that
|
|
347
|
-
failed still describes something the application issues — and a query that failed *because an index
|
|
348
|
-
was missing* is precisely the case v0.3 exists to find, so waiting for a successful status would
|
|
349
|
-
drop the most interesting entries in the file.
|
|
350
|
-
|
|
351
|
-
`record` writes the corpus whole, replacing whatever was there, and writes it atomically so that an
|
|
352
|
-
interrupted run leaves the previous file intact rather than a truncated one. Replacing rather than
|
|
353
|
-
merging is the honest reading: a corpus is evidence of what one run exercised, and a run that
|
|
354
|
-
executed three tests did not exercise the other two hundred. The cost is that a filtered or aborted
|
|
355
|
-
run produces a diff that deletes query shapes which still exist — which is a visible deletion a
|
|
356
|
-
reviewer can question, where a merge would have produced a file that silently outlives the tests
|
|
357
|
-
that justified it.
|
|
358
|
-
|
|
359
|
-
### What a shape is
|
|
360
|
-
|
|
361
|
-
A shape is the part of a `StructuredQuery` that determines which index can serve it: the collection,
|
|
362
|
-
the query scope, the filter tree, and the sort order. Everything else is discarded.
|
|
363
|
-
|
|
364
|
-
**Values are not recorded.** A query's values are customer data, and the corpus is a file that
|
|
365
|
-
persists in a repository. They are also unnecessary: index selection is a function of field paths,
|
|
366
|
-
operators, and directions, not of what is compared against. Discarding them removes the part of a
|
|
367
|
-
captured query most likely to carry a secret, and the only part that changes between runs of the
|
|
368
|
-
same test.
|
|
369
|
-
|
|
370
|
-
It does not make the corpus non-sensitive, and nothing here should be read as saying so. Field paths
|
|
371
|
-
are recorded verbatim, and a field path is not always static: `members.alice@example.com` is an
|
|
372
|
-
ordinary way to query a map, and it is at once a real identifier and a corpus entry that will never
|
|
373
|
-
recur. Collection ids carry schema and sometimes tenancy. A corpus describes an application's data
|
|
374
|
-
model, and it earns the access controls of the repository it is committed to — it does not become
|
|
375
|
-
publishable by having had its values stripped.
|
|
376
|
-
|
|
377
|
-
**Project and database are not recorded.** They are properties of the environment the capture ran
|
|
378
|
-
in, not of the query, and recording them would make a corpus captured against one emulator instance
|
|
379
|
-
look different from the same corpus captured against another.
|
|
380
|
-
|
|
381
|
-
**`limit`, `offset`, cursors, and `select` are not recorded.** None of them changes which index
|
|
382
|
-
serves the query; a projection is served by the index the underlying query already needs.
|
|
383
|
-
|
|
384
|
-
**Occurrence counts are not recorded.** A corpus is a set, not a histogram. A count changes on every
|
|
385
|
-
run without changing anything about what must be indexed, which would make the file churn in every
|
|
386
|
-
diff and train reviewers to skim it. `record` reports counts on stderr, where they are useful for
|
|
387
|
-
triage and where they do not have to be committed.
|
|
388
|
-
|
|
389
|
-
### Vocabulary
|
|
390
|
-
|
|
391
|
-
The corpus reuses the declaration vocabulary of §5, so that a corpus entry and an index key can be
|
|
392
|
-
read against each other with one translation, which the table below states in full: an
|
|
393
|
-
`ARRAY_CONTAINS` filter is served by a field declared `arrayConfig: CONTAINS`.
|
|
394
|
-
|
|
395
|
-
| Corpus | Source on the wire | Aligns with |
|
|
396
|
-
|:--|:--|:--|
|
|
397
|
-
| `collectionGroup` | `CollectionSelector.collection_id` | index `collectionGroup` |
|
|
398
|
-
| `queryScope` — `COLLECTION` \| `COLLECTION_GROUP` | `all_descendants` | index `queryScope` |
|
|
399
|
-
| `direction` — `ASCENDING` \| `DESCENDING` | `Order.direction` | field `order` |
|
|
400
|
-
| `op` — `ARRAY_CONTAINS` | `FieldFilter.Operator` | field `arrayConfig: CONTAINS` |
|
|
401
|
-
|
|
402
|
-
Operators are written with their protobuf enum names: `LESS_THAN`, `LESS_THAN_OR_EQUAL`,
|
|
403
|
-
`GREATER_THAN`, `GREATER_THAN_OR_EQUAL`, `EQUAL`, `NOT_EQUAL`, `ARRAY_CONTAINS`, `IN`,
|
|
404
|
-
`ARRAY_CONTAINS_ANY`, `NOT_IN` for a field filter; `IS_NAN`, `IS_NULL`, `IS_NOT_NAN`, `IS_NOT_NULL`
|
|
405
|
-
for a unary filter; `AND` and `OR` for a composite. Enum names rather than SDK spellings, for the
|
|
406
|
-
same reason §5 writes `ASCENDING` rather than `asc`: the file speaks Firestore's own vocabulary, so
|
|
407
|
-
an entry can be read against a wire capture without a lookup table in between. The list is closed —
|
|
408
|
-
an operator outside it is not written into the corpus under a name of indexwright's invention, and
|
|
409
|
-
protobuf would hand it over as a bare number anyway. It is counted instead, as below.
|
|
410
|
-
|
|
411
|
-
**A published default is applied; an unpublished one is not guessed at.** Every one of these enums
|
|
412
|
-
has a zero value, and the two cases are not alike. `Order.direction` is documented to default to
|
|
413
|
-
`ASCENDING`, so an `Order` that arrives as `DIRECTION_UNSPECIFIED` is recorded as `ASCENDING` —
|
|
414
|
-
that is Firestore's own statement of what the value means, not indexwright's guess, and skipping the
|
|
415
|
-
query instead would drop a real index requirement on the strength of a field the client simply left
|
|
416
|
-
at its default. `OPERATOR_UNSPECIFIED`, an operator added after this specification was written, and
|
|
417
|
-
any other value the vocabulary above cannot name have no such published meaning: the query is
|
|
418
|
-
skipped and counted rather than guessed at. `StructuredQuery.from` is a repeated field, and the same
|
|
419
|
-
line applies — an entry holds exactly one `collectionGroup`, so a query that does not carry exactly
|
|
420
|
-
one selector with a non-empty `collection_id` is skipped, because the corpus has no way to say what
|
|
421
|
-
it means rather than because its meaning is unknown.
|
|
422
|
-
|
|
423
|
-
### Canonical query key
|
|
424
|
-
|
|
425
|
-
```
|
|
426
|
-
<collectionGroup>::<queryScope>::<where>::<orderBy>
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
`<where>` serialises the filter tree: a field or unary filter as `<fieldPath>:<op>`, a composite as
|
|
430
|
-
`<op>(<child>|<child>|…)`. The root is always a composite — `StructuredQuery.where` may be a bare
|
|
431
|
-
field or unary filter, and is then wrapped in an `AND` — so a query with one filter serialises as
|
|
432
|
-
`AND(status:EQUAL)` and a query with none as `AND()`. A root that is already a composite keeps its
|
|
433
|
-
own operator: a top-level `OR` is `OR(…)`, never `AND(OR(…))`. `<orderBy>` is
|
|
434
|
-
`<fieldPath>:<direction>` joined by `|`, and is empty when the query declares no sort order. A
|
|
435
|
-
two-filter, two-sort query keys as:
|
|
436
|
-
|
|
437
|
-
```
|
|
438
|
-
orders::COLLECTION::AND(amount:GREATER_THAN|status:EQUAL)::amount:DESCENDING|createdAt:ASCENDING
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
`<collectionGroup>` and `<fieldPath>` are escaped before they are joined: a backslash becomes `\\`,
|
|
442
|
-
each of `:`, `|`, `(`, `)` is prefixed with one, and every character below `U+0020` becomes `\uXXXX`.
|
|
443
|
-
Enum names need no escaping but are subject to the same rule, so that decoding is uniform. The
|
|
444
|
-
control-character clause is not about ambiguity but about §6's requirement that a value read off an
|
|
445
|
-
untrusted input cannot forge a line of output: a key holding a raw newline would end the line it was
|
|
446
|
-
printed on.
|
|
447
|
-
|
|
448
|
-
Escaping is what makes the key injective, and the key is what entries are de-duplicated on. A field
|
|
449
|
-
path may hold any of these characters — Firestore quotes a path in backticks precisely so that it
|
|
450
|
-
can — and unescaped, one path spelled `` `a:EQUAL|b` `` serialises identically to two filters on `a`
|
|
451
|
-
and `b`. Two distinct queries would then share an entry and only one of them would ever be replayed,
|
|
452
|
-
which is the failure this section closes on: not a gap in coverage but a gap that looks like
|
|
453
|
-
coverage.
|
|
454
|
-
|
|
455
|
-
Both `AND` and `OR` are commutative, so a composite's children are sorted by their own serialised
|
|
456
|
-
form, compared by Unicode code point, before joining. This makes the key independent of the order
|
|
457
|
-
the filters were written in, which is what allows two spellings of one query to collapse into one
|
|
458
|
-
corpus entry. A composite child carrying the same operator as its parent is flattened into it, since
|
|
459
|
-
`AND(a|AND(b|c))` and `AND(a|b|c)` are one query; nesting under a *different* operator is meaningful
|
|
460
|
-
and is kept. Sort order is *not* commutative and is preserved as sent.
|
|
461
|
-
|
|
462
|
-
Repeated children are kept rather than de-duplicated, for the reason §5 compares multisets:
|
|
463
|
-
`tier == "a" OR tier == "b"` is a two-disjunct query whose shape is `OR(tier:EQUAL|tier:EQUAL)`, and
|
|
464
|
-
collapsing it to one disjunct would describe a query that was never issued.
|
|
465
|
-
|
|
466
|
-
### File shape
|
|
467
|
-
|
|
468
|
-
```jsonc
|
|
469
|
-
{
|
|
470
|
-
"corpusVersion": 1,
|
|
471
|
-
"queries": [
|
|
472
|
-
{
|
|
473
|
-
"key": "orders::COLLECTION::AND(status:EQUAL)::createdAt:DESCENDING",
|
|
474
|
-
"collectionGroup": "orders",
|
|
475
|
-
"queryScope": "COLLECTION",
|
|
476
|
-
"where": {
|
|
477
|
-
"op": "AND",
|
|
478
|
-
"filters": [
|
|
479
|
-
{ "fieldPath": "status", "op": "EQUAL" }
|
|
480
|
-
]
|
|
481
|
-
},
|
|
482
|
-
"orderBy": [
|
|
483
|
-
{ "fieldPath": "createdAt", "direction": "DESCENDING" }
|
|
484
|
-
]
|
|
485
|
-
}
|
|
486
|
-
],
|
|
487
|
-
"skipped": ["aggregation-query"]
|
|
488
|
-
}
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
A node carrying `filters` is a composite; a node carrying `fieldPath` is a leaf; no node carries
|
|
492
|
-
both, and none carries neither. Every field is always present, as in §6: `where` is a composite even
|
|
493
|
-
when the query filtered nothing, `orderBy` is `[]` rather than omitted, and `skipped` is `[]` on a
|
|
494
|
-
run that discarded nothing. `queries` is sorted by `key` and `skipped` holds the distinct reasons
|
|
495
|
-
observed, sorted ascending — a set, so that it is as diff-stable as the rest of the file. Both sorts
|
|
496
|
-
compare by Unicode code point. Counts for each reason go to stderr.
|
|
497
|
-
|
|
498
|
-
The stored `where` is the normalised tree the key was computed from, not the tree as it arrived:
|
|
499
|
-
children sorted, same-operator composites flattened, exactly as *Canonical query key* describes. Two
|
|
500
|
-
recorders that observe the same query in different spellings therefore write the same bytes, which
|
|
501
|
-
they would not if the key were normalised and the tree left as sent — the entry that happened to
|
|
502
|
-
win de-duplication would decide the file.
|
|
503
|
-
|
|
504
|
-
`key` is derived from the four fields beneath it and carries nothing they do not: it is there so
|
|
505
|
-
that entries can be sorted, de-duplicated, and cited in a review without re-serialising the tree. It
|
|
506
|
-
is unique within `queries`. A reader handed an entry that breaks any of this — a `key` disagreeing
|
|
507
|
-
with its own tree, a node that is both leaf and composite, a member the format does not define —
|
|
508
|
-
refuses the file rather than repairing it, because the readings it would be choosing between are
|
|
509
|
-
different queries and picking one is how a corpus comes to describe coverage it never had.
|
|
510
|
-
|
|
511
|
-
`corpusVersion` is an integer that names the format, not the tool: it changes only when a corpus
|
|
512
|
-
written by one version can no longer be read correctly by another, and it does not move when
|
|
513
|
-
`@indexwright/record` is released. A reader handed a `corpusVersion` it does not know refuses the
|
|
514
|
-
file and says so. It does not fall back to reading what it recognises, which would silently mis-read
|
|
515
|
-
exactly the change the integer exists to announce.
|
|
516
|
-
|
|
517
|
-
### Implicit fields are not materialised
|
|
518
|
-
|
|
519
|
-
Firestore appends the document key to every query's sort order, and promotes an inequality field
|
|
520
|
-
into it. `record` neither adds those fields nor removes them: the corpus records the sort order as
|
|
521
|
-
sent.
|
|
522
|
-
|
|
523
|
-
What is sent is not the same for every client. The Node server SDK serialises the orders the
|
|
524
|
-
application wrote and leaves the rest to the server; the Firebase Web SDK normalises first, so a
|
|
525
|
-
`where('age', '>', 18)` reaches the wire already carrying `age ASCENDING, __name__ ASCENDING`. One
|
|
526
|
-
application query therefore has two legitimate corpus shapes depending on which SDK issued it, and
|
|
527
|
-
a corpus is comparable across runs of one project rather than across projects. Recording what was
|
|
528
|
-
sent is what keeps that difference visible; normalising in either direction would hide it behind a
|
|
529
|
-
rule this specification would have had to invent.
|
|
530
|
-
|
|
531
|
-
This is deliberate. §5 has to define the implicit `__name__` direction because a linter comparing
|
|
532
|
-
two declarations has no other way to tell whether they name the same resource. A corpus has no such
|
|
533
|
-
need: the v0.3 oracle is Firestore itself, which applies the real rule. Materialising a guessed
|
|
534
|
-
`__name__` into the corpus would put an unpublished behaviour into a durable file and make every
|
|
535
|
-
entry wrong if the guess were wrong. When a human reads a corpus entry against an index key, §5's
|
|
536
|
-
definition is the one that applies.
|
|
537
|
-
|
|
538
|
-
### Replay without values
|
|
539
|
-
|
|
540
|
-
v0.3 must turn a corpus entry back into a query it can execute, and the entry has no values to put
|
|
541
|
-
back. It synthesises them: a unary filter needs none, `IN`, `NOT_IN`, and `ARRAY_CONTAINS_ANY` need
|
|
542
|
-
a one-element array, and everything else needs a single scalar. "Everything else" is a finite list
|
|
543
|
-
rather than a default, because the vocabulary above enumerates the operators a corpus may hold and
|
|
544
|
-
anything outside it was skipped at capture as `unsupported-shape`.
|
|
545
|
-
|
|
546
|
-
Two shapes are not free-form and cannot be synthesised from the same rule. A `__name__` filter takes
|
|
547
|
-
a document reference under the collection being queried, not a scalar, because Firestore validates
|
|
548
|
-
the operand's type against the document key before it selects an index. And an entry whose root
|
|
549
|
-
composite has no children replays with `where` omitted altogether, not as an empty `AND`: a wire
|
|
550
|
-
`CompositeFilter` must carry at least one filter. Both would otherwise fail with `INVALID_ARGUMENT`,
|
|
551
|
-
and an `INVALID_ARGUMENT` is not a `FAILED_PRECONDITION`. v0.3 reports the latter and never the
|
|
552
|
-
former: a replay that comes back invalid is either a synthesis this section got wrong or a query
|
|
553
|
-
that was already invalid when it was captured — the corpus admits those, since it records what was
|
|
554
|
-
sent rather than what succeeded — and neither is a statement about the index set. Both are reported
|
|
555
|
-
as un-replayable entries, which is a defect in the tooling or the test that issued them.
|
|
556
|
-
|
|
557
|
-
This rests on the claim above — that index selection does not depend on the compared value or its
|
|
558
|
-
type. The claim is consistent with how the field is indexed rather than the value, but it is not
|
|
559
|
-
published, and it is the one assumption in v0.3 that a synthesised replay could get wrong. If it is
|
|
560
|
-
false, replay reports `FAILED_PRECONDITION` where a real query would have succeeded, which is a
|
|
561
|
-
false positive of exactly the kind §2 forbids acting on. v0.3 must test the claim before it reports.
|
|
562
|
-
|
|
563
|
-
### What is not captured
|
|
564
|
-
|
|
565
|
-
`record` captures `RunQuery`. Everything else the proxy sees, it counts under one of the reasons
|
|
566
|
-
below and records nothing:
|
|
567
|
-
|
|
568
|
-
- **`Listen`** — a snapshot listener carries its query in `Target.QueryTarget.structured_query` and
|
|
569
|
-
issues no `RunQuery` at all. Its index requirements are exactly those of the query it holds, so
|
|
570
|
-
this is the one omission that costs coverage rather than preventing a misreport: a suite whose
|
|
571
|
-
only exercise of a collection is `onSnapshot` yields a corpus with no entry for it. Counted as
|
|
572
|
-
`listen-query`, and named in §3 as a gap rather than an inherent limit. Capturing it is the first
|
|
573
|
-
extension worth making.
|
|
574
|
-
- **`PartitionQuery`** — carries a `StructuredQuery` the same way, but as a bulk-read entry point
|
|
575
|
-
rather than an application query. Counted as `partition-query`.
|
|
576
|
-
- **`RunAggregationQuery`** — `count()`, `sum()`, and `average()` carry a `StructuredQuery` and have
|
|
577
|
-
their own index requirements, which are not necessarily those of the underlying query. Recording
|
|
578
|
-
the inner query would misreport them, so v0.2 counts them as `aggregation-query`. Capturing them
|
|
579
|
-
properly is a v0.3-or-later extension.
|
|
580
|
-
- **`find_nearest`** — vector search is counted as `vector-query` for the same reason: it is served
|
|
581
|
-
by a `vectorConfig` index whose matching rule this specification does not yet model.
|
|
582
|
-
- **A shape the vocabulary cannot express** — an unrecognised or unspecified enum value, or a `from`
|
|
583
|
-
that does not name exactly one collection. Counted as `unsupported-shape`.
|
|
584
|
-
- **A query-bearing RPC this specification does not model** — `ExecutePipeline` carries a
|
|
585
|
-
`StructuredPipeline` rather than a `StructuredQuery`, and pipelines are outside v0.2. Counted as
|
|
586
|
-
`unsupported-rpc`, so that `skipped: []` keeps meaning *nothing was declined* rather than *nothing
|
|
587
|
-
the proxy happened to recognise was declined*. The same reason covers any other method on the
|
|
588
|
-
Firestore service that is neither captured above nor on the list of methods known to carry no
|
|
589
|
-
query at all: a method added after this was written is counted, not assumed harmless.
|
|
590
|
-
- **A message compressed with an encoding `record` cannot undo** — gRPC marks compression per
|
|
591
|
-
message and names the codec in `grpc-encoding`. `gzip` and `deflate` are undone and the message
|
|
592
|
-
read; anything else is counted as `unsupported-encoding`. A client that negotiates a codec this
|
|
593
|
-
package does not implement would otherwise have every query it sent vanish without trace.
|
|
594
|
-
- **Bytes that do not parse** — a truncated frame, a body that ends mid-message, or a payload that
|
|
595
|
-
is not the message the method declares. Counted as `undecodable-message`. This is the one reason
|
|
596
|
-
that indicates a defect rather than a boundary: it should not occur against a conforming client,
|
|
597
|
-
and a corpus carrying it is reporting that something read the wire wrongly.
|
|
598
|
-
|
|
599
|
-
`skipped` draws from that closed vocabulary and nothing else. No text decoded from the wire is ever
|
|
600
|
-
interpolated into a reason: the corpus is committed, and a free-text field fed by intercepted
|
|
601
|
-
traffic is a second way for data to reach the repository after values have been stripped from the
|
|
602
|
-
first.
|
|
603
|
-
|
|
604
|
-
They are reported rather than dropped silently. §3's known limit — that coverage is bounded by what
|
|
605
|
-
exercises the proxy — is about queries no test issues; a query that *was* issued and then discarded
|
|
606
|
-
without trace would be a different and worse failure, because it would look like coverage.
|
|
607
|
-
|
|
608
|
-
## 8. Design principles
|
|
609
|
-
|
|
610
|
-
**Warn, do not fail.** The rules encode heuristics whose false-positive rates are, at v0.1.0,
|
|
611
|
-
unmeasured. Shipping them as blocking checks would teach users to suppress the tool. Enforcement is
|
|
612
|
-
opt-in and per-adopter.
|
|
613
|
-
|
|
614
|
-
**Never authorise a deletion.** See §2. No output phrasing may suggest that an index is unused or
|
|
615
|
-
safe to remove.
|
|
616
|
-
|
|
617
|
-
**No dependencies in `indexwright`.** A linter that pulls a dependency tree into a build pipeline
|
|
618
|
-
undermines its own purpose. Argument parsing and formatting are implemented in-tree, and the
|
|
619
|
-
published package declares no runtime dependencies. This is a property of the package rather than
|
|
620
|
-
an aspiration of the project: it holds in every version, and a test asserts it.
|
|
621
|
-
|
|
622
|
-
The principle is about what lands in an adopter's tree, so build- and test-time tooling that never
|
|
623
|
-
ships is out of its scope. It is not a claim that no part of indexwright may depend on anything.
|
|
624
|
-
Where a verb needs a library it cannot reasonably write — the Firestore client behind `check` (§3)
|
|
625
|
-
— that verb ships as its own package instead of as a dependency of the linter. What the principle
|
|
626
|
-
forbids is making every adopter of `lint` pay for it.
|
|
627
|
-
|
|
628
|
-
`@indexwright/record` holds the line further than the principle requires: at v0.2 it declares no
|
|
629
|
-
runtime dependency either, because capture turned out to need none (§3). That is not a promise it
|
|
630
|
-
makes for every version, and `check` will end it.
|
|
631
|
-
|
|
632
|
-
**No network, no credentials, in `lint`.** Static analysis must be runnable in any environment,
|
|
633
|
-
including a sandboxed CI step with no cloud access. Network use is confined to the planned
|
|
634
|
-
`record`/`check` verbs, which are separate commands in a separate package (§3).
|
|
635
|
-
|
|
636
|
-
**Delegate undocumented semantics to the platform.** Where Firestore's behaviour is not published —
|
|
637
|
-
principally index matching — indexwright measures rather than models. This bounds what the tool can
|
|
638
|
-
claim, which is the point.
|
|
639
|
-
|
|
640
|
-
**Deterministic output.** Findings are emitted in a stable sort order (file, rule, key), so that
|
|
641
|
-
output can be diffed across runs.
|
|
642
|
-
|
|
643
|
-
## 9. Testing
|
|
644
|
-
|
|
645
|
-
Rules are tested against small hand-written fixtures that isolate one condition each, with explicit
|
|
646
|
-
positive and negative cases.
|
|
647
|
-
|
|
648
|
-
**Assertions are never written against a real project's index file.** A test that asserts a finding
|
|
649
|
-
count over live data fails whenever that data legitimately changes, which trains maintainers to
|
|
650
|
-
edit the test rather than read it. Fixtures encode the invariant; real files are for manual
|
|
651
|
-
exploration only.
|
|
652
|
-
|
|
653
|
-
## 10. Compatibility
|
|
654
|
-
|
|
655
|
-
- Node.js ≥ 22, ESM.
|
|
656
|
-
- Input schema follows the Firebase CLI's `firestore.indexes.json`. Unknown keys are preserved and
|
|
657
|
-
ignored rather than rejected, so that a newer field does not break linting.
|
|
658
|
-
- Semantic versioning. Pre-1.0, rule additions and message changes may land in minor releases;
|
|
659
|
-
the `json` output shape is the stable contract and changes only in major releases after 1.0.
|
|
660
|
-
- The package also exports a JavaScript API, so the rules can be run without spawning a process.
|
|
661
|
-
That API is **provisional**: it is not part of the stable contract before 1.0 and may change in
|
|
662
|
-
any minor release. Only the `json` output shape carries the compatibility promise.
|
|
663
|
-
- indexwright is published as a family: `indexwright`, the linter, and — from v0.2 —
|
|
664
|
-
`@indexwright/record`, capture and coverage. Both carry no runtime dependencies at v0.2; `check`
|
|
665
|
-
will give the second one a Firestore client (§3). They version independently, and their version
|
|
666
|
-
numbers are not held in step: `@indexwright/record` 0.2.0 and `indexwright` 0.2.0 coincide only
|
|
667
|
-
because the family reached v0.2 together. When `check` lands and declares a range of
|
|
668
|
-
`indexwright` whose `json` contract it reads, that range is what ties them, not the numbers.
|
|
669
|
-
- The query corpus (§7) is the second stable contract, and the only one that crosses a package
|
|
670
|
-
boundary as a file. It is versioned by its own `corpusVersion` rather than by either package's
|
|
671
|
-
release number, so a writer and a reader agree on the integer and not on each other's versions.
|
|
672
|
-
Everything else in `@indexwright/record` is provisional before 1.0 on the same terms as the
|
|
673
|
-
JavaScript API above.
|
|
674
|
-
|
|
675
|
-
## 11. Toward 1.0
|
|
676
|
-
|
|
677
|
-
1.0 requires, at minimum:
|
|
678
|
-
|
|
679
|
-
- Measured false-positive rates for R1 and R2 across more than one real project.
|
|
680
|
-
- At least one adopter outside the project of origin.
|
|
681
|
-
- A decision, informed by that data, on whether any rule should default to failing.
|
|
682
|
-
|
|
683
|
-
Until then the version stays below 1.0 and the README states plainly that the rules are provisional.
|