@quereus/quereus 4.3.2 → 4.4.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/dist/src/core/database-external-changes.d.ts +2 -2
- package/dist/src/core/database-external-changes.d.ts.map +1 -1
- package/dist/src/core/database-external-changes.js +12 -9
- package/dist/src/core/database-external-changes.js.map +1 -1
- package/dist/src/core/database-materialized-views-apply.d.ts +156 -14
- package/dist/src/core/database-materialized-views-apply.d.ts.map +1 -1
- package/dist/src/core/database-materialized-views-apply.js +370 -65
- package/dist/src/core/database-materialized-views-apply.js.map +1 -1
- package/dist/src/core/database-materialized-views-plan-builders.d.ts +65 -1
- package/dist/src/core/database-materialized-views-plan-builders.d.ts.map +1 -1
- package/dist/src/core/database-materialized-views-plan-builders.js +347 -8
- package/dist/src/core/database-materialized-views-plan-builders.js.map +1 -1
- package/dist/src/core/database-materialized-views-plans.d.ts +254 -30
- package/dist/src/core/database-materialized-views-plans.d.ts.map +1 -1
- package/dist/src/core/database-materialized-views.d.ts +96 -40
- package/dist/src/core/database-materialized-views.d.ts.map +1 -1
- package/dist/src/core/database-materialized-views.js +172 -116
- package/dist/src/core/database-materialized-views.js.map +1 -1
- package/dist/src/core/database.d.ts.map +1 -1
- package/dist/src/core/database.js +20 -17
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/func/builtins/aggregate.d.ts.map +1 -1
- package/dist/src/func/builtins/aggregate.js +127 -21
- package/dist/src/func/builtins/aggregate.js.map +1 -1
- package/dist/src/func/registration.d.ts +6 -1
- package/dist/src/func/registration.d.ts.map +1 -1
- package/dist/src/func/registration.js +16 -0
- package/dist/src/func/registration.js.map +1 -1
- package/dist/src/planner/analysis/equi-correlation.d.ts +26 -0
- package/dist/src/planner/analysis/equi-correlation.d.ts.map +1 -0
- package/dist/src/planner/analysis/equi-correlation.js +62 -0
- package/dist/src/planner/analysis/equi-correlation.js.map +1 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts +54 -18
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts.map +1 -1
- package/dist/src/planner/analysis/query-rewrite-matcher.js +86 -42
- package/dist/src/planner/analysis/query-rewrite-matcher.js.map +1 -1
- package/dist/src/planner/analysis/scalar-subqueries.d.ts +28 -0
- package/dist/src/planner/analysis/scalar-subqueries.d.ts.map +1 -0
- package/dist/src/planner/analysis/scalar-subqueries.js +60 -0
- package/dist/src/planner/analysis/scalar-subqueries.js.map +1 -0
- package/dist/src/planner/building/select.d.ts.map +1 -1
- package/dist/src/planner/building/select.js +15 -1
- package/dist/src/planner/building/select.js.map +1 -1
- package/dist/src/planner/building/with.d.ts.map +1 -1
- package/dist/src/planner/building/with.js +5 -8
- package/dist/src/planner/building/with.js.map +1 -1
- package/dist/src/planner/cache/materialization-advisory.d.ts +33 -0
- package/dist/src/planner/cache/materialization-advisory.d.ts.map +1 -1
- package/dist/src/planner/cache/materialization-advisory.js +164 -28
- package/dist/src/planner/cache/materialization-advisory.js.map +1 -1
- package/dist/src/planner/cache/reference-graph.d.ts +0 -4
- package/dist/src/planner/cache/reference-graph.d.ts.map +1 -1
- package/dist/src/planner/cache/reference-graph.js +5 -13
- package/dist/src/planner/cache/reference-graph.js.map +1 -1
- package/dist/src/planner/cost/index.d.ts +28 -7
- package/dist/src/planner/cost/index.d.ts.map +1 -1
- package/dist/src/planner/cost/index.js +26 -0
- package/dist/src/planner/cost/index.js.map +1 -1
- package/dist/src/planner/nodes/cache-node.d.ts +18 -2
- package/dist/src/planner/nodes/cache-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/cache-node.js +42 -3
- package/dist/src/planner/nodes/cache-node.js.map +1 -1
- package/dist/src/planner/nodes/cte-node.d.ts +15 -1
- package/dist/src/planner/nodes/cte-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/cte-node.js +12 -2
- package/dist/src/planner/nodes/cte-node.js.map +1 -1
- package/dist/src/planner/nodes/recursive-cte-node.d.ts +30 -1
- package/dist/src/planner/nodes/recursive-cte-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/recursive-cte-node.js +30 -3
- package/dist/src/planner/nodes/recursive-cte-node.js.map +1 -1
- package/dist/src/planner/optimizer.d.ts.map +1 -1
- package/dist/src/planner/optimizer.js +129 -1
- package/dist/src/planner/optimizer.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-cte-optimization.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-cte-optimization.js +8 -1
- package/dist/src/planner/rules/cache/rule-cte-optimization.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-in-subquery-cache.d.ts +7 -0
- package/dist/src/planner/rules/cache/rule-in-subquery-cache.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-in-subquery-cache.js +14 -1
- package/dist/src/planner/rules/cache/rule-in-subquery-cache.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js +53 -54
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-mutating-subquery-cache.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-mutating-subquery-cache.js +6 -0
- package/dist/src/planner/rules/cache/rule-mutating-subquery-cache.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.d.ts +25 -0
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.d.ts.map +1 -0
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.js +176 -0
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.js.map +1 -0
- package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.d.ts +29 -0
- package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.d.ts.map +1 -0
- package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.js +66 -0
- package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.js.map +1 -0
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.js +1 -54
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.js.map +1 -1
- package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.d.ts +191 -0
- package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.d.ts.map +1 -0
- package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.js +623 -0
- package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.js.map +1 -0
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.d.ts +10 -1
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.d.ts.map +1 -1
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js +242 -51
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js.map +1 -1
- package/dist/src/runtime/cache/shared-cache.d.ts +7 -0
- package/dist/src/runtime/cache/shared-cache.d.ts.map +1 -1
- package/dist/src/runtime/cache/shared-cache.js +34 -1
- package/dist/src/runtime/cache/shared-cache.js.map +1 -1
- package/dist/src/runtime/emit/aggregate.d.ts +2 -3
- package/dist/src/runtime/emit/aggregate.d.ts.map +1 -1
- package/dist/src/runtime/emit/aggregate.js +6 -15
- package/dist/src/runtime/emit/aggregate.js.map +1 -1
- package/dist/src/runtime/emit/cache.d.ts +7 -3
- package/dist/src/runtime/emit/cache.d.ts.map +1 -1
- package/dist/src/runtime/emit/cache.js +23 -6
- package/dist/src/runtime/emit/cache.js.map +1 -1
- package/dist/src/runtime/emit/cte.d.ts.map +1 -1
- package/dist/src/runtime/emit/cte.js +43 -20
- package/dist/src/runtime/emit/cte.js.map +1 -1
- package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
- package/dist/src/runtime/emit/dml-executor.js +68 -43
- package/dist/src/runtime/emit/dml-executor.js.map +1 -1
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
- package/dist/src/runtime/emit/materialized-view-helpers.js +5 -4
- package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
- package/dist/src/runtime/emit/recursive-cte.d.ts.map +1 -1
- package/dist/src/runtime/emit/recursive-cte.js +59 -1
- package/dist/src/runtime/emit/recursive-cte.js.map +1 -1
- package/dist/src/runtime/parallel-driver.d.ts +15 -1
- package/dist/src/runtime/parallel-driver.d.ts.map +1 -1
- package/dist/src/runtime/parallel-driver.js +17 -1
- package/dist/src/runtime/parallel-driver.js.map +1 -1
- package/dist/src/runtime/types.d.ts +36 -0
- package/dist/src/runtime/types.d.ts.map +1 -1
- package/dist/src/runtime/types.js.map +1 -1
- package/dist/src/schema/function.d.ts +79 -0
- package/dist/src/schema/function.d.ts.map +1 -1
- package/dist/src/schema/function.js.map +1 -1
- package/dist/src/vtab/memory/layer/scan-layer.js +21 -2
- package/dist/src/vtab/memory/layer/scan-layer.js.map +1 -1
- package/package.json +3 -3
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* the sole caller in the manager is `registerMaterializedView` → {@link buildMaintenancePlan}.
|
|
7
7
|
*/
|
|
8
8
|
import { BlockNode } from '../planner/nodes/block.js';
|
|
9
|
+
import { type ScalarPlanNode } from '../planner/nodes/plan-node.js';
|
|
9
10
|
import { TableReferenceNode } from '../planner/nodes/reference.js';
|
|
10
11
|
import { Scheduler } from '../runtime/scheduler.js';
|
|
11
12
|
import { type MaintenanceSourceStats } from '../planner/cost/index.js';
|
|
12
13
|
import type { MaintainedTableSchema } from '../schema/derivation.js';
|
|
13
14
|
import type { TableSchema } from '../schema/table.js';
|
|
14
15
|
import { type AggregateLike } from './database-materialized-views-analysis.js';
|
|
15
|
-
import type { MaterializedViewManagerContext, MaintenancePlan, FullRebuildPlan } from './database-materialized-views-plans.js';
|
|
16
|
+
import type { MaterializedViewManagerContext, MaintenancePlan, FullRebuildPlan, BackingPkColumn, DeltaAggregateDescriptor } from './database-materialized-views-plans.js';
|
|
16
17
|
/**
|
|
17
18
|
* Build the row-time maintenance plan for an MV — **cost-gated, with a floor, never a
|
|
18
19
|
* shape allowlist**. The builder tries to match a bounded-delta arm by shape
|
|
@@ -86,6 +87,69 @@ export declare function buildInverseProjectionPlan(ctx: MaterializedViewManagerC
|
|
|
86
87
|
* the backing is keyed by the group key regardless of whether it is a source key.
|
|
87
88
|
*/
|
|
88
89
|
export declare function buildAggregateResidualPlan(ctx: MaterializedViewManagerContext, mv: MaintainedTableSchema, analyzed: BlockNode, tableRef: TableReferenceNode, sourceBase: string, aggregate: AggregateLike): MaintenancePlan | null;
|
|
90
|
+
/**
|
|
91
|
+
* Build the {@link DeltaAggregateDescriptor} for a single-source aggregate body, or
|
|
92
|
+
* return `undefined` when any column fails the delta eligibility gate (the arm then
|
|
93
|
+
* stays a plain residual — a gate failure is never an error). Function-generic by
|
|
94
|
+
* construction: every judgment reads the aggregate call node's registry-resolved
|
|
95
|
+
* schema and its DECLARED {@link AggregateAlgebra} — never an aggregate-name list.
|
|
96
|
+
*
|
|
97
|
+
* The gate, per stored backing column:
|
|
98
|
+
* - a **group-key passthrough** column must resolve (transitively) to a GROUP BY
|
|
99
|
+
* source column that also backs a backing-PK position (so its flush value is the
|
|
100
|
+
* group key itself);
|
|
101
|
+
* - a directly **delta-maintainable aggregate** column must be a plain call (`agg(x)` /
|
|
102
|
+
* `agg()` — no DISTINCT, FILTER, ORDER BY, multi-arg) whose declared algebra has
|
|
103
|
+
* `merge` + `negate` + `decode`, with a **bare source column** argument (the delta
|
|
104
|
+
* steps the raw value);
|
|
105
|
+
* - **exact value domain**: the aggregate's declared return type is INTEGER-physical
|
|
106
|
+
* (count-shaped — exact regardless of argument), OR its argument column's static
|
|
107
|
+
* type is INTEGER-physical (integer sum). A REAL/NUMERIC/TEXT sum drifts under
|
|
108
|
+
* repeated add/subtract and would diverge byte-exactly from the live re-evaluation
|
|
109
|
+
* the oracle compares against — such a column disqualifies the whole MV (residual);
|
|
110
|
+
* - a **tighten-only** column (min/max, and any UDAF whose merge is a join-semilattice
|
|
111
|
+
* like `bit_or`/`bool_or`) — a plain call declaring `merge` + `decode` but NO `negate`
|
|
112
|
+
* — is admitted with `deltaClass: 'tighten'`. It needs no exact-value-domain gate
|
|
113
|
+
* (merge is idempotent selection, not accumulating arithmetic → no drift) and is never
|
|
114
|
+
* retraction-safe: an insert `merge`s the new value in cheaply, but a retracted group
|
|
115
|
+
* re-derives wholesale from the residual (via the descriptor's `hasTighten`) rather
|
|
116
|
+
* than trusting the arithmetic;
|
|
117
|
+
* - a **decomposition-maintained** column (the avg class) — a plain call whose algebra
|
|
118
|
+
* declares `decompose` (a scalar formula over sibling partials, e.g. avg ≡ sum/count)
|
|
119
|
+
* but is not itself directly maintainable — is admitted only when EVERY partial it
|
|
120
|
+
* names ({@link resolveDecomposePartial}) is also stored as a sibling column of this
|
|
121
|
+
* same body and is itself delta-maintainable. It carries no independent accumulation;
|
|
122
|
+
* at flush its value is `combine([finalized(partial) …])`. If any partial is missing
|
|
123
|
+
* or not delta-maintainable the whole MV falls to residual (avg is the first client of
|
|
124
|
+
* this class, not a special case);
|
|
125
|
+
* - any other column shape (a constant, an expression over the group key, …) fails.
|
|
126
|
+
*
|
|
127
|
+
* Whole-body requirements:
|
|
128
|
+
* - **no post-aggregate filter** (HAVING, or an outer WHERE over the aggregate
|
|
129
|
+
* output): such a filter breaks the arithmetic invariant "stored row == the
|
|
130
|
+
* group's full accumulator" — a filtered-out group has contributions but no
|
|
131
|
+
* stored row, so a later delta would rebuild it from the identity and understate
|
|
132
|
+
* the aggregate. The residual re-evaluates the filter per group and stays exact;
|
|
133
|
+
* - a **count(*) multiplicity witness** — a zero-arg delta-maintainable column with
|
|
134
|
+
* `decodeExact` (its stored value must reconstruct exactly, since it decides group
|
|
135
|
+
* emptiness under retraction). Without it, emptiness cannot be told from an
|
|
136
|
+
* all-NULL sum;
|
|
137
|
+
* - **BINARY collations** on every backing-PK (group key) column: the flush's stored-
|
|
138
|
+
* row read is the host's `scanEffective` equality-prefix seek, whose prefix compare
|
|
139
|
+
* is binary (scan-layer.ts). Unlike the prefix-delete arm there is no source-PK
|
|
140
|
+
* uniqueness collapsing collation classes to one byte form here, so a non-BINARY
|
|
141
|
+
* group key could miss a collation-equal / byte-different stored row — residual
|
|
142
|
+
* instead;
|
|
143
|
+
* - a body WHERE (if any) must compile to a single-source-row predicate
|
|
144
|
+
* (`compilePredicate`) so per-row contributions can be scoped exactly.
|
|
145
|
+
*
|
|
146
|
+
* Per-column `retractionSafe` (see {@link DeltaAggregateColumn}) is recorded rather
|
|
147
|
+
* than gated: a not-retraction-safe descriptor still serves the bulk-insert fast path,
|
|
148
|
+
* and the flush re-derives only the retracted groups through the residual.
|
|
149
|
+
*/
|
|
150
|
+
export declare function buildDeltaAggregateDescriptor(mv: MaintainedTableSchema, analyzed: BlockNode, tableRef: TableReferenceNode, groupColumns: readonly number[], backingPkDefinition: ReadonlyArray<BackingPkColumn>, backingPkSourceCols: readonly number[], rootAttrs: ReadonlyArray<{
|
|
151
|
+
id: number;
|
|
152
|
+
} | undefined>, sourceAttrToCol: Map<number, number>, producingByAttrId: Map<number, ScalarPlanNode>): DeltaAggregateDescriptor | undefined;
|
|
89
153
|
/**
|
|
90
154
|
* Build a `'join-residual'` plan for a provably-1:1 row-preserving **inner/cross join**
|
|
91
155
|
* body (`select … from T join P on T.fk = P.id`), or return `null` on a shape mismatch
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-materialized-views-plan-builders.d.ts","sourceRoot":"","sources":["../../../src/core/database-materialized-views-plan-builders.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"database-materialized-views-plan-builders.d.ts","sourceRoot":"","sources":["../../../src/core/database-materialized-views-plan-builders.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
import { QuereusError } from '../common/errors.js';
|
|
9
9
|
import { StatusCode } from '../common/types.js';
|
|
10
10
|
import { ColumnReferenceNode } from '../planner/nodes/reference.js';
|
|
11
|
+
import { AggregateFunctionCallNode } from '../planner/nodes/aggregate-function.js';
|
|
12
|
+
import { isAggregateFunctionSchema } from '../schema/function.js';
|
|
13
|
+
import { PhysicalType } from '../types/logical-type.js';
|
|
11
14
|
import { checkDeterministic } from '../planner/validation/determinism-validator.js';
|
|
12
15
|
import { emitPlanNode } from '../runtime/emitters.js';
|
|
13
16
|
import { EmissionContext } from '../runtime/emission-context.js';
|
|
@@ -21,11 +24,17 @@ import { CapabilityDetectors } from '../planner/framework/characteristics.js';
|
|
|
21
24
|
import { selectMaintenanceStrategy, isFullRebuildPathological, seqScanCost, filterCost, projectCost, } from '../planner/cost/index.js';
|
|
22
25
|
import { tryResolveBackingHost } from '../runtime/emit/materialized-view-helpers.js';
|
|
23
26
|
import { compilePredicate } from '../vtab/memory/utils/predicate.js';
|
|
24
|
-
import { findNonReplicableFunction, findNonReplicableCollation, findNonDeterministic, normalizeCollation, findAggregate, containsNodeType, containsAnyJoin, countNodeType, countJoins, findTableFunctionCall, collectTableRefs, collectProducingExprs, resolveTransitiveSourceCol, bodyWhereReferencesLookup, bodyWhereIsNonDeterministic, relationalAttributes, rootRelationalNode, cannotMaterialize, nonReplicableDerivationError, nonReplicableCollationDerivationError, isSingleRowEvaluable, compileSourceRowEvaluator, } from './database-materialized-views-analysis.js';
|
|
27
|
+
import { findNonReplicableFunction, findNonReplicableCollation, findNonDeterministic, normalizeCollation, isBinaryCollation, findAggregate, containsNodeType, containsAnyJoin, countNodeType, countJoins, findTableFunctionCall, collectTableRefs, collectProducingExprs, resolveTransitiveSourceCol, bodyWhereReferencesLookup, bodyWhereIsNonDeterministic, relationalAttributes, rootRelationalNode, cannotMaterialize, nonReplicableDerivationError, nonReplicableCollationDerivationError, isSingleRowEvaluable, compileSourceRowEvaluator, } from './database-materialized-views-analysis.js';
|
|
25
28
|
import { BINARY_COLLATION } from '../util/comparison.js';
|
|
26
29
|
/** Fallback source row estimate when the StatsProvider has no count (mirrors the
|
|
27
30
|
* optimizer's naive default). Only feeds the create-time maintenance cost gate. */
|
|
28
31
|
const DEFAULT_SOURCE_ROWS = 1000;
|
|
32
|
+
/** Create-time estimate of the fraction of changes to a **tighten-only** (min/max) delta
|
|
33
|
+
* aggregate that retract (a delete / update-old-image) and so re-derive their group from
|
|
34
|
+
* the key-filtered residual instead of pure arithmetic. Feeds the `'delta-aggregate'` cost
|
|
35
|
+
* blend ({@link maintenanceCost}); kept well below 1 so an insert-dominated body still
|
|
36
|
+
* prefers the delta arm, yet non-zero so a tighten body costs more than a pure-group one. */
|
|
37
|
+
const DELTA_TIGHTEN_FALLBACK_RATIO = 0.25;
|
|
29
38
|
/**
|
|
30
39
|
* Snapshot a backing table's physical primary key as the maintenance plans consume it: the
|
|
31
40
|
* declared `(index, desc, collation)` triple plus the collation comparator that name resolves
|
|
@@ -123,6 +132,18 @@ export function buildMaintenancePlan(ctx, mv) {
|
|
|
123
132
|
}
|
|
124
133
|
// Try a bounded-delta arm; a shape that fits none falls through to the floor.
|
|
125
134
|
const boundedDelta = tryBuildBoundedDeltaArm(ctx, mv, analyzed);
|
|
135
|
+
// A residual arm additionally carries the whole-body scheduler — the per-statement
|
|
136
|
+
// degrade-to-rebuild escape: when a statement's distinct affected-key count makes k
|
|
137
|
+
// residual runs cost more than one rebuild (`shouldDegradeToRebuild`), the flush runs
|
|
138
|
+
// this and applies a single `'replace-all'` diff instead. Compiled here (not per arm
|
|
139
|
+
// builder) so the three residual arms share one compilation site; inverse-projection
|
|
140
|
+
// never degrades (always the cheapest arm) and the floor has its own bodyScheduler.
|
|
141
|
+
// NOTE: adds one whole-body optimize+emit per residual-MV (re)registration; if
|
|
142
|
+
// registration latency ever matters (e.g. bulk catalog import), compile this lazily
|
|
143
|
+
// on the first degrade instead.
|
|
144
|
+
if (boundedDelta && boundedDelta.kind !== 'inverse-projection' && boundedDelta.kind !== 'full-rebuild') {
|
|
145
|
+
boundedDelta.fullRebuildScheduler = compileWholeBodyScheduler(ctx, analyzed);
|
|
146
|
+
}
|
|
126
147
|
const plan = boundedDelta ?? buildFullRebuildPlan(ctx, mv, analyzed);
|
|
127
148
|
// Precompute the NOT-NULL ordering-seeded PK skew guard once, here at plan build —
|
|
128
149
|
// which `alter … drop not null` re-runs (it recompiles the row-time plan live), so the
|
|
@@ -471,17 +492,33 @@ export function buildAggregateResidualPlan(ctx, mv, analyzed, tableRef, sourceBa
|
|
|
471
492
|
const residualScheduler = compileResidual(ctx, analyzed, relKey, groupColumns, 'gk');
|
|
472
493
|
if (!residualScheduler)
|
|
473
494
|
return null; // could not parameterize the residual → floor
|
|
495
|
+
// Delta-aggregate fast path: build the descriptor when EVERY stored aggregate
|
|
496
|
+
// column is delta-maintainable by its declared algebra (see the builder below).
|
|
497
|
+
// `undefined` on any gate failure — the arm then stays a plain residual, unchanged.
|
|
498
|
+
const delta = buildDeltaAggregateDescriptor(mv, analyzed, tableRef, groupColumns, backingPkDefinition, backingPkSourceCols, rootAttrs, sourceAttrToCol, producingByAttrId);
|
|
474
499
|
// ── Cost gate ──
|
|
475
500
|
// The residual is the structurally-sound incremental arm for an aggregate body;
|
|
476
501
|
// 'full-rebuild' is the always-correct floor for shapes where the residual is NOT
|
|
477
|
-
// sound, so (as with inverse-projection) it is not a competitor here.
|
|
478
|
-
//
|
|
479
|
-
|
|
502
|
+
// sound, so (as with inverse-projection) it is not a competitor here. When the delta
|
|
503
|
+
// descriptor built, 'delta-aggregate' joins the sound set — an O(1)-per-change
|
|
504
|
+
// arithmetic RMW, strictly cheaper than the per-group residual under the cost model,
|
|
505
|
+
// so the argmin picks it. The descriptor is attached only when the gate chose it
|
|
506
|
+
// (cost-strategy decoupling: `kind` stays 'residual-recompute' either way).
|
|
507
|
+
const soundStrategies = delta
|
|
508
|
+
? ['delta-aggregate', 'residual-recompute']
|
|
509
|
+
: ['residual-recompute'];
|
|
480
510
|
const hasPredicate = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
|
|
481
511
|
const sourceStats = estimateMaintenanceStats(ctx, tableRef.tableSchema, backing.columns.length, hasPredicate);
|
|
512
|
+
// A tighten-only (min/max) column re-derives a retracted group from the residual, so the
|
|
513
|
+
// delta arm's cost is no longer pure arithmetic — feed the create-time expected-rescan
|
|
514
|
+
// fraction into the `'delta-aggregate'` cost blend (maintenanceCost). A tighten body then
|
|
515
|
+
// costs strictly more than a pure-group body but, for this insert-dominated estimate, still
|
|
516
|
+
// below the always-residual arm, so the argmin keeps the delta arm.
|
|
517
|
+
if (delta?.hasTighten)
|
|
518
|
+
sourceStats.deltaTightenFallbackRatio = DELTA_TIGHTEN_FALLBACK_RATIO;
|
|
482
519
|
const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
|
|
483
520
|
const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
|
|
484
|
-
if (chosenStrategy
|
|
521
|
+
if (!soundStrategies.includes(chosenStrategy)) {
|
|
485
522
|
throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
486
523
|
}
|
|
487
524
|
return {
|
|
@@ -493,14 +530,306 @@ export function buildAggregateResidualPlan(ctx, mv, analyzed, tableRef, sourceBa
|
|
|
493
530
|
chosenStrategy,
|
|
494
531
|
sourceStats,
|
|
495
532
|
binding: { kind: 'group', groupColumns: [...groupColumns] },
|
|
496
|
-
degradeToRebuild: false,
|
|
497
533
|
residualScheduler,
|
|
498
534
|
bindParamPrefix: 'gk',
|
|
499
535
|
bindColumns: groupColumns,
|
|
500
536
|
backingPkDefinition,
|
|
501
537
|
backingPkSourceCols,
|
|
538
|
+
delta: chosenStrategy === 'delta-aggregate' ? delta : undefined,
|
|
502
539
|
};
|
|
503
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Build the {@link DeltaAggregateDescriptor} for a single-source aggregate body, or
|
|
543
|
+
* return `undefined` when any column fails the delta eligibility gate (the arm then
|
|
544
|
+
* stays a plain residual — a gate failure is never an error). Function-generic by
|
|
545
|
+
* construction: every judgment reads the aggregate call node's registry-resolved
|
|
546
|
+
* schema and its DECLARED {@link AggregateAlgebra} — never an aggregate-name list.
|
|
547
|
+
*
|
|
548
|
+
* The gate, per stored backing column:
|
|
549
|
+
* - a **group-key passthrough** column must resolve (transitively) to a GROUP BY
|
|
550
|
+
* source column that also backs a backing-PK position (so its flush value is the
|
|
551
|
+
* group key itself);
|
|
552
|
+
* - a directly **delta-maintainable aggregate** column must be a plain call (`agg(x)` /
|
|
553
|
+
* `agg()` — no DISTINCT, FILTER, ORDER BY, multi-arg) whose declared algebra has
|
|
554
|
+
* `merge` + `negate` + `decode`, with a **bare source column** argument (the delta
|
|
555
|
+
* steps the raw value);
|
|
556
|
+
* - **exact value domain**: the aggregate's declared return type is INTEGER-physical
|
|
557
|
+
* (count-shaped — exact regardless of argument), OR its argument column's static
|
|
558
|
+
* type is INTEGER-physical (integer sum). A REAL/NUMERIC/TEXT sum drifts under
|
|
559
|
+
* repeated add/subtract and would diverge byte-exactly from the live re-evaluation
|
|
560
|
+
* the oracle compares against — such a column disqualifies the whole MV (residual);
|
|
561
|
+
* - a **tighten-only** column (min/max, and any UDAF whose merge is a join-semilattice
|
|
562
|
+
* like `bit_or`/`bool_or`) — a plain call declaring `merge` + `decode` but NO `negate`
|
|
563
|
+
* — is admitted with `deltaClass: 'tighten'`. It needs no exact-value-domain gate
|
|
564
|
+
* (merge is idempotent selection, not accumulating arithmetic → no drift) and is never
|
|
565
|
+
* retraction-safe: an insert `merge`s the new value in cheaply, but a retracted group
|
|
566
|
+
* re-derives wholesale from the residual (via the descriptor's `hasTighten`) rather
|
|
567
|
+
* than trusting the arithmetic;
|
|
568
|
+
* - a **decomposition-maintained** column (the avg class) — a plain call whose algebra
|
|
569
|
+
* declares `decompose` (a scalar formula over sibling partials, e.g. avg ≡ sum/count)
|
|
570
|
+
* but is not itself directly maintainable — is admitted only when EVERY partial it
|
|
571
|
+
* names ({@link resolveDecomposePartial}) is also stored as a sibling column of this
|
|
572
|
+
* same body and is itself delta-maintainable. It carries no independent accumulation;
|
|
573
|
+
* at flush its value is `combine([finalized(partial) …])`. If any partial is missing
|
|
574
|
+
* or not delta-maintainable the whole MV falls to residual (avg is the first client of
|
|
575
|
+
* this class, not a special case);
|
|
576
|
+
* - any other column shape (a constant, an expression over the group key, …) fails.
|
|
577
|
+
*
|
|
578
|
+
* Whole-body requirements:
|
|
579
|
+
* - **no post-aggregate filter** (HAVING, or an outer WHERE over the aggregate
|
|
580
|
+
* output): such a filter breaks the arithmetic invariant "stored row == the
|
|
581
|
+
* group's full accumulator" — a filtered-out group has contributions but no
|
|
582
|
+
* stored row, so a later delta would rebuild it from the identity and understate
|
|
583
|
+
* the aggregate. The residual re-evaluates the filter per group and stays exact;
|
|
584
|
+
* - a **count(*) multiplicity witness** — a zero-arg delta-maintainable column with
|
|
585
|
+
* `decodeExact` (its stored value must reconstruct exactly, since it decides group
|
|
586
|
+
* emptiness under retraction). Without it, emptiness cannot be told from an
|
|
587
|
+
* all-NULL sum;
|
|
588
|
+
* - **BINARY collations** on every backing-PK (group key) column: the flush's stored-
|
|
589
|
+
* row read is the host's `scanEffective` equality-prefix seek, whose prefix compare
|
|
590
|
+
* is binary (scan-layer.ts). Unlike the prefix-delete arm there is no source-PK
|
|
591
|
+
* uniqueness collapsing collation classes to one byte form here, so a non-BINARY
|
|
592
|
+
* group key could miss a collation-equal / byte-different stored row — residual
|
|
593
|
+
* instead;
|
|
594
|
+
* - a body WHERE (if any) must compile to a single-source-row predicate
|
|
595
|
+
* (`compilePredicate`) so per-row contributions can be scoped exactly.
|
|
596
|
+
*
|
|
597
|
+
* Per-column `retractionSafe` (see {@link DeltaAggregateColumn}) is recorded rather
|
|
598
|
+
* than gated: a not-retraction-safe descriptor still serves the bulk-insert fast path,
|
|
599
|
+
* and the flush re-derives only the retracted groups through the residual.
|
|
600
|
+
*/
|
|
601
|
+
export function buildDeltaAggregateDescriptor(mv, analyzed, tableRef, groupColumns, backingPkDefinition, backingPkSourceCols, rootAttrs, sourceAttrToCol, producingByAttrId) {
|
|
602
|
+
const sourceSchema = tableRef.tableSchema;
|
|
603
|
+
const groupColumnSet = new Set(groupColumns);
|
|
604
|
+
// Post-aggregate filter gate: a Filter whose subtree contains the aggregate is a
|
|
605
|
+
// HAVING (or an outer WHERE over the aggregate output). It breaks the arithmetic
|
|
606
|
+
// invariant "stored row == the group's full accumulator" — a filtered-out group has
|
|
607
|
+
// contributions but no stored row, so a later delta would rebuild it from the
|
|
608
|
+
// identity and understate the aggregate. (The body WHERE sits BELOW the aggregate —
|
|
609
|
+
// its Filter subtree contains no aggregate node — and stays delta-eligible via the
|
|
610
|
+
// compiled scope predicate below.)
|
|
611
|
+
if (hasFilterAboveAggregate(analyzed))
|
|
612
|
+
return undefined;
|
|
613
|
+
// Binary-collation gate on the backing PK (the flush's binary equality-prefix read).
|
|
614
|
+
for (const d of backingPkDefinition) {
|
|
615
|
+
if (!isBinaryCollation(d.collation))
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
// Body WHERE → single-row scope predicate (contributions outside it are dropped,
|
|
619
|
+
// mirroring the residual's own WHERE). An uncompilable WHERE shape → residual.
|
|
620
|
+
let predicate;
|
|
621
|
+
const bodyWhere = mv.derivation.selectAst.type === 'select' ? mv.derivation.selectAst.where : undefined;
|
|
622
|
+
if (bodyWhere) {
|
|
623
|
+
try {
|
|
624
|
+
predicate = compilePredicate(bodyWhere, sourceSchema.columns);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Chase an output attr's producing chain to its terminal (non-ColumnReference)
|
|
631
|
+
// expression — the aggregate call node for an aggregate output column.
|
|
632
|
+
const resolveTerminalProducing = (attrId) => {
|
|
633
|
+
const seen = new Set();
|
|
634
|
+
let cur = attrId;
|
|
635
|
+
while (!seen.has(cur)) {
|
|
636
|
+
seen.add(cur);
|
|
637
|
+
const expr = producingByAttrId.get(cur);
|
|
638
|
+
if (expr instanceof ColumnReferenceNode) {
|
|
639
|
+
cur = expr.attributeId;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
return expr;
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
};
|
|
646
|
+
const aggColumns = [];
|
|
647
|
+
const groupOut = [];
|
|
648
|
+
// Decomposition-maintained columns (the avg class), collected raw here and bound to
|
|
649
|
+
// their sibling stored partials in a second pass below — the partials may project
|
|
650
|
+
// before OR after the decompose column, so they cannot be resolved inline.
|
|
651
|
+
const pendingDecompose = [];
|
|
652
|
+
for (let outCol = 0; outCol < rootAttrs.length; outCol++) {
|
|
653
|
+
const attr = rootAttrs[outCol];
|
|
654
|
+
if (!attr)
|
|
655
|
+
return undefined;
|
|
656
|
+
const sourceCol = resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId);
|
|
657
|
+
if (sourceCol !== undefined) {
|
|
658
|
+
// Group-key passthrough: its flush value is the group key at the matching
|
|
659
|
+
// backing-PK position (covers duplicate projections of a group column).
|
|
660
|
+
if (!groupColumnSet.has(sourceCol))
|
|
661
|
+
return undefined;
|
|
662
|
+
const pkPos = backingPkSourceCols.indexOf(sourceCol);
|
|
663
|
+
if (pkPos < 0)
|
|
664
|
+
return undefined;
|
|
665
|
+
groupOut.push({ backingCol: outCol, pkPos });
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const producing = resolveTerminalProducing(attr.id);
|
|
669
|
+
if (!(producing instanceof AggregateFunctionCallNode))
|
|
670
|
+
return undefined;
|
|
671
|
+
// Plain call only: DISTINCT folds duplicates (delta cannot), FILTER/ORDER BY
|
|
672
|
+
// change the contribution set, multi-arg has no single raw value to step.
|
|
673
|
+
if (producing.isDistinct || producing.filter || (producing.orderBy && producing.orderBy.length > 0))
|
|
674
|
+
return undefined;
|
|
675
|
+
if (producing.args.length > 1)
|
|
676
|
+
return undefined;
|
|
677
|
+
const schema = producing.functionSchema;
|
|
678
|
+
if (!isAggregateFunctionSchema(schema))
|
|
679
|
+
return undefined;
|
|
680
|
+
const algebra = schema.algebra;
|
|
681
|
+
// A bare source column argument (both classes below step / decompose over the raw
|
|
682
|
+
// value); a zero-arg (count(*)-shaped) call carries none.
|
|
683
|
+
let argSourceCol;
|
|
684
|
+
if (producing.args.length === 1) {
|
|
685
|
+
const arg = producing.args[0];
|
|
686
|
+
if (!(arg instanceof ColumnReferenceNode))
|
|
687
|
+
return undefined; // bare column args only
|
|
688
|
+
argSourceCol = resolveTransitiveSourceCol(arg.attributeId, sourceAttrToCol, producingByAttrId);
|
|
689
|
+
if (argSourceCol === undefined)
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
// Class routing — declaration-driven, never an aggregate-name list:
|
|
693
|
+
// - **directly delta-maintainable** (`merge` + `negate` (retraction) + `decode` —
|
|
694
|
+
// the backing stores finalized values, not accumulators): accumulate it as an
|
|
695
|
+
// ordinary partial;
|
|
696
|
+
// - else a **decomposition-maintained** column whose value is a formula over
|
|
697
|
+
// sibling partials (`algebra.decompose`, e.g. avg ≡ sum/count) — deferred to the
|
|
698
|
+
// pass below that binds each partial to a stored sibling; it accumulates nothing;
|
|
699
|
+
// - anything else disqualifies the whole MV → residual.
|
|
700
|
+
if (algebra?.negate && algebra.decode) {
|
|
701
|
+
// Exact numeric domain: an INTEGER-physical declared result (count-shaped — the
|
|
702
|
+
// accumulator never holds a float) or an INTEGER-physical argument column
|
|
703
|
+
// (integer sum). Anything else may drift under repeated add/subtract.
|
|
704
|
+
// NOTE: if a REAL-domain sum ever needs the fast path, it needs compensated
|
|
705
|
+
// (Kahan) accumulation or a periodic-rescan discipline — a design change, not a
|
|
706
|
+
// relaxation of this gate.
|
|
707
|
+
const resultExact = schema.returnType.logicalType.physicalType === PhysicalType.INTEGER;
|
|
708
|
+
const argExact = argSourceCol !== undefined
|
|
709
|
+
&& sourceSchema.columns[argSourceCol]?.logicalType.physicalType === PhysicalType.INTEGER;
|
|
710
|
+
if (!resultExact && !argExact)
|
|
711
|
+
return undefined;
|
|
712
|
+
const retractionSafe = algebra.decodeExact === true
|
|
713
|
+
|| (argSourceCol !== undefined && sourceSchema.columns[argSourceCol]?.notNull === true);
|
|
714
|
+
aggColumns.push({
|
|
715
|
+
backingCol: outCol,
|
|
716
|
+
schema,
|
|
717
|
+
algebra,
|
|
718
|
+
deltaClass: 'group',
|
|
719
|
+
argSourceCol,
|
|
720
|
+
isMultiplicity: producing.args.length === 0,
|
|
721
|
+
retractionSafe,
|
|
722
|
+
});
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
// Tighten-only (join-semilattice): `merge` + `decode` but NO `negate` — min/max, and
|
|
726
|
+
// any UDAF whose merge is idempotent selection (bit_or/bool_or). An insert `merge`s the
|
|
727
|
+
// new value in cheaply (min/max tighten toward the new extreme); a retraction cannot be
|
|
728
|
+
// undone arithmetically, so a retracted group falls back to the residual
|
|
729
|
+
// (retractionSafe:false, and the descriptor's hasTighten routes the whole group there
|
|
730
|
+
// whether or not a row is stored). NO exact-numeric-domain gate: merge is idempotent
|
|
731
|
+
// selection, not accumulating arithmetic, so there is no float drift to guard — a tighten
|
|
732
|
+
// column over any comparable domain (REAL/TEXT included) stays byte-exact vs the live
|
|
733
|
+
// re-evaluation. A negate+decode column that failed the exact-domain gate above already
|
|
734
|
+
// returned undefined, so only a decode-without-negate aggregate reaches here.
|
|
735
|
+
if (algebra?.decode && !algebra.negate) {
|
|
736
|
+
aggColumns.push({
|
|
737
|
+
backingCol: outCol,
|
|
738
|
+
schema,
|
|
739
|
+
algebra,
|
|
740
|
+
deltaClass: 'tighten',
|
|
741
|
+
argSourceCol,
|
|
742
|
+
isMultiplicity: producing.args.length === 0,
|
|
743
|
+
retractionSafe: false,
|
|
744
|
+
});
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (algebra?.decompose) {
|
|
748
|
+
pendingDecompose.push({ backingCol: outCol, argSourceCol, decomposition: algebra.decompose });
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
return undefined; // neither directly maintainable, tighten-only, nor decomposable → residual
|
|
752
|
+
}
|
|
753
|
+
// Multiplicity witness: a zero-arg delta-maintainable column whose decode is exact
|
|
754
|
+
// (its merged value decides group emptiness, so the reconstruction must survive
|
|
755
|
+
// retraction). Structural — not name-based. A decompose column is never the witness
|
|
756
|
+
// (it accumulates nothing — the requirement is a real stored count(*)).
|
|
757
|
+
const multiplicityIndex = aggColumns.findIndex(c => c.isMultiplicity && c.algebra.decodeExact === true);
|
|
758
|
+
if (multiplicityIndex < 0)
|
|
759
|
+
return undefined;
|
|
760
|
+
if (aggColumns.length === 0)
|
|
761
|
+
return undefined; // defensive — findAggregate implies ≥1
|
|
762
|
+
// Bind each decomposition column's partials to sibling STORED, delta-maintained columns
|
|
763
|
+
// (every `aggColumns` entry already passed the accumulator gate, so it is itself delta-
|
|
764
|
+
// maintainable). A partial that maps to no such sibling disqualifies the whole MV →
|
|
765
|
+
// residual (honest and visible: the user wrote a body without the partials the formula
|
|
766
|
+
// names). The decompose column's `combine` recomputes exactly per group at flush.
|
|
767
|
+
const decomposeColumns = [];
|
|
768
|
+
for (const pd of pendingDecompose) {
|
|
769
|
+
const partialIndices = [];
|
|
770
|
+
for (const p of pd.decomposition.partials) {
|
|
771
|
+
const idx = resolveDecomposePartial(p, pd.argSourceCol, aggColumns, sourceSchema);
|
|
772
|
+
if (idx === undefined)
|
|
773
|
+
return undefined;
|
|
774
|
+
partialIndices.push(idx);
|
|
775
|
+
}
|
|
776
|
+
decomposeColumns.push({ backingCol: pd.backingCol, partialIndices, combine: pd.decomposition.combine });
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
aggColumns,
|
|
780
|
+
decomposeColumns,
|
|
781
|
+
groupColumns: groupOut,
|
|
782
|
+
multiplicityIndex,
|
|
783
|
+
backingColumnCount: rootAttrs.length,
|
|
784
|
+
retractionSafe: aggColumns.every(c => c.retractionSafe),
|
|
785
|
+
hasTighten: aggColumns.some(c => c.deltaClass === 'tighten'),
|
|
786
|
+
predicate,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Resolve one {@link AggregateDecomposition} partial (e.g. avg's `sum` / `count`) to the
|
|
791
|
+
* index of a sibling stored, delta-maintainable aggregate column — an `aggColumns` entry
|
|
792
|
+
* matching the partial's function name and argument. Returns `undefined` when no such
|
|
793
|
+
* sibling is stored (the decompose column is then not maintainable → the MV falls to the
|
|
794
|
+
* residual). The write-side twin of `query-rewrite-matcher.resolveMergeablePartial`.
|
|
795
|
+
*
|
|
796
|
+
* `'same-arg'` targets a sibling over the decompose column's own argument (`f(thisArg)`);
|
|
797
|
+
* `'star'` targets a zero-arg (`count(*)`-shaped) sibling. The **count(*) fallback** mirrors
|
|
798
|
+
* the read-side relaxation from `feat-mv-agg-rollup-retarget`: a stored `count(*)` substitutes
|
|
799
|
+
* for a `count(x)` divisor only when `x` is declared **NOT NULL** — then `count(*)` excludes
|
|
800
|
+
* the same (zero) NULLs `count(x)` would, so the recombine stays exact.
|
|
801
|
+
*/
|
|
802
|
+
function resolveDecomposePartial(p, ownerArgSourceCol, aggColumns, sourceSchema) {
|
|
803
|
+
// NOTE: a partial resolves only to an `aggColumns` entry, which already passed the
|
|
804
|
+
// accumulator gate — a BARE source column argument plus the INTEGER-exact-domain check.
|
|
805
|
+
// That is what keeps the decomposed value byte-exact: a partial like `sum(log x)` (a
|
|
806
|
+
// non-bare, float-producing argument) can never be a stored delta-maintainable sibling,
|
|
807
|
+
// so a true geometric mean is NOT delta-decomposable and correctly falls to the residual.
|
|
808
|
+
const targetArg = p.arg === 'star' ? undefined : ownerArgSourceCol;
|
|
809
|
+
const funcName = p.func.toLowerCase();
|
|
810
|
+
const find = (name, argCol) => aggColumns.findIndex(c => c.schema.name.toLowerCase() === name && c.argSourceCol === argCol);
|
|
811
|
+
let idx = find(funcName, targetArg);
|
|
812
|
+
if (idx < 0 && funcName === 'count' && targetArg !== undefined
|
|
813
|
+
&& sourceSchema.columns[targetArg]?.notNull === true) {
|
|
814
|
+
idx = find('count', undefined); // count(*) qualifies as the count(x) divisor when x is NOT NULL
|
|
815
|
+
}
|
|
816
|
+
return idx < 0 ? undefined : idx;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* True when any Filter node's subtree contains an aggregate node — i.e. the plan
|
|
820
|
+
* filters the AGGREGATE OUTPUT (HAVING, or an outer WHERE over a grouped subquery).
|
|
821
|
+
* The body WHERE is the complement case: its Filter sits below the aggregate, so its
|
|
822
|
+
* subtree contains none. Used by {@link buildDeltaAggregateDescriptor}'s gate.
|
|
823
|
+
*/
|
|
824
|
+
function hasFilterAboveAggregate(node) {
|
|
825
|
+
if (node.nodeType === PlanNodeType.Filter && findAggregate(node) !== undefined)
|
|
826
|
+
return true;
|
|
827
|
+
for (const child of node.getChildren()) {
|
|
828
|
+
if (hasFilterAboveAggregate(child))
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
504
833
|
/**
|
|
505
834
|
* Build a `'join-residual'` plan for a provably-1:1 row-preserving **inner/cross join**
|
|
506
835
|
* body (`select … from T join P on T.fk = P.id`), or return `null` on a shape mismatch
|
|
@@ -679,7 +1008,6 @@ export function buildJoinResidualPlan(ctx, mv, analyzed, tableRefs) {
|
|
|
679
1008
|
chosenStrategy,
|
|
680
1009
|
sourceStats,
|
|
681
1010
|
binding: { kind: 'row', keyColumns: [...tPkCols] },
|
|
682
|
-
degradeToRebuild: false,
|
|
683
1011
|
residualScheduler: forwardResidual,
|
|
684
1012
|
bindParamPrefix: 'pk',
|
|
685
1013
|
bindColumns: tPkCols,
|
|
@@ -866,6 +1194,18 @@ export function compileResidual(ctx, analyzed, relKey, bindColumns, paramPrefix)
|
|
|
866
1194
|
const instruction = emitPlanNode(optimized, new EmissionContext(db));
|
|
867
1195
|
return new Scheduler(instruction);
|
|
868
1196
|
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Compile the WHOLE optimized body (no key filter, read-side rewrite suppressed) into a
|
|
1199
|
+
* reusable {@link Scheduler} — the residual arms' per-statement degrade-to-rebuild
|
|
1200
|
+
* escape ({@link ResidualArmCommon.fullRebuildScheduler}). Mirrors the floor's own
|
|
1201
|
+
* `bodyScheduler` compilation in {@link buildFullRebuildPlan}: run to completion against
|
|
1202
|
+
* live mid-transaction source state, its rows feed a single `'replace-all'` keyed diff.
|
|
1203
|
+
*/
|
|
1204
|
+
function compileWholeBodyScheduler(ctx, analyzed) {
|
|
1205
|
+
const db = ctx;
|
|
1206
|
+
const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => ctx.optimizer.optimize(analyzed, db));
|
|
1207
|
+
return new Scheduler(emitPlanNode(optimized, new EmissionContext(db)));
|
|
1208
|
+
}
|
|
869
1209
|
/**
|
|
870
1210
|
* Build a `'prefix-delete'` plan for a single-source lateral-TVF fan-out body
|
|
871
1211
|
* (`select T.pk…, …, f.* from T cross join lateral tvf(<args over T>) f`), or return
|
|
@@ -1025,7 +1365,6 @@ export function buildLateralTvfPrefixDeletePlan(ctx, mv, analyzed, tableRef, sou
|
|
|
1025
1365
|
chosenStrategy,
|
|
1026
1366
|
sourceStats,
|
|
1027
1367
|
binding: { kind: 'row', keyColumns: [...sourcePkCols] },
|
|
1028
|
-
degradeToRebuild: false,
|
|
1029
1368
|
residualScheduler,
|
|
1030
1369
|
bindParamPrefix: 'pk',
|
|
1031
1370
|
bindColumns: sourcePkCols,
|