@omgbase/oqx 0.14.0 → 0.14.1
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 +64 -0
- package/README.md +139 -6
- package/dist/adapters/indexed.d.ts +13 -1
- package/dist/adapters/indexed.d.ts.map +1 -1
- package/dist/adapters/indexed.js +55 -24
- package/dist/adapters/indexed.js.map +1 -1
- package/dist/context.d.ts +24 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +55 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +268 -35
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/optimize/analysis.d.ts +45 -0
- package/dist/optimize/analysis.d.ts.map +1 -0
- package/dist/optimize/analysis.js +194 -0
- package/dist/optimize/analysis.js.map +1 -0
- package/dist/optimize/driver.d.ts +8 -0
- package/dist/optimize/driver.d.ts.map +1 -0
- package/dist/optimize/driver.js +35 -0
- package/dist/optimize/driver.js.map +1 -0
- package/dist/optimize/hash-index.d.ts +34 -0
- package/dist/optimize/hash-index.d.ts.map +1 -0
- package/dist/optimize/hash-index.js +79 -0
- package/dist/optimize/hash-index.js.map +1 -0
- package/dist/optimize/index.d.ts +8 -0
- package/dist/optimize/index.d.ts.map +1 -0
- package/dist/optimize/index.js +11 -0
- package/dist/optimize/index.js.map +1 -0
- package/dist/optimize/ir.d.ts +84 -0
- package/dist/optimize/ir.d.ts.map +1 -0
- package/dist/optimize/ir.js +60 -0
- package/dist/optimize/ir.js.map +1 -0
- package/dist/optimize/rules.d.ts +8 -0
- package/dist/optimize/rules.d.ts.map +1 -0
- package/dist/optimize/rules.js +136 -0
- package/dist/optimize/rules.js.map +1 -0
- package/package.json +1 -1
- package/src/adapters/indexed.ts +60 -24
- package/src/context.ts +24 -0
- package/src/engine.ts +303 -33
- package/src/index.ts +6 -1
- package/src/optimize/analysis.ts +183 -0
- package/src/optimize/driver.ts +37 -0
- package/src/optimize/hash-index.ts +91 -0
- package/src/optimize/index.ts +16 -0
- package/src/optimize/ir.ts +128 -0
- package/src/optimize/rules.ts +136 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Rule } from "./ir.js";
|
|
2
|
+
export declare const correlatedEqualityProbe: Rule;
|
|
3
|
+
export declare const stableReceiver: Rule;
|
|
4
|
+
export declare const invariantBlock: Rule;
|
|
5
|
+
export declare const cardinalityOnly: Rule;
|
|
6
|
+
/** The rules the engine applies by default, in order. */
|
|
7
|
+
export declare const DEFAULT_RULES: readonly Rule[];
|
|
8
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/optimize/rules.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAe,IAAI,EAAe,MAAM,SAAS,CAAC;AA2B9D,eAAO,MAAM,uBAAuB,EAAE,IAgBrC,CAAC;AA6BF,eAAO,MAAM,cAAc,EAAE,IAI5B,CAAC;AAiBF,eAAO,MAAM,cAAc,EAAE,IAK5B,CAAC;AAkBF,eAAO,MAAM,eAAe,EAAE,IAM7B,CAAC;AAEF,yDAAyD;AACzD,eAAO,MAAM,aAAa,EAAE,SAAS,IAAI,EAA+E,CAAC"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The rule catalog. Each rule states its precondition and the argument that the
|
|
2
|
+
// rewritten plan is indistinguishable from the scan it replaces — same rows in
|
|
3
|
+
// the same order, same lifts, same error (same message) at the same point, or
|
|
4
|
+
// no error — under SEMANTICS §13's strict left-to-right `&&`.
|
|
5
|
+
import { exprRaiseFree, exprReadsCurrentScope, exprReadsScopeIn, localPath, opHasLifts, opReadsScopeIn, whereHasLifts, whereRaiseFree, } from "./analysis.js";
|
|
6
|
+
import { conjunction, conjuncts } from "./ir.js";
|
|
7
|
+
// ---- rule 1: correlated equality → hash probe ------------------------------------
|
|
8
|
+
//
|
|
9
|
+
// Precondition. The block has no `from` re-projection (its rows ARE the
|
|
10
|
+
// receiver's rows) and no `follow`; its `where` has no lift anywhere (a lift
|
|
11
|
+
// into an enclosing scope by one row could change what the outer side reads
|
|
12
|
+
// for the next row); some top-level conjunct k is `local == outer` (either
|
|
13
|
+
// side) with `local` an identifier/member chain on the block's row and `outer`
|
|
14
|
+
// a raise-free expression reading nothing from the block's row; and every
|
|
15
|
+
// conjunct left of k is raise-free (no call, no `single`, no lift, no bound, no
|
|
16
|
+
// binding past the run's count).
|
|
17
|
+
//
|
|
18
|
+
// Argument. The scan evaluates, per receiver row r in order, c1(r) && … &&
|
|
19
|
+
// ck(r) && …, short-circuiting. The probe selects exactly the rows with
|
|
20
|
+
// local(r) == outer (the index reproduces §5 equality, `outer` is evaluated
|
|
21
|
+
// once because nothing in the block can change what it reads) and evaluates the
|
|
22
|
+
// residual conjunction over them, in receiver order. For a selected row every
|
|
23
|
+
// conjunct is evaluated as the scan would (ck is dropped: it is true by
|
|
24
|
+
// construction and, being two reads, cannot raise or bind). For a row the probe
|
|
25
|
+
// rejects, the scan would have evaluated c1..c(k-1) — which cannot raise or
|
|
26
|
+
// bind — then ck, false, and stopped: no result, no error, no lift. So the two
|
|
27
|
+
// paths keep the same rows, raise the same first error, and bind the same
|
|
28
|
+
// lifts. A probe that cannot be built at run time (a receiver that is not
|
|
29
|
+
// stable, an index whose construction throws) falls back to the scan itself.
|
|
30
|
+
export const correlatedEqualityProbe = (plan, ctx) => {
|
|
31
|
+
if (plan.correlated.length > 0 || plan.from.length > 0 || plan.follow || !plan.where)
|
|
32
|
+
return null;
|
|
33
|
+
if (whereHasLifts(plan.where))
|
|
34
|
+
return null;
|
|
35
|
+
const correlated = [];
|
|
36
|
+
const residual = [];
|
|
37
|
+
let prefixRaiseFree = true;
|
|
38
|
+
conjuncts(plan.where).forEach((part, index) => {
|
|
39
|
+
const eq = prefixRaiseFree ? asCorrelation(part, index, ctx) : null;
|
|
40
|
+
if (eq)
|
|
41
|
+
correlated.push(eq);
|
|
42
|
+
else {
|
|
43
|
+
residual.push(part);
|
|
44
|
+
prefixRaiseFree &&= whereRaiseFree(part, ctx);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
if (correlated.length === 0)
|
|
48
|
+
return null;
|
|
49
|
+
return { ...plan, correlated, residual: conjunction(residual) };
|
|
50
|
+
};
|
|
51
|
+
/** `local == outer` / `outer == local` as a correlation, else null. */
|
|
52
|
+
function asCorrelation(w, index, ctx) {
|
|
53
|
+
if (w.kind !== "scalar" || w.expr.kind !== "binary" || w.expr.op !== "==")
|
|
54
|
+
return null;
|
|
55
|
+
const { left, right } = w.expr;
|
|
56
|
+
return pair(left, right, index, ctx) ?? pair(right, left, index, ctx);
|
|
57
|
+
}
|
|
58
|
+
function pair(local, outer, index, ctx) {
|
|
59
|
+
const path = localPath(local);
|
|
60
|
+
if (path === null)
|
|
61
|
+
return null;
|
|
62
|
+
if (exprReadsCurrentScope(outer) || !exprRaiseFree(outer, ctx))
|
|
63
|
+
return null;
|
|
64
|
+
return { local, outer, path, index };
|
|
65
|
+
}
|
|
66
|
+
// ---- rule 1b: a receiver stable across enclosing rows ----------------------------
|
|
67
|
+
//
|
|
68
|
+
// Precondition. The receiver expression, read in the enclosing scope (depth
|
|
69
|
+
// `plan.depth`), reads no scope at depth 1..depth: only the root (named roots,
|
|
70
|
+
// `^…^name` reaching it, or past it) and bindings.
|
|
71
|
+
//
|
|
72
|
+
// Argument. Such an expression yields the same value for every enclosing row
|
|
73
|
+
// (evaluation is pure; the root and the bindings do not change within a run),
|
|
74
|
+
// so its rows can be materialized once per run and indexed once per path. A
|
|
75
|
+
// receiver that does read an enclosing row is still indexable when it happens
|
|
76
|
+
// to evaluate to the same object twice (the engine keys indexes by collection
|
|
77
|
+
// identity as well); a receiver that yields a fresh value per row is scanned.
|
|
78
|
+
export const stableReceiver = (plan) => {
|
|
79
|
+
if (plan.receiverStable)
|
|
80
|
+
return null;
|
|
81
|
+
if (exprReadsScopeIn(plan.receiver, plan.depth, plan.depth))
|
|
82
|
+
return null;
|
|
83
|
+
return { ...plan, receiverStable: true };
|
|
84
|
+
};
|
|
85
|
+
// ---- rule 2: invariant nested block → evaluate once per run ----------------------
|
|
86
|
+
//
|
|
87
|
+
// Precondition. Nothing in the block — receiver, `from`, `where`, `select`,
|
|
88
|
+
// `order by`, bound, `follow` — reads a scope at depth 1..depth (an enclosing
|
|
89
|
+
// row), and the block lifts nothing (a lift is a side effect on an enclosing
|
|
90
|
+
// scope that must recur per row).
|
|
91
|
+
//
|
|
92
|
+
// Argument. The block's value is a function of the root scope, the bindings
|
|
93
|
+
// and its own rows only, all constant within a run, so every evaluation yields
|
|
94
|
+
// the same value or raises the same error. The engine evaluates it the first
|
|
95
|
+
// time the scan would (so an error surfaces at the same point) and returns the
|
|
96
|
+
// memoized value afterwards; a run that raised never reaches a second
|
|
97
|
+
// evaluation. The memoized value is shared by reference across the enclosing
|
|
98
|
+
// rows that project it.
|
|
99
|
+
export const invariantBlock = (plan) => {
|
|
100
|
+
if (plan.invariant)
|
|
101
|
+
return null;
|
|
102
|
+
if (opHasLifts(plan.node))
|
|
103
|
+
return null;
|
|
104
|
+
if (opReadsScopeIn(plan.node, plan.depth, plan.depth))
|
|
105
|
+
return null;
|
|
106
|
+
return { ...plan, invariant: true };
|
|
107
|
+
};
|
|
108
|
+
// ---- rule 3: exists / none / count from cardinality -------------------------------
|
|
109
|
+
//
|
|
110
|
+
// Precondition. A where-position `exists`, `none` or `count` without `distinct`
|
|
111
|
+
// (dedup projects every row), without `order by` (`count` evaluates the keys),
|
|
112
|
+
// without `follow`, and with nothing left to evaluate per row: either the block
|
|
113
|
+
// has no `where`, or every conjunct is correlated (so the probe's bucket is the
|
|
114
|
+
// matched set).
|
|
115
|
+
//
|
|
116
|
+
// Argument. With no residual predicate the matched rows are exactly the
|
|
117
|
+
// accessed rows (§13: the receiver's rows filtered by `where`), entering a row
|
|
118
|
+
// as a scope cannot raise, and the consumers only need the count after the
|
|
119
|
+
// bound (§18): `count` is `boundedCount(n)`, `exists` is `boundedCount(n) > 0`,
|
|
120
|
+
// `none` its negation — what the scan computes after materializing. When the
|
|
121
|
+
// probe is unavailable at run time the residual is the whole `where` and the
|
|
122
|
+
// engine takes the per-row path.
|
|
123
|
+
export const cardinalityOnly = (plan) => {
|
|
124
|
+
if (plan.fromCardinality)
|
|
125
|
+
return null;
|
|
126
|
+
if (plan.consumer !== "exists" && plan.consumer !== "none" && plan.consumer !== "count")
|
|
127
|
+
return null;
|
|
128
|
+
if (plan.distinct || plan.orderBy || plan.follow)
|
|
129
|
+
return null;
|
|
130
|
+
if (plan.where !== null && plan.residual !== null)
|
|
131
|
+
return null;
|
|
132
|
+
return { ...plan, fromCardinality: true };
|
|
133
|
+
};
|
|
134
|
+
/** The rules the engine applies by default, in order. */
|
|
135
|
+
export const DEFAULT_RULES = [correlatedEqualityProbe, stableReceiver, invariantBlock, cardinalityOnly];
|
|
136
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/optimize/rules.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/adapters/indexed.ts
CHANGED
|
@@ -1,38 +1,66 @@
|
|
|
1
1
|
// An in-memory optimizing planner: it hash-indexes a named root collection on
|
|
2
|
-
// chosen fields and, for a query whose where-clause contains equality
|
|
3
|
-
// on those fields, answers from the index (candidate intersection)
|
|
4
|
-
// full scan. Everything it can't turn into an index probe is left
|
|
5
|
-
// query the in-memory engine finishes over the candidate rows.
|
|
2
|
+
// chosen fields and, for a query whose where-clause contains equality
|
|
3
|
+
// predicates on those fields, answers from the index (candidate intersection)
|
|
4
|
+
// instead of a full scan. Everything it can't turn into an index probe is left
|
|
5
|
+
// as a residual query the in-memory engine finishes over the candidate rows.
|
|
6
6
|
//
|
|
7
7
|
// This is the smallest honest demonstration of plan optimization: same answers
|
|
8
8
|
// as a naive scan, far fewer rows examined, and partial-pushdown correctness via
|
|
9
9
|
// the residual.
|
|
10
|
+
//
|
|
11
|
+
// The same indexes serve the engine's correlated probes: `context()` is a
|
|
12
|
+
// `DataContext` that resolves the root and answers `indexFor(collection, path)`
|
|
13
|
+
// from them, so a nested `^emp first { where id == ^manager_id }` probes the
|
|
14
|
+
// pre-built index instead of building one per run. The planner's own plans
|
|
15
|
+
// carry that context too, so a residual's nested blocks still see every root.
|
|
16
|
+
//
|
|
17
|
+
// Buckets are keyed under OQX equality (`HashIndex`): absent ≡ null, `-0` ≡ `0`,
|
|
18
|
+
// `NaN` matches nothing, objects by reference — exactly what `==` compares, so
|
|
19
|
+
// a probe returns the rows the scan would keep (a plain `Map` would keep `null`
|
|
20
|
+
// and `undefined` apart and match `NaN` to itself).
|
|
10
21
|
|
|
11
22
|
import type { Query } from "../ast.ts";
|
|
23
|
+
import type { DataContext } from "../context.ts";
|
|
24
|
+
import { DefaultContext } from "../context.ts";
|
|
12
25
|
import type { Plan, QueryPlanner } from "../planner.ts";
|
|
13
|
-
import { partitionPushable, residualQuery, asEquality, constValue } from "../plan.ts";
|
|
26
|
+
import { partitionPushable, residualQuery, asEquality, constValue, ROWS_ROOT } from "../plan.ts";
|
|
27
|
+
import type { RowIndex } from "../optimize/hash-index.ts";
|
|
28
|
+
import { HashIndex, intersectPositions } from "../optimize/hash-index.ts";
|
|
14
29
|
|
|
15
30
|
export class IndexedCollection implements QueryPlanner {
|
|
16
31
|
private name: string;
|
|
17
32
|
private rows: readonly unknown[];
|
|
18
|
-
private indexes = new Map<string,
|
|
33
|
+
private indexes = new Map<string, HashIndex>();
|
|
19
34
|
|
|
20
|
-
/** Index `rows` (exposed as root `name`) on each field in `indexFields`.
|
|
35
|
+
/** Index `rows` (exposed as root `name`) on each field in `indexFields`. A
|
|
36
|
+
* field is read as the engine reads a bare identifier (own property; array
|
|
37
|
+
* element for an integer-spelled name); a row without it keys as absent. */
|
|
21
38
|
constructor(name: string, rows: readonly unknown[], indexFields: readonly string[]) {
|
|
22
39
|
this.name = name;
|
|
23
40
|
this.rows = rows;
|
|
41
|
+
const read = new DefaultContext();
|
|
24
42
|
for (const field of indexFields) {
|
|
25
|
-
const idx = new
|
|
26
|
-
|
|
27
|
-
const key = (row as Record<string, unknown>)?.[field];
|
|
28
|
-
let bucket = idx.get(key);
|
|
29
|
-
if (!bucket) idx.set(key, (bucket = []));
|
|
30
|
-
bucket.push(row);
|
|
31
|
-
}
|
|
43
|
+
const idx = new HashIndex();
|
|
44
|
+
rows.forEach((row, i) => idx.add(read.get(row, field), i));
|
|
32
45
|
this.indexes.set(field, idx);
|
|
33
46
|
}
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
/** The pre-built index for `collection` on `path` when `collection` is this
|
|
50
|
+
* collection's rows and `path` is one indexed field — the `DataContext.indexFor`
|
|
51
|
+
* seam, which `context()` wires up. */
|
|
52
|
+
indexFor(collection: unknown, path: readonly string[]): RowIndex | undefined {
|
|
53
|
+
if (collection !== this.rows || path.length !== 1) return undefined;
|
|
54
|
+
return this.indexes.get(path[0]!);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A `DataContext` over plain objects that serves this collection as the
|
|
58
|
+
* root `name` (plus `extraRoots`) and exposes the indexes through
|
|
59
|
+
* `indexFor`, so the engine's correlated probes reuse them. */
|
|
60
|
+
context(extraRoots: Record<string, unknown> = {}): DataContext {
|
|
61
|
+
return new IndexedContext(this, { ...extraRoots, [this.name]: this.rows });
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
plan(query: Query, params: readonly unknown[]): Plan | null {
|
|
37
65
|
if (query.source.kind !== "ident" || query.source.name !== this.name) return null;
|
|
38
66
|
if (query.from.length > 0 || query.follow) return null;
|
|
@@ -44,18 +72,26 @@ export class IndexedCollection implements QueryPlanner {
|
|
|
44
72
|
if (pushed.length === 0) return null; // no index probe available — let the scan handle it
|
|
45
73
|
|
|
46
74
|
// Intersect the candidate sets from each indexed equality (smallest first).
|
|
47
|
-
|
|
48
|
-
for (const e of pushed) {
|
|
75
|
+
const buckets = pushed.map((e) => {
|
|
49
76
|
const eq = asEquality(e)!;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
77
|
+
return this.indexes.get(eq.field)!.lookup(constValue(eq.value, params));
|
|
78
|
+
}).sort((a, b) => a.length - b.length);
|
|
79
|
+
let candidate = buckets[0]!;
|
|
80
|
+
for (let i = 1; i < buckets.length && candidate.length > 0; i++) candidate = intersectPositions(candidate, buckets[i]!);
|
|
81
|
+
const rows = candidate.map((i) => this.rows[i]);
|
|
82
|
+
return { rows: () => rows, residual: residualQuery(query, residual), context: this.context({ [ROWS_ROOT]: rows }) };
|
|
55
83
|
}
|
|
56
84
|
}
|
|
57
85
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
class IndexedContext extends DefaultContext {
|
|
87
|
+
private owner: IndexedCollection;
|
|
88
|
+
|
|
89
|
+
constructor(owner: IndexedCollection, roots: Record<string, unknown>) {
|
|
90
|
+
super(roots);
|
|
91
|
+
this.owner = owner;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
indexFor(collection: unknown, path: readonly string[]): RowIndex | undefined {
|
|
95
|
+
return this.owner.indexFor(collection, path);
|
|
96
|
+
}
|
|
61
97
|
}
|
package/src/context.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { coerceCollection, regexMatches, BUILTIN_FUNCTIONS, BUILTIN_METHODS } from "./semantics.ts";
|
|
12
12
|
import type { RegexDialect } from "./regex.ts";
|
|
13
|
+
import type { RowIndex } from "./optimize/hash-index.ts";
|
|
13
14
|
|
|
14
15
|
// An array's only properties are its integer indices, spelled canonically
|
|
15
16
|
// (`"0"`, `"12"`; never `"01"`, `"length"`, or a method name).
|
|
@@ -36,6 +37,18 @@ export interface DataContext {
|
|
|
36
37
|
get(row: unknown, key: string): unknown;
|
|
37
38
|
/** Coerce a relation/source value into rows (may be lazy). */
|
|
38
39
|
toRows(value: unknown): Iterable<unknown>;
|
|
40
|
+
/** Optional: a value this context handed out as a stand-in for a collection
|
|
41
|
+
* it has not read yet (a lazy table handle), resolved to what it stands for.
|
|
42
|
+
* The engine calls it on every value it is about to observe AS A VALUE — an
|
|
43
|
+
* operand of `==`/`in`/arithmetic, a function or method argument, a projected
|
|
44
|
+
* item, an `order by` or `distinct` key, a `where` scalar, a lift — and never
|
|
45
|
+
* on a value it reads in ROW POSITION (the query source, a block receiver, a
|
|
46
|
+
* body-level `from`, a `follow` destination), which reaches `toRows` and
|
|
47
|
+
* `indexFor` as handed out, so a store-backed context can answer a probe on
|
|
48
|
+
* the handle without reading the table and still never lets the stand-in be
|
|
49
|
+
* seen by the language. Absent = identity; a context whose handle already
|
|
50
|
+
* behaves as the collection (a `Proxy` over an array) needs none. */
|
|
51
|
+
materialize?(value: unknown): unknown;
|
|
39
52
|
/** Identity of a row for `follow` cycle detection / dedup. */
|
|
40
53
|
identity(row: unknown): unknown;
|
|
41
54
|
/** Optional custom free function; return `{ handled: false }` to defer. */
|
|
@@ -50,6 +63,17 @@ export interface DataContext {
|
|
|
50
63
|
* `regexMatches(recv, args, dialect)` is the helper). `BUILTIN_METHODS.matches`
|
|
51
64
|
* is always the baseline. */
|
|
52
65
|
readonly regexDialect?: RegexDialect;
|
|
66
|
+
/** Optional: a pre-built equality index over `collection` (a value this
|
|
67
|
+
* context served as a root or relation) on the property path `path`
|
|
68
|
+
* (`["customer_id"]`, `["meta", "id"]`; `[]` keys by the row itself). The
|
|
69
|
+
* engine asks before building its own hash index for a correlated equality
|
|
70
|
+
* in a nested block (`where id == ^customer_id`); return `undefined` to let
|
|
71
|
+
* it build one. `lookup(value)` must return the ascending positions, into
|
|
72
|
+
* `toRows(collection)` in order, of the rows whose value at `path` equals
|
|
73
|
+
* `value` under OQX equality (SEMANTICS §5: absent ≡ null, `-0` ≡ `0`, `NaN`
|
|
74
|
+
* matches nothing, objects by reference). `IndexedCollection.context()`
|
|
75
|
+
* implements it over its indexes. */
|
|
76
|
+
indexFor?(collection: unknown, path: readonly string[]): RowIndex | undefined;
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
export interface DefaultContextOptions {
|