@omgbase/oqx 0.24.0 → 0.26.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 +13 -0
- package/README.md +267 -202
- package/dist/ast.d.ts +19 -1
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +3 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +4 -2
- package/dist/build.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +95 -44
- package/dist/engine.js.map +1 -1
- package/dist/optimize/analysis.d.ts +3 -2
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +12 -2
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +4 -1
- package/dist/optimize/ir.d.ts.map +1 -1
- package/dist/optimize/ir.js +1 -0
- package/dist/optimize/ir.js.map +1 -1
- package/dist/optimize/rules.d.ts.map +1 -1
- package/dist/optimize/rules.js +3 -2
- package/dist/optimize/rules.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +206 -42
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts +2 -2
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +88 -25
- package/dist/print.js.map +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +39 -1
- package/dist/references.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +9 -2
- package/dist/resolve.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walk.d.ts +1 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +5 -0
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +20 -1
- package/src/build.ts +5 -3
- package/src/engine.ts +89 -39
- package/src/optimize/analysis.ts +9 -2
- package/src/optimize/ir.ts +5 -1
- package/src/optimize/rules.ts +3 -2
- package/src/parser.ts +193 -41
- package/src/print.ts +92 -30
- package/src/references.ts +34 -2
- package/src/resolve.ts +10 -3
- package/src/version.ts +1 -1
- package/src/walk.ts +6 -0
package/src/engine.ts
CHANGED
|
@@ -139,6 +139,8 @@ const HARD_DEPTH_CAP = 8;
|
|
|
139
139
|
type Projection = Pick<Subquery, "select" | "values">;
|
|
140
140
|
// The grouping clauses of a body (0.24). Both `Query` and `Subquery` carry them.
|
|
141
141
|
type Grouping = Pick<Subquery, "groupBy" | "having">;
|
|
142
|
+
// The `flatten` clauses of a body (0.25). Both `Query` and `Subquery` carry them.
|
|
143
|
+
type Flattening = Pick<Subquery, "flatten">;
|
|
142
144
|
|
|
143
145
|
// The `$key: expr` item of a projection, if any — the identity the rows it
|
|
144
146
|
// yields carry (SEMANTICS §16, since 0.20).
|
|
@@ -182,18 +184,19 @@ export class InMemoryEngine implements Engine {
|
|
|
182
184
|
const src = this.coerceRows(this.evalRaw(query.source, root), query.source);
|
|
183
185
|
const rows = this.reproject(src.rows, query.from, root);
|
|
184
186
|
// One object as the source yields one record (SEMANTICS §3) — unless a
|
|
185
|
-
// `from` re-projection or a
|
|
186
|
-
const single = src.single && query.from.length === 0 && !query.follow;
|
|
187
|
+
// `from` re-projection, a walk or a `flatten` fans it out.
|
|
188
|
+
const single = src.single && query.from.length === 0 && !query.follow && query.flatten.length === 0;
|
|
187
189
|
|
|
188
190
|
const bound = this.boundOf(query, root);
|
|
189
191
|
if (query.follow) return this.runFollow(query, rows, root, bound);
|
|
190
192
|
|
|
191
193
|
// Consumer-directed short-circuits (skipped under `distinct`, which must
|
|
192
|
-
// materialize + dedup by projection before reducing,
|
|
193
|
-
// whose consumer counts groups
|
|
194
|
-
//
|
|
195
|
-
// (offset+1)th match — or
|
|
196
|
-
|
|
194
|
+
// materialize + dedup by projection before reducing, under `group by`,
|
|
195
|
+
// whose consumer counts groups, and under `flatten`, whose consumer counts
|
|
196
|
+
// flattened rows). Counting never materializes rows; exists/none stop as
|
|
197
|
+
// soon as the bound is known to be non-empty (the (offset+1)th match — or
|
|
198
|
+
// the first, when unbounded).
|
|
199
|
+
if (!query.distinct && !query.groupBy && query.flatten.length === 0 && (query.consumer === "exists" || query.consumer === "none" || query.consumer === "count")) {
|
|
197
200
|
const need = query.consumer === "count" ? Infinity : bound.offset + 1;
|
|
198
201
|
let n = 0;
|
|
199
202
|
for (const r of rows) {
|
|
@@ -208,7 +211,7 @@ export class InMemoryEngine implements Engine {
|
|
|
208
211
|
// first over an unordered, non-distinct set needs only the rows up to the
|
|
209
212
|
// bound (offset + 1). single materializes everything so its error can
|
|
210
213
|
// report how many rows actually matched.
|
|
211
|
-
const cap = query.consumer === "first" && !query.orderBy && !query.distinct && !query.groupBy
|
|
214
|
+
const cap = query.consumer === "first" && !query.orderBy && !query.distinct && !query.groupBy && query.flatten.length === 0
|
|
212
215
|
? bound.offset + Math.min(1, bound.limit ?? 1)
|
|
213
216
|
: Infinity;
|
|
214
217
|
let kept: Scope[] = [];
|
|
@@ -219,6 +222,7 @@ export class InMemoryEngine implements Engine {
|
|
|
219
222
|
if (kept.length >= cap) break;
|
|
220
223
|
}
|
|
221
224
|
}
|
|
225
|
+
if (query.flatten.length > 0) kept = this.flattenScopes(kept, query);
|
|
222
226
|
if (query.groupBy) kept = this.groupScopes(kept, query, root);
|
|
223
227
|
this.applyKey(kept, query);
|
|
224
228
|
this.sortScopes(kept, query.orderBy);
|
|
@@ -329,6 +333,44 @@ export class InMemoryEngine implements Engine {
|
|
|
329
333
|
return s;
|
|
330
334
|
}
|
|
331
335
|
|
|
336
|
+
// ---- flatten (SEMANTICS §25) -----------------------------------------------
|
|
337
|
+
|
|
338
|
+
// Each `flatten` clause, in writing order, fans the scopes out: the expression
|
|
339
|
+
// is read in the row's scope (as `where` is) and its value split — an array
|
|
340
|
+
// (or a host collection) → one scope per element in collection order, so an
|
|
341
|
+
// empty collection yields none; absent → one scope with the name absent;
|
|
342
|
+
// anything else (an object, a scalar, a range) → one scope with the value.
|
|
343
|
+
// A flattened scope IS the parent scope — the same row, parent, lifts and
|
|
344
|
+
// intrinsics, so `$it`, `^` and `$depth` are unchanged — with the name bound
|
|
345
|
+
// beside the lifts (step 4 of §2: it shadows a same-named row property) and
|
|
346
|
+
// `$key` the pair `[parent identity, element identity]` (§16): an element's
|
|
347
|
+
// identity is the host's (an entry's key), else its structural value.
|
|
348
|
+
private flattenScopes(scopes: Scope[], body: Flattening): Scope[] {
|
|
349
|
+
for (const f of body.flatten) {
|
|
350
|
+
const out: Scope[] = [];
|
|
351
|
+
for (const s of scopes) {
|
|
352
|
+
const parentKey = this.scopeIdentity(s);
|
|
353
|
+
for (const elem of this.flattenElements(this.evalRaw(f.expr, s))) {
|
|
354
|
+
const key = [parentKey, elem == null ? null : this.hostIdentity(elem)];
|
|
355
|
+
out.push({ ...s, lifts: { ...(s.lifts ?? {}), [f.name]: elem }, meta: { ...(s.meta ?? {}), [KEY]: key } });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
scopes = out;
|
|
359
|
+
}
|
|
360
|
+
return scopes;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// The elements a flattened value splits into (§25): §3's coercion with one
|
|
364
|
+
// softening — a scalar is one element, not an error, since no rows are
|
|
365
|
+
// sourced from it — and absent as one absent element rather than none.
|
|
366
|
+
private flattenElements(v: unknown): unknown[] {
|
|
367
|
+
if (v == null) return [undefined];
|
|
368
|
+
if (typeof v !== "object" || isRange(v) || isEntry(v)) return [v];
|
|
369
|
+
if (Array.isArray(v)) return v;
|
|
370
|
+
const single = this.ctx.isSingle ? this.ctx.isSingle(v) : !(Symbol.iterator in v);
|
|
371
|
+
return single ? [v] : Array.from(this.ctx.toRows(v));
|
|
372
|
+
}
|
|
373
|
+
|
|
332
374
|
// ---- grouping (SEMANTICS §24) ----------------------------------------------
|
|
333
375
|
|
|
334
376
|
// The kept scopes of a body, one GROUP scope per distinct key tuple, in order
|
|
@@ -425,6 +467,7 @@ export class InMemoryEngine implements Engine {
|
|
|
425
467
|
const occ = this.followWalk(this.keyed(seeds, query, root), query.follow!, query.frontier, root);
|
|
426
468
|
let scopes: Scope[] = occ.map((o) => this.enter(o.row, root, { lifts: {}, meta: o.meta }));
|
|
427
469
|
if (post) scopes = scopes.filter((s) => this.evalWhere(post, s));
|
|
470
|
+
if (query.flatten.length > 0) scopes = this.flattenScopes(scopes, query);
|
|
428
471
|
if (query.groupBy) scopes = this.groupScopes(scopes, query, root);
|
|
429
472
|
this.sortScopes(scopes, query.orderBy);
|
|
430
473
|
if (query.distinct) scopes = this.dedupByProjection(scopes, query);
|
|
@@ -633,7 +676,6 @@ export class InMemoryEngine implements Engine {
|
|
|
633
676
|
}
|
|
634
677
|
|
|
635
678
|
private evalWhereOp(op: OpNode, scope: Scope): boolean {
|
|
636
|
-
if (op.sub.follow) throw new OqxError("`follow` is only valid on a select-position collect { … }, not a where op", "eval");
|
|
637
679
|
const plan = this.planOf(op, scope);
|
|
638
680
|
if (!plan.invariant) return this.evalWhereOpPlanned(op, plan, scope);
|
|
639
681
|
return this.memoized(op, scope, () => this.evalWhereOpPlanned(op, plan, scope));
|
|
@@ -644,15 +686,21 @@ export class InMemoryEngine implements Engine {
|
|
|
644
686
|
if (op.op === "exists" || op.op === "none" || op.op === "count") {
|
|
645
687
|
const acc = this.access(op, plan, scope);
|
|
646
688
|
let n: number;
|
|
647
|
-
|
|
689
|
+
// A block with a `follow` (legal in every body since 0.26) yields its
|
|
690
|
+
// walk's occurrences: no cardinality answer, no first-match stop — the
|
|
691
|
+
// walk is run and counted like any other rows.
|
|
692
|
+
if (op.sub.follow) {
|
|
693
|
+
n = this.opRows(op, scope, bound, acc).length;
|
|
694
|
+
} else if (plan.fromCardinality && acc.where === null) {
|
|
648
695
|
// Nothing left to evaluate per row: the accessed rows ARE the matched
|
|
649
696
|
// rows, and the consumer only needs their count after the bound.
|
|
650
697
|
n = boundedCount(acc.rows.length, bound);
|
|
651
698
|
scope.run.trace?.({ kind: "cardinality", count: n });
|
|
652
|
-
} else if (op.op !== "count" && bound === UNBOUNDED && !op.sub.groupBy) {
|
|
699
|
+
} else if (op.op !== "count" && bound === UNBOUNDED && !op.sub.groupBy && op.sub.flatten.length === 0) {
|
|
653
700
|
// Unbounded exists/none: stop at the first match (dedup cannot change
|
|
654
|
-
// emptiness). Bounded,
|
|
655
|
-
//
|
|
701
|
+
// emptiness). Bounded, grouped (`having` may empty the groups) or
|
|
702
|
+
// flattened (an empty collection yields no row): the offset/limit
|
|
703
|
+
// decide emptiness, so materialize.
|
|
656
704
|
n = this.anyMatch(op, scope, acc) ? 1 : 0;
|
|
657
705
|
} else n = this.opRows(op, scope, bound, acc).length;
|
|
658
706
|
if (op.op === "count") return op.countCmp ? compareCount(n, op.countCmp) : n > 0;
|
|
@@ -677,13 +725,28 @@ export class InMemoryEngine implements Engine {
|
|
|
677
725
|
return this.opRows(op, scope, bound).length > 0;
|
|
678
726
|
}
|
|
679
727
|
|
|
680
|
-
// The rows a consumer op reduces
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
728
|
+
// The rows a consumer op reduces — in every position (a where-position test,
|
|
729
|
+
// a select-position directive, a destination block): matched (or, with a
|
|
730
|
+
// `follow`, the walk's occurrences over the matched seeds) → flattened →
|
|
731
|
+
// grouped → ordered → distinct → bounded. `destination` marks a follow
|
|
732
|
+
// destination block: its `frontier` is the destination's (read by the outer
|
|
733
|
+
// walk), never a seed frontier for a `follow` of its own.
|
|
734
|
+
private opRows(op: OpNode, scope: Scope, bound: Bound, acc?: Access, destination = false): Scope[] {
|
|
735
|
+
const sub = op.sub;
|
|
736
|
+
let scopes: Scope[];
|
|
737
|
+
if (sub.follow) {
|
|
738
|
+
// The seeds are the matched rows themselves (`followWalk` enters them); a
|
|
739
|
+
// follow block is never correlated, so `where` here is the whole predicate.
|
|
740
|
+
const { rows, where } = acc ?? this.access(op, this.planOf(op, scope), scope);
|
|
741
|
+
const seeds = where ? rows.filter((r) => this.evalWhere(where, this.enter(r, scope, { lifts: {} }))) : rows;
|
|
742
|
+
scopes = this.followWalk(this.keyed(seeds, sub, scope), sub.follow, destination ? null : sub.frontier, scope).map((o) => this.enter(o.row, scope, { meta: o.meta }));
|
|
743
|
+
} else scopes = this.matchRows(op, scope, acc);
|
|
744
|
+
if (sub.flatten.length > 0) scopes = this.flattenScopes(scopes, sub);
|
|
745
|
+
if (sub.groupBy) scopes = this.groupScopes(scopes, sub, scope);
|
|
746
|
+
// An occurrence's `$key` is the walk's identity (`keyed` gave the seeds theirs).
|
|
747
|
+
if (!sub.follow) this.applyKey(scopes, sub);
|
|
748
|
+
this.sortScopes(scopes, sub.orderBy);
|
|
749
|
+
if (op.distinct) scopes = this.dedupByProjection(scopes, sub);
|
|
687
750
|
return sliceBound(scopes, bound);
|
|
688
751
|
}
|
|
689
752
|
|
|
@@ -765,9 +828,9 @@ export class InMemoryEngine implements Engine {
|
|
|
765
828
|
}
|
|
766
829
|
|
|
767
830
|
// The plain scan: the rows, re-projected by `from`, with the whole `where`.
|
|
768
|
-
// A `from` re-projection fans a single receiver out: the block is plural.
|
|
831
|
+
// A `from` re-projection or a `flatten` fans a single receiver out: the block is plural.
|
|
769
832
|
private scan(src: Rows, op: OpNode, scope: Scope): Access {
|
|
770
|
-
return { rows: this.reproject(src.rows, op.sub.from, scope), where: op.sub.where, single: src.single && op.sub.from.length === 0 };
|
|
833
|
+
return { rows: this.reproject(src.rows, op.sub.from, scope), where: op.sub.where, single: src.single && op.sub.from.length === 0 && op.sub.flatten.length === 0 };
|
|
771
834
|
}
|
|
772
835
|
|
|
773
836
|
private reproject(rows: unknown[], from: readonly Expr[], scope: Scope): unknown[] {
|
|
@@ -883,23 +946,10 @@ export class InMemoryEngine implements Engine {
|
|
|
883
946
|
private evalCollectScopes(op: OpNode, scope: Scope, plan: BlockPlan = this.planOf(op, scope), destination = false): { scopes: Scope[]; single: boolean } {
|
|
884
947
|
const sub = op.sub;
|
|
885
948
|
const bound = this.boundOf(sub, scope);
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
// follow block is never correlated, so `where` here is the whole predicate.
|
|
891
|
-
const { rows, where } = this.access(op, plan, scope);
|
|
892
|
-
const seeds = where ? rows.filter((r) => this.evalWhere(where, this.enter(r, scope, { lifts: {} }))) : rows;
|
|
893
|
-
scopes = this.followWalk(this.keyed(seeds, sub, scope), sub.follow, destination ? null : sub.frontier, scope).map((o) => this.enter(o.row, scope, { meta: o.meta }));
|
|
894
|
-
if (sub.groupBy) scopes = this.groupScopes(scopes, sub, scope);
|
|
895
|
-
this.sortScopes(scopes, sub.orderBy);
|
|
896
|
-
if (op.distinct) scopes = this.dedupByProjection(scopes, sub);
|
|
897
|
-
scopes = sliceBound(scopes, bound);
|
|
898
|
-
} else {
|
|
899
|
-
const acc = this.access(op, plan, scope);
|
|
900
|
-
single = acc.single;
|
|
901
|
-
scopes = this.opRows(op, scope, bound, acc);
|
|
902
|
-
}
|
|
949
|
+
const acc = this.access(op, plan, scope);
|
|
950
|
+
// A walk is always plural (SEMANTICS §20).
|
|
951
|
+
const single = sub.follow ? false : acc.single;
|
|
952
|
+
const scopes = this.opRows(op, scope, bound, acc, destination);
|
|
903
953
|
switch (op.op) {
|
|
904
954
|
case "collect": case "count": return { scopes, single };
|
|
905
955
|
case "first": return { scopes: scopes.slice(0, 1), single };
|
package/src/optimize/analysis.ts
CHANGED
|
@@ -72,6 +72,8 @@ function subReadsScopeIn(sub: Subquery, at: number, upTo: number, destination: b
|
|
|
72
72
|
if (sub.limit && exprReadsScopeIn(sub.limit, at, upTo)) return true;
|
|
73
73
|
if (sub.offset && exprReadsScopeIn(sub.offset, at, upTo)) return true;
|
|
74
74
|
if (sub.frontier && exprReadsScopeIn(sub.frontier, destination ? at - 1 : at, upTo)) return true;
|
|
75
|
+
// A `flatten` expression is read on each row (0.25); its block is an op read there.
|
|
76
|
+
if (sub.flatten.some((f) => exprReadsScopeIn(f.expr, at, upTo))) return true;
|
|
75
77
|
// `group by` keys are read on the rows, `having` on the groups — both at `at` (0.24).
|
|
76
78
|
if (sub.groupBy?.keys.some((k) => exprReadsScopeIn(k.expr, at, upTo))) return true;
|
|
77
79
|
if (sub.having && whereReadsScopeIn(sub.having, at, upTo)) return true;
|
|
@@ -133,8 +135,9 @@ export function whereRaiseFree(w: Where, ctx: RuleContext): boolean {
|
|
|
133
135
|
|
|
134
136
|
/** A where-position directive that can neither raise nor bind: `exists`,
|
|
135
137
|
* `none` or `count` (never `single`, which raises on several rows; `collect`
|
|
136
|
-
* binds lifts; `first`/`single` project), without `follow` (
|
|
137
|
-
*
|
|
138
|
+
* binds lifts; `first`/`single` project), without `follow` (a walk's
|
|
139
|
+
* destinations may raise, and a walk is never a cardinality answer), without a
|
|
140
|
+
* bound (an invalid value raises), without `distinct`
|
|
138
141
|
* (dedup projects, and a projected range raises), whose receiver, `from`,
|
|
139
142
|
* `where` and `order by` (evaluated by `count`) are raise-free and which lifts
|
|
140
143
|
* nothing anywhere. */
|
|
@@ -148,6 +151,9 @@ export function opRaiseFree(op: OpNode, ctx: RuleContext): boolean {
|
|
|
148
151
|
if (!sub.from.every((e) => exprRaiseFree(e, ctx))) return false;
|
|
149
152
|
if (sub.where && !whereRaiseFree(sub.where, ctx)) return false;
|
|
150
153
|
if (sub.orderBy && !sub.orderBy.every((o) => exprRaiseFree(o.expr, ctx))) return false;
|
|
154
|
+
// A `flatten` value is coerced without error (a scalar is one row, §25), so
|
|
155
|
+
// the clause raises only when its expression does.
|
|
156
|
+
if (!sub.flatten.every((f) => exprRaiseFree(f.expr, ctx))) return false;
|
|
151
157
|
return !opHasLifts(op);
|
|
152
158
|
}
|
|
153
159
|
|
|
@@ -163,6 +169,7 @@ function subHasLifts(sub: Subquery): boolean {
|
|
|
163
169
|
if (sub.select.some((item) => (item.kind === "field" ? item.lift > 0 : opHasLifts(item.op)))) return true;
|
|
164
170
|
if (sub.where && whereHasLifts(sub.where)) return true;
|
|
165
171
|
if (sub.having && whereHasLifts(sub.having)) return true;
|
|
172
|
+
if (sub.flatten.some((f) => f.expr.kind === "op" && opHasLifts(f.expr))) return true;
|
|
166
173
|
if (sub.follow?.destinations.some((d) => d.kind === "op" && opHasLifts(d))) return true;
|
|
167
174
|
return false;
|
|
168
175
|
}
|
package/src/optimize/ir.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// cannot be built, a receiver that is not stable) always degrade to the plain
|
|
11
11
|
// scan the plan started from.
|
|
12
12
|
|
|
13
|
-
import type { Consumer, Expr, Follow, GroupBy, OpNode, OrderSpec, SelectItem, Where } from "../ast.ts";
|
|
13
|
+
import type { Consumer, Expr, Flatten, Follow, GroupBy, OpNode, OrderSpec, SelectItem, Where } from "../ast.ts";
|
|
14
14
|
|
|
15
15
|
/** One hoisted equality `local == outer` from the block's top-level `&&`
|
|
16
16
|
* conjunction: `local` reads only the block's row (an identifier or member
|
|
@@ -45,6 +45,9 @@ export interface BlockPlan {
|
|
|
45
45
|
readonly distinct: boolean;
|
|
46
46
|
readonly bound: { readonly limit: Expr | null; readonly offset: Expr | null };
|
|
47
47
|
readonly follow: Follow | null;
|
|
48
|
+
/** `flatten` clauses (0.25): the matched rows are fanned out per element
|
|
49
|
+
* before the consumer sees them, so a consumer counts FLATTENED rows. */
|
|
50
|
+
readonly flatten: readonly Flatten[];
|
|
48
51
|
/** `group by` / `having` (0.24): the matched rows are grouped before the
|
|
49
52
|
* consumer sees them, so a consumer counts GROUPS, never rows. */
|
|
50
53
|
readonly groupBy: GroupBy | null;
|
|
@@ -94,6 +97,7 @@ export function logicalBlock(node: OpNode, depth: number): BlockPlan {
|
|
|
94
97
|
distinct: node.distinct,
|
|
95
98
|
bound: { limit: sub.limit, offset: sub.offset },
|
|
96
99
|
follow: sub.follow,
|
|
100
|
+
flatten: sub.flatten,
|
|
97
101
|
groupBy: sub.groupBy,
|
|
98
102
|
having: sub.having,
|
|
99
103
|
correlated: [],
|
package/src/optimize/rules.ts
CHANGED
|
@@ -113,7 +113,8 @@ export const invariantBlock: Rule = (plan) => {
|
|
|
113
113
|
// Precondition. A where-position `exists`, `none` or `count` without `distinct`
|
|
114
114
|
// (dedup projects every row), without `order by` (`count` evaluates the keys),
|
|
115
115
|
// without `follow`, without `group by` (the consumer counts groups, not rows —
|
|
116
|
-
// 0.24),
|
|
116
|
+
// 0.24), without `flatten` (the consumer counts flattened rows — 0.25), and
|
|
117
|
+
// with nothing left to evaluate per row: either the block has no
|
|
117
118
|
// `where`, or every conjunct is correlated (so the probe's bucket is the
|
|
118
119
|
// matched set).
|
|
119
120
|
//
|
|
@@ -128,7 +129,7 @@ export const invariantBlock: Rule = (plan) => {
|
|
|
128
129
|
export const cardinalityOnly: Rule = (plan) => {
|
|
129
130
|
if (plan.fromCardinality) return null;
|
|
130
131
|
if (plan.consumer !== "exists" && plan.consumer !== "none" && plan.consumer !== "count") return null;
|
|
131
|
-
if (plan.distinct || plan.orderBy || plan.follow || plan.groupBy) return null;
|
|
132
|
+
if (plan.distinct || plan.orderBy || plan.follow || plan.groupBy || plan.flatten.length > 0) return null;
|
|
132
133
|
if (plan.where !== null && plan.residual !== null) return null;
|
|
133
134
|
return { ...plan, fromCardinality: true };
|
|
134
135
|
};
|