mongo-query-normalizer 0.2.1 → 0.2.3
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/README.md +109 -441
- package/README.zh-CN.md +109 -430
- package/dist/compile/compile.js +13 -0
- package/dist/compile/compile.js.map +1 -1
- package/dist/normalize.d.ts.map +1 -1
- package/dist/normalize.js +3 -0
- package/dist/normalize.js.map +1 -1
- package/dist/passes/normalize-predicate.d.ts.map +1 -1
- package/dist/passes/normalize-predicate.js +38 -4
- package/dist/passes/normalize-predicate.js.map +1 -1
- package/dist/predicate/capabilities/eq/eq-eq.js +9 -11
- package/dist/predicate/capabilities/eq/eq-eq.js.map +1 -1
- package/dist/predicate/capabilities/eq/eq-in.d.ts.map +1 -1
- package/dist/predicate/capabilities/eq/eq-in.js +9 -50
- package/dist/predicate/capabilities/eq/eq-in.js.map +1 -1
- package/dist/predicate/capabilities/eq/eq-range.d.ts.map +1 -1
- package/dist/predicate/capabilities/eq/eq-range.js +11 -80
- package/dist/predicate/capabilities/eq/eq-range.js.map +1 -1
- package/dist/predicate/capabilities/range/range-range.d.ts.map +1 -1
- package/dist/predicate/capabilities/range/range-range.js +1 -10
- package/dist/predicate/capabilities/range/range-range.js.map +1 -1
- package/dist/predicate/planner/relation-planner.d.ts.map +1 -1
- package/dist/predicate/planner/relation-planner.js +0 -7
- package/dist/predicate/planner/relation-planner.js.map +1 -1
- package/dist/predicate/safety/predicate-safety-policy.d.ts +8 -0
- package/dist/predicate/safety/predicate-safety-policy.d.ts.map +1 -1
- package/dist/predicate/safety/predicate-safety-policy.js.map +1 -1
- package/dist/predicate/shared/cardinality-risk.d.ts +2 -0
- package/dist/predicate/shared/cardinality-risk.d.ts.map +1 -0
- package/dist/predicate/shared/cardinality-risk.js +7 -0
- package/dist/predicate/shared/cardinality-risk.js.map +1 -0
- package/dist/predicate/shared/field-cardinality-guards.d.ts +4 -0
- package/dist/predicate/shared/field-cardinality-guards.d.ts.map +1 -0
- package/dist/predicate/shared/field-cardinality-guards.js +11 -0
- package/dist/predicate/shared/field-cardinality-guards.js.map +1 -0
- package/dist/predicate/shared/field-cardinality-policy.d.ts +3 -0
- package/dist/predicate/shared/field-cardinality-policy.d.ts.map +1 -0
- package/dist/predicate/shared/field-cardinality-policy.js +5 -0
- package/dist/predicate/shared/field-cardinality-policy.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,523 +1,191 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mongo-query-normalizer
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
An **observable, level-based** normalizer for MongoDB query objects. It stabilizes query **shape** at the conservative default, and adds **`predicate`** and **`scope`** levels with **documented, test-backed contracts** (see [SPEC.md](SPEC.md) and [docs/normalization-matrix.md](docs/normalization-matrix.md) / [中文](docs/normalization-matrix.zh-CN.md)). It returns **predictable** output plus **metadata**—not a MongoDB planner optimizer.
|
|
6
|
-
|
|
7
|
-
> **Default posture:** **`shape`** is the smallest, structural-only pass and the recommended default for the widest production use. **`predicate`** and **`scope`** apply additional conservative rewrites under explicit contracts; adopt them when you need those transforms and accept their modeled-operator scope (opaque operators stay preserved).
|
|
8
|
-
>
|
|
9
|
-
> **As of `v0.2.0`:** predicate rewrites are intentionally narrowed to an explicitly validated surface (`eq.eq`, `eq.ne`, `eq.in`, `eq.range`, `range.range`). High-risk combinations (for example null-vs-missing, array-sensitive semantics, `$exists`/`$nin`, object-vs-dotted-path mixes, opaque mixes) remain conservative by design.
|
|
3
|
+
> A safe MongoDB query normalizer — **correctness over cleverness**
|
|
10
4
|
|
|
11
5
|
---
|
|
12
6
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
- Query **shape** diverges across builders and hand-written filters.
|
|
16
|
-
- Outputs can be **hard to compare**, log, or diff without a stable pass.
|
|
17
|
-
- You need a **low-risk normalization layer** that defaults to conservative behavior.
|
|
18
|
-
|
|
19
|
-
This library does **not** promise to make queries faster or to pick optimal indexes.
|
|
20
|
-
|
|
21
|
-
---
|
|
7
|
+
## ✨ What it does
|
|
22
8
|
|
|
23
|
-
|
|
9
|
+
**Turn messy Mongo queries into clean, stable, and predictable ones — safely.**
|
|
24
10
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## Install
|
|
11
|
+
```js
|
|
12
|
+
// before
|
|
13
|
+
{
|
|
14
|
+
$and: [
|
|
15
|
+
{ status: "open" },
|
|
16
|
+
{ status: { $in: ["open", "closed"] } }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
34
19
|
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
// after
|
|
21
|
+
{ status: "open" }
|
|
37
22
|
```
|
|
38
23
|
|
|
39
24
|
---
|
|
40
25
|
|
|
41
|
-
##
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
import { normalizeQuery } from "mongo-query-normalizer";
|
|
45
|
-
|
|
46
|
-
const result = normalizeQuery({
|
|
47
|
-
$and: [{ status: "open" }, { $and: [{ priority: { $gte: 1 } }] }],
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
console.log(result.query);
|
|
51
|
-
console.log(result.meta);
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## Complete usage guide
|
|
57
|
-
|
|
58
|
-
### 1) Minimal usage (recommended default)
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
import { normalizeQuery } from "mongo-query-normalizer";
|
|
62
|
-
|
|
63
|
-
const { query: normalizedQuery, meta } = normalizeQuery(inputQuery);
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
- Without `options`, default behavior is `level: "shape"`.
|
|
67
|
-
- Best for low-risk structural stabilization: logging, cache-key normalization, query diff alignment.
|
|
68
|
-
|
|
69
|
-
### 2) Pick a level explicitly
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
normalizeQuery(inputQuery, { level: "shape" }); // structural only (default)
|
|
73
|
-
normalizeQuery(inputQuery, { level: "predicate" }); // modeled predicate cleanup
|
|
74
|
-
normalizeQuery(inputQuery, { level: "scope" }); // scope propagation / conservative pruning
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
- `shape`: safest structural normalization.
|
|
78
|
-
- `predicate`: dedupe / merge / contradiction collapse for modeled operators.
|
|
79
|
-
- `scope`: adds inherited-constraint propagation and conservative branch decisions on top of `predicate`.
|
|
80
|
-
|
|
81
|
-
### 3) Full `options` example
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
import { normalizeQuery } from "mongo-query-normalizer";
|
|
85
|
-
|
|
86
|
-
const result = normalizeQuery(inputQuery, {
|
|
87
|
-
level: "scope",
|
|
88
|
-
rules: {
|
|
89
|
-
// shape-related
|
|
90
|
-
flattenLogical: true,
|
|
91
|
-
removeEmptyLogical: true,
|
|
92
|
-
collapseSingleChildLogical: true,
|
|
93
|
-
dedupeLogicalChildren: true,
|
|
94
|
-
// predicate-related
|
|
95
|
-
dedupeSameFieldPredicates: true,
|
|
96
|
-
mergeComparablePredicates: true,
|
|
97
|
-
collapseContradictions: true,
|
|
98
|
-
// ordering-related
|
|
99
|
-
sortLogicalChildren: true,
|
|
100
|
-
sortFieldPredicates: true,
|
|
101
|
-
// scope observe-only rule (no structural hoist)
|
|
102
|
-
detectCommonPredicatesInOr: true,
|
|
103
|
-
},
|
|
104
|
-
safety: {
|
|
105
|
-
maxNormalizeDepth: 32,
|
|
106
|
-
maxNodeGrowthRatio: 1.5,
|
|
107
|
-
},
|
|
108
|
-
observe: {
|
|
109
|
-
collectWarnings: true,
|
|
110
|
-
collectMetrics: false,
|
|
111
|
-
collectPredicateTraces: false,
|
|
112
|
-
collectScopeTraces: false,
|
|
113
|
-
},
|
|
114
|
-
predicate: {
|
|
115
|
-
safetyPolicy: {
|
|
116
|
-
// override only fields you care about
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
scope: {
|
|
120
|
-
safetyPolicy: {
|
|
121
|
-
// override only fields you care about
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### 4) Inspect resolved runtime options
|
|
26
|
+
## ⚠️ Why this matters
|
|
128
27
|
|
|
129
|
-
|
|
130
|
-
import { resolveNormalizeOptions } from "mongo-query-normalizer";
|
|
28
|
+
If you build dynamic queries, you will eventually get:
|
|
131
29
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
30
|
+
* duplicated conditions
|
|
31
|
+
* inconsistent query shapes
|
|
32
|
+
* hard-to-debug filters
|
|
33
|
+
* subtle semantic bugs
|
|
136
34
|
|
|
137
|
-
|
|
138
|
-
```
|
|
35
|
+
Most tools try to “optimize” queries.
|
|
139
36
|
|
|
140
|
-
|
|
141
|
-
- Useful for logging a startup-time normalization config snapshot.
|
|
37
|
+
👉 This library does something different:
|
|
142
38
|
|
|
143
|
-
|
|
39
|
+
> **It only applies transformations that are provably safe.**
|
|
144
40
|
|
|
145
|
-
|
|
146
|
-
const { query: normalizedQuery, meta } = normalizeQuery(inputQuery, options);
|
|
41
|
+
---
|
|
147
42
|
|
|
148
|
-
|
|
149
|
-
logger.warn({ reason: meta.bailoutReason }, "normalization bailed out");
|
|
150
|
-
}
|
|
43
|
+
## 🛡️ Safe by design
|
|
151
44
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
},
|
|
160
|
-
"query normalized"
|
|
161
|
-
);
|
|
45
|
+
```js
|
|
46
|
+
// NOT simplified (correctly)
|
|
47
|
+
{
|
|
48
|
+
$and: [
|
|
49
|
+
{ uids: "1" },
|
|
50
|
+
{ uids: "2" }
|
|
51
|
+
]
|
|
162
52
|
}
|
|
163
53
|
```
|
|
164
54
|
|
|
165
|
-
|
|
166
|
-
- `meta`: observability data (changed flag, rule traces, warnings, hashes, optional stats/traces).
|
|
55
|
+
Why?
|
|
167
56
|
|
|
168
|
-
|
|
57
|
+
Because MongoDB arrays can match both:
|
|
169
58
|
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
export function normalizeForFind(rawFilter) {
|
|
173
|
-
return normalizeQuery(rawFilter, { level: "shape" }).query;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// B. Use stronger convergence in offline paths
|
|
177
|
-
export function normalizeForBatch(rawFilter) {
|
|
178
|
-
return normalizeQuery(rawFilter, { level: "predicate" }).query;
|
|
179
|
-
}
|
|
59
|
+
```js
|
|
60
|
+
{ uids: ["1", "2"] }
|
|
180
61
|
```
|
|
181
62
|
|
|
182
|
-
- Prefer `shape` for online request paths.
|
|
183
|
-
- Enable `predicate` / `scope` when there is clear benefit plus test coverage.
|
|
184
|
-
|
|
185
|
-
### 7) Errors and boundaries
|
|
186
|
-
|
|
187
|
-
- Invalid `level` throws an error (for example, typos).
|
|
188
|
-
- Unsupported or unknown operators are generally preserved as opaque; semantic merge behavior is not guaranteed for them.
|
|
189
|
-
- The library target is stability and observability, not query planning optimization.
|
|
190
|
-
|
|
191
|
-
---
|
|
192
|
-
|
|
193
|
-
## Default behavior
|
|
194
|
-
|
|
195
|
-
- **Default `level` is `"shape"`** (see `resolveNormalizeOptions()`).
|
|
196
|
-
- By default there is **no** predicate merge at `shape`. At **`scope`**, core work is inherited-constraint propagation and conservative branch decisions; **`detectCommonPredicatesInOr`** is an **optional, observe-only** rule (warnings / traces)—never a structural hoist.
|
|
197
|
-
- The goal is **stability and observability**, not “smart optimization.”
|
|
198
|
-
|
|
199
63
|
---
|
|
200
64
|
|
|
201
|
-
##
|
|
202
|
-
|
|
203
|
-
- Use **`shape`** when you only need structural stabilization (flatten, dedupe children, ordering, etc.).
|
|
204
|
-
- Use **`predicate`** when you need same-field dedupe, modeled comparable merges, and contradiction collapse on **modeled** operators; opaque subtrees stay preserved.
|
|
205
|
-
- Use **`scope`** when you need inherited-constraint propagation, conservative pruning, and narrow coverage elimination as described in the spec and matrix. **`detectCommonPredicatesInOr`** (when enabled) is **observe-only** and does not rewrite structure.
|
|
206
|
-
|
|
207
|
-
Authoritative behavior boundaries are in **[SPEC.md](SPEC.md)**, **[docs/normalization-matrix.md](docs/normalization-matrix.md)**, and contract tests under **`test/contracts/`**—not informal README prose alone.
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## Levels
|
|
212
|
-
|
|
213
|
-
### `shape` (default)
|
|
214
|
-
|
|
215
|
-
**Recommended default** for the lowest-risk path. Safe structural normalization only, for example:
|
|
216
|
-
|
|
217
|
-
- flatten compound (`$and` / `$or`) nodes
|
|
218
|
-
- remove empty compound nodes
|
|
219
|
-
- collapse single-child compound nodes
|
|
220
|
-
- dedupe compound children
|
|
221
|
-
- canonical ordering
|
|
222
|
-
|
|
223
|
-
### `predicate`
|
|
224
|
-
|
|
225
|
-
On top of `shape`, conservative **predicate** cleanup on **modeled** operators:
|
|
226
|
-
|
|
227
|
-
- dedupe same-field predicates
|
|
228
|
-
- merge comparable predicates where modeled
|
|
229
|
-
- collapse clear contradictions to an unsatisfiable filter
|
|
230
|
-
- merge **direct** `$and` children that share the same field name before further predicate work (so contradictions like `{ $and: [{ a: 1 }, { a: 2 }] }` can be detected)
|
|
231
|
-
|
|
232
|
-
### `scope`
|
|
233
|
-
|
|
234
|
-
On top of `predicate`:
|
|
235
|
-
|
|
236
|
-
- **Inherited constraint propagation** (phase-1 allowlist) and **conservative branch pruning**; **coverage elimination** only in narrow, tested cases when policy allows
|
|
237
|
-
- Optional **`detectCommonPredicatesInOr`**: observe-only (warnings / traces); **no** structural rewrite
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
## `meta` fields
|
|
242
|
-
|
|
243
|
-
| Field | Meaning |
|
|
244
|
-
|--------|---------|
|
|
245
|
-
| `changed` | Structural/predicate output differs from input (hash-based) |
|
|
246
|
-
| `level` | Resolved normalization level |
|
|
247
|
-
| `appliedRules` / `skippedRules` | Rule tracing |
|
|
248
|
-
| `warnings` | Non-fatal issues when `observe.collectWarnings` is enabled (rule notices, detection text, etc.) |
|
|
249
|
-
| `bailedOut` | Safety stop; output reverts to pre-pass parse for that call |
|
|
250
|
-
| `bailoutReason` | Why bailout happened, if any |
|
|
251
|
-
| `beforeHash` / `afterHash` | Stable hashes for diffing |
|
|
252
|
-
| `stats` | Optional before/after tree metrics (`observe.collectMetrics`) |
|
|
253
|
-
| `predicateTraces` | When `observe.collectPredicateTraces`: per-field planner / skip / contradiction signals |
|
|
254
|
-
| `scopeTrace` | When `observe.collectScopeTraces`: constraint extraction rejections + scope decision events |
|
|
255
|
-
|
|
256
|
-
---
|
|
257
|
-
|
|
258
|
-
## Unsupported / opaque behavior
|
|
259
|
-
|
|
260
|
-
Structures such as **`$nor`**, **`$regex`**, **`$not`**, **`$elemMatch`**, **`$expr`**, geo/text queries, and **unknown** operators are generally treated as **opaque**: they pass through or are preserved without full semantic rewriting. They are **not** guaranteed to participate in merge or contradiction logic.
|
|
261
|
-
|
|
262
|
-
---
|
|
65
|
+
## ❌ What this is NOT
|
|
263
66
|
|
|
264
|
-
|
|
67
|
+
* Not a query optimizer
|
|
68
|
+
* Not an index advisor
|
|
69
|
+
* Not a performance tool
|
|
265
70
|
|
|
266
|
-
|
|
71
|
+
It will **never guess**:
|
|
267
72
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
73
|
+
* field cardinality
|
|
74
|
+
* schema constraints
|
|
75
|
+
* data distribution
|
|
271
76
|
|
|
272
|
-
|
|
77
|
+
If unsure → **skip**
|
|
273
78
|
|
|
274
79
|
---
|
|
275
80
|
|
|
276
|
-
##
|
|
277
|
-
|
|
278
|
-
1. Default level is **`shape`**.
|
|
279
|
-
2. **`predicate`** / **`scope`** may change structure while aiming for **semantic equivalence** on **modeled** operators.
|
|
280
|
-
3. **Opaque** nodes are not rewritten semantically.
|
|
281
|
-
4. Output should be **idempotent** under the same options when no bailout occurs.
|
|
282
|
-
5. This library is **not** the MongoDB query planner or an optimizer.
|
|
283
|
-
|
|
284
|
-
---
|
|
285
|
-
|
|
286
|
-
## Example scenarios
|
|
287
|
-
|
|
288
|
-
**Online main path** — use default (`shape`); this remains the most production-safe baseline in `v0.2.0`:
|
|
81
|
+
## 🚀 Quick start
|
|
289
82
|
|
|
290
83
|
```ts
|
|
291
|
-
normalizeQuery
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
**Predicate or scope** — pass `level` explicitly; review [SPEC.md](SPEC.md) and contract tests for supported vs preserved patterns:
|
|
84
|
+
import { normalizeQuery } from "mongo-query-normalizer";
|
|
295
85
|
|
|
296
|
-
|
|
297
|
-
normalizeQuery(query, { level: "predicate" });
|
|
86
|
+
const { query } = normalizeQuery(inputQuery);
|
|
298
87
|
```
|
|
299
88
|
|
|
300
89
|
---
|
|
301
90
|
|
|
302
|
-
##
|
|
91
|
+
## 🧠 Where it fits
|
|
303
92
|
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
|
|
93
|
+
```text
|
|
94
|
+
Query Builder / ORM
|
|
95
|
+
↓
|
|
96
|
+
normalizeQuery ← (this library)
|
|
97
|
+
↓
|
|
98
|
+
MongoDB
|
|
307
99
|
```
|
|
308
100
|
|
|
309
|
-
|
|
101
|
+
You don’t replace your builder.
|
|
102
|
+
You **sanitize its output**.
|
|
310
103
|
|
|
311
104
|
---
|
|
312
105
|
|
|
313
|
-
##
|
|
314
|
-
|
|
315
|
-
### Test layout
|
|
106
|
+
## 🧩 When to use
|
|
316
107
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
Tests the public API and configuration surface.
|
|
324
|
-
|
|
325
|
-
Put tests here when they verify:
|
|
326
|
-
|
|
327
|
-
* `normalizeQuery` return shape and top-level behavior
|
|
328
|
-
* `resolveNormalizeOptions`
|
|
329
|
-
* package exports
|
|
330
|
-
|
|
331
|
-
Do **not** put level-specific normalization behavior here.
|
|
108
|
+
* dynamic filters / search APIs
|
|
109
|
+
* BI / reporting systems
|
|
110
|
+
* user-generated queries
|
|
111
|
+
* multi-team codebases with inconsistent query styles
|
|
112
|
+
* logging / caching / diffing queries
|
|
332
113
|
|
|
333
114
|
---
|
|
334
115
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
Tests the behavior boundary of each `NormalizeLevel`.
|
|
338
|
-
|
|
339
|
-
Current levels:
|
|
340
|
-
|
|
341
|
-
* `shape`
|
|
342
|
-
* `predicate`
|
|
343
|
-
* `scope`
|
|
344
|
-
|
|
345
|
-
Each level test file should focus on four things:
|
|
116
|
+
## ⚙️ Levels
|
|
346
117
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
118
|
+
| Level | What it does | Safety |
|
|
119
|
+
| ----------- | ------------------------------ | --------- |
|
|
120
|
+
| `shape` | structural normalization | 🟢 safest |
|
|
121
|
+
| `predicate` | safe predicate simplification | 🟡 |
|
|
122
|
+
| `scope` | limited constraint propagation | 🟡 |
|
|
351
123
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
* normalized query structure
|
|
355
|
-
* observable cross-level differences
|
|
356
|
-
* stable public metadata
|
|
357
|
-
|
|
358
|
-
Avoid overfitting to:
|
|
359
|
-
|
|
360
|
-
* exact warning text
|
|
361
|
-
* exact internal rule IDs
|
|
362
|
-
* fixed child ordering unless ordering itself is part of the contract
|
|
363
|
-
|
|
364
|
-
---
|
|
365
|
-
|
|
366
|
-
#### `test/contracts/`
|
|
367
|
-
|
|
368
|
-
Tests contracts that should hold across levels, or default behavior that is separate from any single level.
|
|
369
|
-
|
|
370
|
-
Put tests here when they verify:
|
|
371
|
-
|
|
372
|
-
* default level behavior
|
|
373
|
-
* idempotency across all levels
|
|
374
|
-
* output invariants across all levels
|
|
375
|
-
* opaque subtree preservation across all levels
|
|
376
|
-
* formal **`predicate` / `scope`** contracts (supported merges, opaque preservation, scope policy guards, rule toggles)—see `test/contracts/predicate-scope-stable-contract.test.js`
|
|
377
|
-
|
|
378
|
-
Use `test/helpers/level-contract-runner.js` for all-level suites.
|
|
124
|
+
Default is `shape`.
|
|
379
125
|
|
|
380
126
|
---
|
|
381
127
|
|
|
382
|
-
|
|
128
|
+
## 📦 Output
|
|
383
129
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
#### `test/property/`
|
|
392
|
-
|
|
393
|
-
Tests property-based and metamorphic behavior.
|
|
394
|
-
|
|
395
|
-
Use this directory for:
|
|
396
|
-
|
|
397
|
-
* randomized semantic checks
|
|
398
|
-
* metamorphic invariants
|
|
399
|
-
* broad input-space validation
|
|
400
|
-
|
|
401
|
-
Do not use it as the primary place to express level boundaries.
|
|
402
|
-
|
|
403
|
-
---
|
|
404
|
-
|
|
405
|
-
#### `test/regression/`
|
|
406
|
-
|
|
407
|
-
Tests known historical failures and hand-crafted regression cases.
|
|
408
|
-
|
|
409
|
-
Add a regression test here when fixing a bug that should stay fixed.
|
|
410
|
-
|
|
411
|
-
---
|
|
412
|
-
|
|
413
|
-
#### `test/performance/`
|
|
414
|
-
|
|
415
|
-
Tests performance guards or complexity-sensitive behavior.
|
|
416
|
-
|
|
417
|
-
These tests should stay focused on performance-related expectations, not general normalization structure.
|
|
418
|
-
|
|
419
|
-
---
|
|
420
|
-
|
|
421
|
-
### Helper files
|
|
422
|
-
|
|
423
|
-
#### `test/helpers/level-runner.js`
|
|
424
|
-
|
|
425
|
-
Shared helper for running a query at a specific level.
|
|
426
|
-
|
|
427
|
-
#### `test/helpers/level-cases.js`
|
|
428
|
-
|
|
429
|
-
Shared fixed inputs used across level tests.
|
|
430
|
-
Prefer adding reusable representative cases here instead of duplicating inline fixtures.
|
|
431
|
-
|
|
432
|
-
#### `test/helpers/level-contract-runner.js`
|
|
433
|
-
|
|
434
|
-
Shared `LEVELS` list and helpers for all-level contract suites.
|
|
130
|
+
```ts
|
|
131
|
+
{
|
|
132
|
+
query, // normalized query
|
|
133
|
+
meta // debug / trace info
|
|
134
|
+
}
|
|
135
|
+
```
|
|
435
136
|
|
|
436
137
|
---
|
|
437
138
|
|
|
438
|
-
|
|
139
|
+
## 🎯 Design philosophy
|
|
439
140
|
|
|
440
|
-
|
|
141
|
+
> If a rewrite might be wrong, don’t do it.
|
|
441
142
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
*
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
* Is this enabled only at a specific level?
|
|
448
|
-
|
|
449
|
-
* Add to `test/levels/`
|
|
450
|
-
* Should this hold for all levels?
|
|
451
|
-
|
|
452
|
-
* Add to `test/contracts/`
|
|
453
|
-
* Is this about semantic preservation or randomized validation?
|
|
454
|
-
|
|
455
|
-
* Add to `test/semantic/` or `test/property/`
|
|
456
|
-
* Is this a bug fix for a previously broken case?
|
|
457
|
-
|
|
458
|
-
* Add to `test/regression/`
|
|
143
|
+
* no schema assumptions
|
|
144
|
+
* no array guessing
|
|
145
|
+
* no unsafe merges
|
|
146
|
+
* deterministic output
|
|
147
|
+
* idempotent results
|
|
459
148
|
|
|
460
149
|
---
|
|
461
150
|
|
|
462
|
-
|
|
151
|
+
## 🔍 Example
|
|
463
152
|
|
|
464
|
-
|
|
153
|
+
```ts
|
|
154
|
+
const result = normalizeQuery({
|
|
155
|
+
$and: [
|
|
156
|
+
{ status: "open" },
|
|
157
|
+
{ status: { $in: ["open", "closed"] } }
|
|
158
|
+
]
|
|
159
|
+
});
|
|
465
160
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
4. add at least one contrast case against the adjacent level
|
|
161
|
+
console.log(result.query);
|
|
162
|
+
// { status: "open" }
|
|
163
|
+
```
|
|
470
164
|
|
|
471
165
|
---
|
|
472
166
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
Prefer:
|
|
476
|
-
|
|
477
|
-
* example-based tests for level boundaries
|
|
478
|
-
* query-shape assertions
|
|
479
|
-
* contrast tests between adjacent levels
|
|
480
|
-
* shared fixtures for representative cases
|
|
167
|
+
## 📚 Docs
|
|
481
168
|
|
|
482
|
-
|
|
169
|
+
* [`SPEC.md`](SPEC.md) — behavior spec
|
|
170
|
+
* [`docs/normalization-matrix.md`](docs/normalization-matrix.md) — rule coverage by operator and level
|
|
171
|
+
* [`docs/CANONICAL_FORM.md`](docs/CANONICAL_FORM.md) — canonical output shape and idempotency
|
|
172
|
+
* [`CHANGELOG.md`](CHANGELOG.md) — release notes
|
|
173
|
+
* [`test/REGRESSION.md`](test/REGRESSION.md) — reproducing property / semantic test failures
|
|
483
174
|
|
|
484
|
-
|
|
485
|
-
* repeating the same fixture with only superficial assertion changes
|
|
486
|
-
* putting default-level behavior inside a specific level test
|
|
487
|
-
* mixing exports/API tests with normalization behavior tests
|
|
175
|
+
**中文:** [`README.zh-CN.md`](README.zh-CN.md) · [`SPEC.zh-CN.md`](SPEC.zh-CN.md) · [`docs/normalization-matrix.zh-CN.md`](docs/normalization-matrix.zh-CN.md) · [`CHANGELOG.zh-CN.md`](CHANGELOG.zh-CN.md)
|
|
488
176
|
|
|
489
177
|
---
|
|
490
178
|
|
|
491
|
-
|
|
179
|
+
## 🧪 Testing
|
|
492
180
|
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
* `semantic/property/regression/performance` answer: **whether the system remains correct, robust, and efficient**
|
|
181
|
+
* semantic equivalence tests (real MongoDB)
|
|
182
|
+
* property-based testing
|
|
183
|
+
* regression suites
|
|
497
184
|
|
|
498
185
|
---
|
|
499
186
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
Randomized semantic tests use **`mongodb-memory-server`** + **`fast-check`** to compare **real** `find` results (same `sort` / `skip` / `limit`, projection `{ _id: 1 }`) before and after `normalizeQuery` on a **fixed document schema** and a **restricted operator set** (see `test/helpers/arbitraries.js`). They assert matching **`_id` order**, **idempotency** of the returned `query`, and (for opaque operators) **non-crash / stable second pass** only. **`FC_SEED` / `FC_RUNS` defaults are centralized in `test/helpers/fc-config.js`** (also re-exported from `arbitraries.js`).
|
|
503
|
-
|
|
504
|
-
To **avoid downloading** a MongoDB binary, set one of **`MONGODB_BINARY`**, **`MONGOD_BINARY`**, or **`MONGOMS_SYSTEM_BINARY`** to your local `mongod` path before running semantic tests (see `test/helpers/mongo-fixture.js`).
|
|
505
|
-
|
|
506
|
-
* **`npm run test`** — build, then `test:unit`, then `test:semantic`.
|
|
507
|
-
* **`npm run test:api`** — `test/api/**/*.test.js` only.
|
|
508
|
-
* **`npm run test:levels`** — `test/levels/**/*.test.js` and `test/contracts/*.test.js`.
|
|
509
|
-
* **`npm run test:unit`** — all `test/**/*.test.js` except `test/semantic/**`, `test/regression/**`, and `test/property/**` (includes `test/api/**`, `test/levels/**`, `test/contracts/**`, `test/performance/**`, and other unit tests).
|
|
510
|
-
* **`npm run test:semantic`** — semantic + regression + property folders (defaults when env unset: see `fc-config.js`).
|
|
511
|
-
* **`npm run test:semantic:quick`** — lower **`FC_RUNS`** (script sets `45`) + **`FC_SEED=42`**, still runs `test/regression/**` and `test/property/**`.
|
|
512
|
-
* **`npm run test:semantic:ci`** — CI-oriented env (`FC_RUNS=200`, `FC_SEED=42` in script).
|
|
513
|
-
|
|
514
|
-
Override property-test parameters: **`FC_SEED`**, **`FC_RUNS`**, optional **`FC_QUICK=1`** (see `fc-config.js`). How to reproduce failures and when to add a fixed regression case: **`test/REGRESSION.md`**.
|
|
515
|
-
|
|
516
|
-
Full-text, geo, heavy **`$expr`**, **`$where`**, aggregation, collation, etc. stay **out** of the main semantic equivalence generator; opaque contracts live in **`test/contracts/opaque-operators.all-levels.test.js`**.
|
|
517
|
-
|
|
518
|
-
---
|
|
187
|
+
## ⭐ Philosophy
|
|
519
188
|
|
|
520
|
-
|
|
189
|
+
Most query tools try to be smart.
|
|
521
190
|
|
|
522
|
-
|
|
523
|
-
- [docs/CANONICAL_FORM.md](docs/CANONICAL_FORM.md) — idempotency and canonical shape notes.
|
|
191
|
+
This one tries to be **correct**.
|