mongo-query-normalizer 0.1.0 → 0.2.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/README.md +369 -39
- package/README.zh-CN.md +363 -44
- package/dist/ast/types.d.ts +4 -0
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/normalize-context.d.ts +5 -1
- package/dist/normalize-context.d.ts.map +1 -1
- package/dist/normalize-context.js +3 -0
- package/dist/normalize-context.js.map +1 -1
- package/dist/normalize.d.ts +1 -1
- package/dist/normalize.d.ts.map +1 -1
- package/dist/normalize.js +105 -52
- package/dist/normalize.js.map +1 -1
- package/dist/options/constants.d.ts.map +1 -1
- package/dist/options/constants.js +7 -16
- package/dist/options/constants.js.map +1 -1
- package/dist/options/resolve.d.ts.map +1 -1
- package/dist/options/resolve.js +32 -1
- package/dist/options/resolve.js.map +1 -1
- package/dist/options/types.d.ts +1 -1
- package/dist/options/types.d.ts.map +1 -1
- package/dist/passes/normalize-predicate.d.ts.map +1 -1
- package/dist/passes/normalize-predicate.js +93 -14
- package/dist/passes/normalize-predicate.js.map +1 -1
- package/dist/predicate/analysis/detect-array-sensitive.d.ts +3 -0
- package/dist/predicate/analysis/detect-array-sensitive.d.ts.map +1 -0
- package/dist/predicate/analysis/detect-array-sensitive.js +33 -0
- package/dist/predicate/analysis/detect-array-sensitive.js.map +1 -0
- package/dist/predicate/analysis/detect-null-sensitive.d.ts +3 -0
- package/dist/predicate/analysis/detect-null-sensitive.d.ts.map +1 -0
- package/dist/predicate/analysis/detect-null-sensitive.js +24 -0
- package/dist/predicate/analysis/detect-null-sensitive.js.map +1 -0
- package/dist/predicate/analysis/detect-opaque-mix.d.ts +3 -0
- package/dist/predicate/analysis/detect-opaque-mix.d.ts.map +1 -0
- package/dist/predicate/analysis/detect-opaque-mix.js +9 -0
- package/dist/predicate/analysis/detect-opaque-mix.js.map +1 -0
- package/dist/predicate/analysis/detect-path-conflict-risk.d.ts +3 -0
- package/dist/predicate/analysis/detect-path-conflict-risk.d.ts.map +1 -0
- package/dist/predicate/analysis/detect-path-conflict-risk.js +14 -0
- package/dist/predicate/analysis/detect-path-conflict-risk.js.map +1 -0
- package/dist/predicate/analysis/merge-range-bounds.d.ts +27 -0
- package/dist/predicate/analysis/merge-range-bounds.d.ts.map +1 -0
- package/dist/predicate/analysis/merge-range-bounds.js +150 -0
- package/dist/predicate/analysis/merge-range-bounds.js.map +1 -0
- package/dist/predicate/capabilities/eq/eq-eq.d.ts +3 -0
- package/dist/predicate/capabilities/eq/eq-eq.d.ts.map +1 -0
- package/dist/predicate/capabilities/eq/eq-eq.js +53 -0
- package/dist/predicate/capabilities/eq/eq-eq.js.map +1 -0
- package/dist/predicate/capabilities/eq/eq-in.d.ts +3 -0
- package/dist/predicate/capabilities/eq/eq-in.d.ts.map +1 -0
- package/dist/predicate/capabilities/eq/eq-in.js +117 -0
- package/dist/predicate/capabilities/eq/eq-in.js.map +1 -0
- package/dist/predicate/capabilities/eq/eq-ne.d.ts +3 -0
- package/dist/predicate/capabilities/eq/eq-ne.d.ts.map +1 -0
- package/dist/predicate/capabilities/eq/eq-ne.js +37 -0
- package/dist/predicate/capabilities/eq/eq-ne.js.map +1 -0
- package/dist/predicate/capabilities/eq/eq-range.d.ts +3 -0
- package/dist/predicate/capabilities/eq/eq-range.d.ts.map +1 -0
- package/dist/predicate/capabilities/eq/eq-range.js +138 -0
- package/dist/predicate/capabilities/eq/eq-range.js.map +1 -0
- package/dist/predicate/capabilities/range/range-range.d.ts +3 -0
- package/dist/predicate/capabilities/range/range-range.d.ts.map +1 -0
- package/dist/predicate/capabilities/range/range-range.js +154 -0
- package/dist/predicate/capabilities/range/range-range.js.map +1 -0
- package/dist/predicate/capabilities/shared/capability-types.d.ts +11 -0
- package/dist/predicate/capabilities/shared/capability-types.d.ts.map +1 -0
- package/dist/predicate/capabilities/shared/capability-types.js +3 -0
- package/dist/predicate/capabilities/shared/capability-types.js.map +1 -0
- package/dist/predicate/capabilities/shared/relation-context.d.ts +13 -0
- package/dist/predicate/capabilities/shared/relation-context.d.ts.map +1 -0
- package/dist/predicate/capabilities/shared/relation-context.js +3 -0
- package/dist/predicate/capabilities/shared/relation-context.js.map +1 -0
- package/dist/predicate/capabilities/shared/relation-result.d.ts +12 -0
- package/dist/predicate/capabilities/shared/relation-result.d.ts.map +1 -0
- package/dist/predicate/capabilities/shared/relation-result.js +14 -0
- package/dist/predicate/capabilities/shared/relation-result.js.map +1 -0
- package/dist/predicate/index.d.ts +12 -0
- package/dist/predicate/index.d.ts.map +1 -0
- package/dist/predicate/index.js +22 -0
- package/dist/predicate/index.js.map +1 -0
- package/dist/predicate/ir/build-field-bundle.d.ts +7 -0
- package/dist/predicate/ir/build-field-bundle.d.ts.map +1 -0
- package/dist/predicate/ir/build-field-bundle.js +88 -0
- package/dist/predicate/ir/build-field-bundle.js.map +1 -0
- package/dist/predicate/ir/compile-field-bundle.d.ts +4 -0
- package/dist/predicate/ir/compile-field-bundle.d.ts.map +1 -0
- package/dist/predicate/ir/compile-field-bundle.js +57 -0
- package/dist/predicate/ir/compile-field-bundle.js.map +1 -0
- package/dist/predicate/ir/dedupe-atoms.d.ts +6 -0
- package/dist/predicate/ir/dedupe-atoms.d.ts.map +1 -0
- package/dist/predicate/ir/dedupe-atoms.js +42 -0
- package/dist/predicate/ir/dedupe-atoms.js.map +1 -0
- package/dist/predicate/ir/field-predicate-bundle.d.ts +16 -0
- package/dist/predicate/ir/field-predicate-bundle.d.ts.map +1 -0
- package/dist/predicate/ir/field-predicate-bundle.js +3 -0
- package/dist/predicate/ir/field-predicate-bundle.js.map +1 -0
- package/dist/predicate/ir/predicate-atom.d.ts +33 -0
- package/dist/predicate/ir/predicate-atom.d.ts.map +1 -0
- package/dist/predicate/ir/predicate-atom.js +3 -0
- package/dist/predicate/ir/predicate-atom.js.map +1 -0
- package/dist/predicate/local-normalize-result.d.ts +17 -0
- package/dist/predicate/local-normalize-result.d.ts.map +1 -0
- package/dist/predicate/local-normalize-result.js +3 -0
- package/dist/predicate/local-normalize-result.js.map +1 -0
- package/dist/predicate/meta/collect-predicate-meta.d.ts +16 -0
- package/dist/predicate/meta/collect-predicate-meta.d.ts.map +1 -0
- package/dist/predicate/meta/collect-predicate-meta.js +13 -0
- package/dist/predicate/meta/collect-predicate-meta.js.map +1 -0
- package/dist/predicate/normalize-field-predicate-bundle.d.ts +18 -0
- package/dist/predicate/normalize-field-predicate-bundle.d.ts.map +1 -0
- package/dist/predicate/normalize-field-predicate-bundle.js +126 -0
- package/dist/predicate/normalize-field-predicate-bundle.js.map +1 -0
- package/dist/predicate/normalize-predicate.d.ts +5 -0
- package/dist/predicate/normalize-predicate.d.ts.map +1 -0
- package/dist/predicate/normalize-predicate.js +10 -0
- package/dist/predicate/normalize-predicate.js.map +1 -0
- package/dist/predicate/planner/capability-selector.d.ts +8 -0
- package/dist/predicate/planner/capability-selector.d.ts.map +1 -0
- package/dist/predicate/planner/capability-selector.js +23 -0
- package/dist/predicate/planner/capability-selector.js.map +1 -0
- package/dist/predicate/planner/relation-plan.d.ts +10 -0
- package/dist/predicate/planner/relation-plan.d.ts.map +1 -0
- package/dist/predicate/planner/relation-plan.js +3 -0
- package/dist/predicate/planner/relation-plan.js.map +1 -0
- package/dist/predicate/planner/relation-planner.d.ts +5 -0
- package/dist/predicate/planner/relation-planner.d.ts.map +1 -0
- package/dist/predicate/planner/relation-planner.js +62 -0
- package/dist/predicate/planner/relation-planner.js.map +1 -0
- package/dist/predicate/registry/predicate-capability-registry.d.ts +3 -0
- package/dist/predicate/registry/predicate-capability-registry.d.ts.map +1 -0
- package/dist/predicate/registry/predicate-capability-registry.js +19 -0
- package/dist/predicate/registry/predicate-capability-registry.js.map +1 -0
- package/dist/predicate/safety/predicate-safety-policy.d.ts +9 -0
- package/dist/predicate/safety/predicate-safety-policy.d.ts.map +1 -0
- package/dist/predicate/safety/predicate-safety-policy.js +11 -0
- package/dist/predicate/safety/predicate-safety-policy.js.map +1 -0
- package/dist/predicate/utils/bson-compare.d.ts +2 -0
- package/dist/predicate/utils/bson-compare.d.ts.map +1 -0
- package/dist/predicate/utils/bson-compare.js +7 -0
- package/dist/predicate/utils/bson-compare.js.map +1 -0
- package/dist/predicate/utils/intersect-in-lists.d.ts +5 -0
- package/dist/predicate/utils/intersect-in-lists.d.ts.map +1 -0
- package/dist/predicate/utils/intersect-in-lists.js +15 -0
- package/dist/predicate/utils/intersect-in-lists.js.map +1 -0
- package/dist/predicate/utils/set-ops.d.ts +3 -0
- package/dist/predicate/utils/set-ops.d.ts.map +1 -0
- package/dist/predicate/utils/set-ops.js +18 -0
- package/dist/predicate/utils/set-ops.js.map +1 -0
- package/dist/predicate/utils/value-equality.d.ts +2 -0
- package/dist/predicate/utils/value-equality.d.ts.map +1 -0
- package/dist/predicate/utils/value-equality.js +8 -0
- package/dist/predicate/utils/value-equality.js.map +1 -0
- package/dist/rules/or-common-predicate/detect-common-predicates-in-or.d.ts +6 -0
- package/dist/rules/or-common-predicate/detect-common-predicates-in-or.d.ts.map +1 -0
- package/dist/rules/or-common-predicate/detect-common-predicates-in-or.js +83 -0
- package/dist/rules/or-common-predicate/detect-common-predicates-in-or.js.map +1 -0
- package/dist/rules/shape/collapse-single-child-logical.js +1 -1
- package/dist/rules/shape/collapse-single-child-logical.js.map +1 -1
- package/dist/rules/shape/dedupe-logical-children.js +1 -1
- package/dist/rules/shape/dedupe-logical-children.js.map +1 -1
- package/dist/rules/shape/flatten-logical.js +2 -2
- package/dist/rules/shape/flatten-logical.js.map +1 -1
- package/dist/rules/shape/remove-empty-logical.js +2 -2
- package/dist/rules/shape/remove-empty-logical.js.map +1 -1
- package/dist/scope/analysis/analyze-branch-coverage.d.ts +5 -0
- package/dist/scope/analysis/analyze-branch-coverage.d.ts.map +1 -0
- package/dist/scope/analysis/analyze-branch-coverage.js +8 -0
- package/dist/scope/analysis/analyze-branch-coverage.js.map +1 -0
- package/dist/scope/analysis/analyze-branch-satisfiability.d.ts +9 -0
- package/dist/scope/analysis/analyze-branch-satisfiability.d.ts.map +1 -0
- package/dist/scope/analysis/analyze-branch-satisfiability.js +33 -0
- package/dist/scope/analysis/analyze-branch-satisfiability.js.map +1 -0
- package/dist/scope/context/build-inherited-constraints.d.ts +6 -0
- package/dist/scope/context/build-inherited-constraints.d.ts.map +1 -0
- package/dist/scope/context/build-inherited-constraints.js +119 -0
- package/dist/scope/context/build-inherited-constraints.js.map +1 -0
- package/dist/scope/context/constraint-set.d.ts +19 -0
- package/dist/scope/context/constraint-set.d.ts.map +1 -0
- package/dist/scope/context/constraint-set.js +26 -0
- package/dist/scope/context/constraint-set.js.map +1 -0
- package/dist/scope/context/merge-constraint-sources.d.ts +4 -0
- package/dist/scope/context/merge-constraint-sources.d.ts.map +1 -0
- package/dist/scope/context/merge-constraint-sources.js +28 -0
- package/dist/scope/context/merge-constraint-sources.js.map +1 -0
- package/dist/scope/index.d.ts +8 -0
- package/dist/scope/index.d.ts.map +1 -0
- package/dist/scope/index.js +19 -0
- package/dist/scope/index.js.map +1 -0
- package/dist/scope/meta/collect-scope-meta.d.ts +5 -0
- package/dist/scope/meta/collect-scope-meta.d.ts.map +1 -0
- package/dist/scope/meta/collect-scope-meta.js +7 -0
- package/dist/scope/meta/collect-scope-meta.js.map +1 -0
- package/dist/scope/normalize-scope.d.ts +4 -0
- package/dist/scope/normalize-scope.d.ts.map +1 -0
- package/dist/scope/normalize-scope.js +149 -0
- package/dist/scope/normalize-scope.js.map +1 -0
- package/dist/scope/planner/scope-plan.d.ts +6 -0
- package/dist/scope/planner/scope-plan.d.ts.map +1 -0
- package/dist/scope/planner/scope-plan.js +9 -0
- package/dist/scope/planner/scope-plan.js.map +1 -0
- package/dist/scope/planner/scope-rewrite-planner.d.ts +4 -0
- package/dist/scope/planner/scope-rewrite-planner.d.ts.map +1 -0
- package/dist/scope/planner/scope-rewrite-planner.js +8 -0
- package/dist/scope/planner/scope-rewrite-planner.js.map +1 -0
- package/dist/scope/propagation/create-branch-local-bundle.d.ts +5 -0
- package/dist/scope/propagation/create-branch-local-bundle.d.ts.map +1 -0
- package/dist/scope/propagation/create-branch-local-bundle.js +16 -0
- package/dist/scope/propagation/create-branch-local-bundle.js.map +1 -0
- package/dist/scope/propagation/propagate-constraints-to-children.d.ts +6 -0
- package/dist/scope/propagation/propagate-constraints-to-children.d.ts.map +1 -0
- package/dist/scope/propagation/propagate-constraints-to-children.js +9 -0
- package/dist/scope/propagation/propagate-constraints-to-children.js.map +1 -0
- package/dist/scope/rewrite/collapse-single-branch.d.ts +5 -0
- package/dist/scope/rewrite/collapse-single-branch.d.ts.map +1 -0
- package/dist/scope/rewrite/collapse-single-branch.js +28 -0
- package/dist/scope/rewrite/collapse-single-branch.js.map +1 -0
- package/dist/scope/rewrite/prune-impossible-branches.d.ts +6 -0
- package/dist/scope/rewrite/prune-impossible-branches.d.ts.map +1 -0
- package/dist/scope/rewrite/prune-impossible-branches.js +32 -0
- package/dist/scope/rewrite/prune-impossible-branches.js.map +1 -0
- package/dist/scope/rewrite/remove-covered-local-constraints.d.ts +6 -0
- package/dist/scope/rewrite/remove-covered-local-constraints.d.ts.map +1 -0
- package/dist/scope/rewrite/remove-covered-local-constraints.js +76 -0
- package/dist/scope/rewrite/remove-covered-local-constraints.js.map +1 -0
- package/dist/scope/safety/scope-safety-policy.d.ts +10 -0
- package/dist/scope/safety/scope-safety-policy.d.ts.map +1 -0
- package/dist/scope/safety/scope-safety-policy.js +12 -0
- package/dist/scope/safety/scope-safety-policy.js.map +1 -0
- package/dist/types.d.ts +70 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
**English** | [中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
An **observable, level-based** normalizer for MongoDB query objects. It stabilizes query **shape** at the conservative default,
|
|
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
6
|
|
|
7
|
-
> **
|
|
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.
|
|
8
10
|
|
|
9
11
|
---
|
|
10
12
|
|
|
@@ -20,8 +22,8 @@ This library does **not** promise to make queries faster or to pick optimal inde
|
|
|
20
22
|
|
|
21
23
|
## Features
|
|
22
24
|
|
|
23
|
-
- **Level-based** normalization (`shape` → `predicate` → `
|
|
24
|
-
- **Conservative default**: `shape` only out of the box (
|
|
25
|
+
- **Level-based** normalization (`shape` → `predicate` → `scope`)
|
|
26
|
+
- **Conservative default**: `shape` only out of the box (lowest-risk structural pass)
|
|
25
27
|
- **Observable** `meta`: changed flags, applied/skipped rules, warnings, hashes, optional stats
|
|
26
28
|
- **Stable / idempotent** output when rules apply (same options)
|
|
27
29
|
- **Opaque fallback** for unsupported operators (passthrough, not semantically rewritten)
|
|
@@ -51,19 +53,158 @@ console.log(result.meta);
|
|
|
51
53
|
|
|
52
54
|
---
|
|
53
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
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { resolveNormalizeOptions } from "mongo-query-normalizer";
|
|
131
|
+
|
|
132
|
+
const resolvedOptions = resolveNormalizeOptions({
|
|
133
|
+
level: "predicate",
|
|
134
|
+
observe: { collectMetrics: true },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
console.log(resolvedOptions);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- Useful for debugging why a rule is enabled/disabled.
|
|
141
|
+
- Useful for logging a startup-time normalization config snapshot.
|
|
142
|
+
|
|
143
|
+
### 5) Consume `query` and `meta`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const { query: normalizedQuery, meta } = normalizeQuery(inputQuery, options);
|
|
147
|
+
|
|
148
|
+
if (meta.bailedOut) {
|
|
149
|
+
logger.warn({ reason: meta.bailoutReason }, "normalization bailed out");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (meta.changed) {
|
|
153
|
+
logger.info(
|
|
154
|
+
{
|
|
155
|
+
level: meta.level,
|
|
156
|
+
beforeHash: meta.beforeHash,
|
|
157
|
+
afterHash: meta.afterHash,
|
|
158
|
+
appliedRules: meta.appliedRules,
|
|
159
|
+
},
|
|
160
|
+
"query normalized"
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `query`: normalized query object.
|
|
166
|
+
- `meta`: observability data (changed flag, rule traces, warnings, hashes, optional stats/traces).
|
|
167
|
+
|
|
168
|
+
### 6) Typical integration patterns
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// A. Normalize centrally in data-access layer
|
|
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
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
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
|
+
|
|
54
193
|
## Default behavior
|
|
55
194
|
|
|
56
195
|
- **Default `level` is `"shape"`** (see `resolveNormalizeOptions()`).
|
|
57
|
-
- By default there is **no**
|
|
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.
|
|
58
197
|
- The goal is **stability and observability**, not “smart optimization.”
|
|
59
198
|
|
|
60
199
|
---
|
|
61
200
|
|
|
62
|
-
##
|
|
201
|
+
## Choosing a level
|
|
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.
|
|
63
206
|
|
|
64
|
-
|
|
65
|
-
- Levels above `shape` (`predicate`, `logical`, `experimental`) are **preview / unstable** surfaces. Use them for **offline analysis**, **replay testing**, and **targeted experiments** when you explicitly accept preview semantics—not as a default for all online requests.
|
|
66
|
-
- If you opt into a non-`shape` level, **`meta.warnings` includes a boundary notice** for that call. In **non-production** runs (`NODE_ENV !== "production"`), the library also prints a **matching `console.warn` once per level per process** so local development surfaces the same guidance without spamming repeated logs.
|
|
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.
|
|
67
208
|
|
|
68
209
|
---
|
|
69
210
|
|
|
@@ -71,32 +212,29 @@ console.log(result.meta);
|
|
|
71
212
|
|
|
72
213
|
### `shape` (default)
|
|
73
214
|
|
|
74
|
-
**Recommended for
|
|
215
|
+
**Recommended default** for the lowest-risk path. Safe structural normalization only, for example:
|
|
75
216
|
|
|
76
|
-
- flatten
|
|
77
|
-
- remove empty
|
|
78
|
-
- collapse single-child
|
|
79
|
-
- dedupe
|
|
217
|
+
- flatten compound (`$and` / `$or`) nodes
|
|
218
|
+
- remove empty compound nodes
|
|
219
|
+
- collapse single-child compound nodes
|
|
220
|
+
- dedupe compound children
|
|
80
221
|
- canonical ordering
|
|
81
222
|
|
|
82
223
|
### `predicate`
|
|
83
224
|
|
|
84
|
-
|
|
225
|
+
On top of `shape`, conservative **predicate** cleanup on **modeled** operators:
|
|
85
226
|
|
|
86
227
|
- dedupe same-field predicates
|
|
87
228
|
- merge comparable predicates where modeled
|
|
88
229
|
- collapse clear contradictions to an unsatisfiable filter
|
|
89
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)
|
|
90
231
|
|
|
91
|
-
### `
|
|
232
|
+
### `scope`
|
|
92
233
|
|
|
93
|
-
|
|
234
|
+
On top of `predicate`:
|
|
94
235
|
|
|
95
|
-
- **
|
|
96
|
-
|
|
97
|
-
### `experimental`
|
|
98
|
-
|
|
99
|
-
**Preview / not recommended for general production** in v0.1.0. May **hoist** common predicates from `$or` when the corresponding rule is enabled—**not** for blanket production rollout.
|
|
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
|
|
100
238
|
|
|
101
239
|
---
|
|
102
240
|
|
|
@@ -107,11 +245,13 @@ console.log(result.meta);
|
|
|
107
245
|
| `changed` | Structural/predicate output differs from input (hash-based) |
|
|
108
246
|
| `level` | Resolved normalization level |
|
|
109
247
|
| `appliedRules` / `skippedRules` | Rule tracing |
|
|
110
|
-
| `warnings` | Non-fatal issues when
|
|
248
|
+
| `warnings` | Non-fatal issues when `observe.collectWarnings` is enabled (rule notices, detection text, etc.) |
|
|
111
249
|
| `bailedOut` | Safety stop; output reverts to pre-pass parse for that call |
|
|
112
250
|
| `bailoutReason` | Why bailout happened, if any |
|
|
113
251
|
| `beforeHash` / `afterHash` | Stable hashes for diffing |
|
|
114
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 |
|
|
115
255
|
|
|
116
256
|
---
|
|
117
257
|
|
|
@@ -136,24 +276,22 @@ The **public contract** is:
|
|
|
136
276
|
## Principles (explicit)
|
|
137
277
|
|
|
138
278
|
1. Default level is **`shape`**.
|
|
139
|
-
2.
|
|
140
|
-
3.
|
|
141
|
-
4.
|
|
142
|
-
5. **
|
|
143
|
-
6. Output should be **idempotent** under the same options when no bailout occurs.
|
|
144
|
-
7. This library is **not** the MongoDB query planner or an optimizer.
|
|
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.
|
|
145
283
|
|
|
146
284
|
---
|
|
147
285
|
|
|
148
286
|
## Example scenarios
|
|
149
287
|
|
|
150
|
-
**Online main path** — use default (`shape`); this
|
|
288
|
+
**Online main path** — use default (`shape`); this remains the most production-safe baseline in `v0.2.0`:
|
|
151
289
|
|
|
152
290
|
```ts
|
|
153
291
|
normalizeQuery(query);
|
|
154
292
|
```
|
|
155
293
|
|
|
156
|
-
**
|
|
294
|
+
**Predicate or scope** — pass `level` explicitly; review [SPEC.md](SPEC.md) and contract tests for supported vs preserved patterns:
|
|
157
295
|
|
|
158
296
|
```ts
|
|
159
297
|
normalizeQuery(query, { level: "predicate" });
|
|
@@ -168,22 +306,214 @@ normalizeQuery(query, options?) => { query, meta }
|
|
|
168
306
|
resolveNormalizeOptions(options?) => ResolvedNormalizeOptions
|
|
169
307
|
```
|
|
170
308
|
|
|
171
|
-
Types: `NormalizeLevel`, `NormalizeOptions`, `NormalizeRules`, `NormalizeSafety`, `NormalizeObserve`, `ResolvedNormalizeOptions`, `NormalizeResult`, `NormalizeStats
|
|
309
|
+
Types: `NormalizeLevel`, `NormalizeOptions`, `NormalizeRules`, `NormalizeSafety`, `NormalizeObserve`, `ResolvedNormalizeOptions`, `NormalizeResult`, `NormalizeStats`, `PredicateSafetyPolicy`, `ScopeSafetyPolicy`, trace-related types (see package exports).
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Testing
|
|
314
|
+
|
|
315
|
+
### Test layout
|
|
316
|
+
|
|
317
|
+
This repository organizes tests by **API surface**, **normalization level**, and **cross-level contracts**, while preserving deeper semantic and regression suites.
|
|
318
|
+
|
|
319
|
+
### Directory responsibilities
|
|
320
|
+
|
|
321
|
+
#### `test/api/`
|
|
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.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
#### `test/levels/`
|
|
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:
|
|
346
|
+
|
|
347
|
+
1. positive capabilities of that level
|
|
348
|
+
2. behavior explicitly not enabled at that level
|
|
349
|
+
3. contrast with the adjacent level(s)
|
|
350
|
+
4. a small number of representative contracts for that level
|
|
351
|
+
|
|
352
|
+
Prefer asserting:
|
|
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.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
#### `test/semantic/`
|
|
383
|
+
|
|
384
|
+
Tests semantic equivalence against execution behavior.
|
|
385
|
+
These tests validate that normalization preserves meaning.
|
|
386
|
+
|
|
387
|
+
This directory is intentionally separate from `levels/` and `contracts/`.
|
|
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.
|
|
172
402
|
|
|
173
403
|
---
|
|
174
404
|
|
|
175
|
-
|
|
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.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### Rules for adding new tests
|
|
439
|
+
|
|
440
|
+
#### When adding a new normalization rule
|
|
441
|
+
|
|
442
|
+
Ask first:
|
|
443
|
+
|
|
444
|
+
* Is this a public API behavior?
|
|
445
|
+
|
|
446
|
+
* Add to `test/api/`
|
|
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/`
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
#### When adding a new level
|
|
463
|
+
|
|
464
|
+
At minimum, update all of the following:
|
|
465
|
+
|
|
466
|
+
1. add a new `test/levels/<level>-level.test.js`
|
|
467
|
+
2. register the level in `test/helpers/level-contract-runner.js`
|
|
468
|
+
3. ensure all-level contract suites cover it
|
|
469
|
+
4. add at least one contrast case against the adjacent level
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
### Testing style guidance
|
|
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
|
|
481
|
+
|
|
482
|
+
Avoid:
|
|
483
|
+
|
|
484
|
+
* coupling level tests to unstable implementation details
|
|
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
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
### Practical rule of thumb
|
|
492
|
+
|
|
493
|
+
* `api/` answers: **how the library is used**
|
|
494
|
+
* `levels/` answers: **what each level does and does not do**
|
|
495
|
+
* `contracts/` answers: **what must always remain true**
|
|
496
|
+
* `semantic/property/regression/performance` answer: **whether the system remains correct, robust, and efficient**
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
### npm scripts and property-test tooling
|
|
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`).
|
|
176
503
|
|
|
177
|
-
|
|
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`).
|
|
178
505
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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).
|
|
183
513
|
|
|
184
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`**.
|
|
185
515
|
|
|
186
|
-
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.test.js`**.
|
|
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`**.
|
|
187
517
|
|
|
188
518
|
---
|
|
189
519
|
|