Documentation
¶
Overview ¶
Package physical defines stage-type string constants used by Stage.Type. Exchange stages are inserted by EnsureDistribution to bridge distribution mismatches between child output and parent required input.
Package physical converts logical plans to physical execution plans.
Index ¶
- Constants
- Variables
- func AssertExchangeConsistency(stages []Stage) error
- func BuildAggregateShuffleSQL(cand AggregateShuffleCandidate, stages []Stage) (string, error)
- func BuildJoinResidualFilter(filter, buildAlias string) ...
- func BuildSemiAntiFilter(filter string) ...
- func CanProbeSplit(stages []Stage, workerCount int) (probeAlias string, probeFiles []string, ok bool)
- func CountJoinStages(stages []Stage) int
- func GatherOutputSchema(stages []Stage) []parquet.Column
- func GatherOutputWireUnconstrainedDecimal(stages []Stage) map[string]bool
- func HashPartitionCount(workerCount int) int
- func IsPGSystemColumn(name string) bool
- func NewComputedColumnsOp(cols []exec.ProjectColumn) exec.UnaryOperator
- func ParseSemiAntiNE(filter string) (probeCol, buildCol string, ok bool)
- func ProjectionOutputType(node plansql.Node, fallback parquet.TypeID) expr.DeclType
- func RefuseReservedSlotName(name, where string) error
- func RefuseReservedSlotNames(names []string, where string) error
- func ReservedSlotFamily(name string) string
- func SemiAntiBuildStoreCols(rightKeys []string, joinFilter string) []string
- func SetExchangePartialAggEnabled(on bool) bool
- func SlotName(family SlotFamily, n int) string
- func ValidateNativeDAGShape(stages []Stage) error
- func WithManifestSnapshot(ctx context.Context, snap *ManifestSnapshot) context.Context
- type AggSpec
- type AggregateShuffleCandidate
- type AggregateShuffleDiag
- type AggregateShuffleRejectReason
- type ChainedJoinSpec
- type ComputedCol
- type DecimalCoercion
- type DistKind
- type Distribution
- type DynamicFilterConsume
- type DynamicFilterEmit
- type ExchangeStage
- type FilterAliasSpec
- type FusedJoinSpec
- type GroupKeyResolution
- type ManifestSnapshot
- type OutputRename
- type PhysicalPlan
- type Planner
- func (p *Planner) AnnotateScanColumns(ctx context.Context, node *logical.Node)
- func (p *Planner) AttachedFilterExprs() []string
- func (p *Planner) AttachedProjectionOutputs() []string
- func (p *Planner) EstimatePlanScanBytes(ctx context.Context, n *logical.Node) (int64, bool)
- func (p *Planner) ExpandFederatedScans(stages []Stage) []Stage
- func (p *Planner) Plan(ctx context.Context, node *logical.Node) (*PhysicalPlan, error)
- func (p *Planner) PlanDistributed(ctx context.Context, node *logical.Node) ([]Stage, error)
- func (p *Planner) ValidateColumns(ctx context.Context, info *plansql.SelectInfo) error
- type PreComputedAggregateMeta
- type ProjectExprSpec
- type QueryCost
- type RecordBatch
- type RequiredDistribution
- type RequiredKind
- type ShuffleCandidate
- type SlotFamily
- type SortKeySpec
- type Stage
- type UnionArm
- type WindowColSpec
Constants ¶
const ( StageScan = "scan" StageAggregate = "aggregate" StageSort = "sort" StageHashJoin = "hash_join" StageBroadcastJoin = "broadcast_join" // StageSortMergeJoin is a hash-shuffled join executed as a sort-merge // join (docs/design/sort-merge-join.md): identical exchange children and // distribution properties to StageHashJoin — only the join operator // differs. Emitted when the SortMergeJoinBytes gate passes. StageSortMergeJoin = "sort_merge_join" StageWindow = "window" StagePipeline = "pipeline" // StageUnion concatenates the arms of a UNION ALL. One task per arm: // task i reads arm i's whole output and projects it onto the result // column names (SQL takes those from the first arm), so every task // emits the same schema and the stage's files ARE the concatenation. // Nothing merges across tasks — concatenation is exactly the absence // of a merge, which is what makes UNION ALL the tractable set // operation on the DAG. See Stage.UnionArms. StageUnion = "union" // StageLimit bounds its input GLOBALLY: one task, reading every // partition of its dependency, applying OFFSET then LIMIT once. // // A LIMIT is only a bound if exactly one thing applies it to the whole // stream. The two places that could were the coordinator's post-gather // MergeInfo pass — which reads the ROOT node only — and a sort stage's // top-N, which needs an ORDER BY below the LIMIT. A LIMIT anywhere else // in the tree reached neither and bounded nothing: the derived table // yielded every row and the outer query computed over all of them, // silently (#478). This stage is that third place. Singleton by // construction, because a per-task bound is not a global one — k tasks // each keeping n rows is not the first n rows of their union. StageLimit = "limit" // StageProject applies a projection and/or a filter to its single // dependency's output and nothing else. // // It exists because a logical Project emits no stage on the DAG and a // logical Filter is appended to whatever stage was emitted last. Both // shortcuts are sound only when the stage underneath can express what it // is handed; when it cannot — a Filter above a Project that has itself // been materialized onto the producer, a Filter above a deduped // `cte-alias` whose target is SHARED with another reference and must not // be filtered for it — the predicate used to be attached to a stage that // ignored it or that a later pass deleted, and the query answered // without it (#656). // // Singleton and one task, for StageLimit's reason: one task reading // every partition of its input is always correct for a per-row operator, // and the stage is only ever emitted for shapes that had no answer at // all before. Fragment shape: // // [OpShuffleSource, OpProject?, OpFilter?, sink] // // The filter runs ABOVE the projection — a predicate that reaches this // stage is one written against the projection's outputs. StageProject = "project" // The stage types walkStages spells as literals. Named here so the // planner-side mirrors of the coordinator's fragment builders // (stageEvaluatesFilter, stageAppliesProjection) can enumerate them // without repeating string literals that a typo would silently drop out // of a switch. StageFinalAggregate = "final_aggregate" StageMergeAggregate = "merge_aggregate" StageMergeSort = "merge_sort" // Exchange stages — inserted by EnsureDistribution. // Repartition is the rename of the legacy "shuffle" type; the string // value changes so that the old name does not silently leak through. StageExchangeRepartition = "exchange-repartition" StageExchangeReplicate = "exchange-replicate" StageExchangeGather = "exchange-gather" )
const ( SlotWindowOutput = plansql.SlotWindowOutput SlotWindowKey = plansql.SlotWindowKey SlotSortKey = plansql.SlotSortKey SlotGroupKey = plansql.SlotGroupKey SlotAggInput = plansql.SlotAggInput SlotNestedAgg = plansql.SlotNestedAgg SlotScalar = plansql.SlotScalar SlotHaving = plansql.SlotHaving SlotTwoLevel = plansql.SlotTwoLevel SlotSetOpCount = plansql.SlotSetOpCount SlotAvgSum = plansql.SlotAvgSum SlotAvgCount = plansql.SlotAvgCount SlotVarState = plansql.SlotVarState SlotCovarState = plansql.SlotCovarState SlotPreComputedAgg = plansql.SlotPreComputedAgg SlotSubsumeFlag = plansql.SlotSubsumeFlag SlotRowLocator = plansql.SlotRowLocator SlotRowCountOnly = plansql.SlotRowCountOnly SlotDefaultPart = plansql.SlotDefaultPart )
const ( SetOpLeftCountCol = "__setop_lcnt" SetOpRightCountCol = "__setop_rcnt" )
SetOpLeftCountCol / SetOpRightCountCol are the per-arm tag columns an INTERSECT/EXCEPT lowering appends to each arm's projection: arm 0 tags every row (1, 0), arm 1 tags (0, 1). SUMming them under a GROUP BY over the full result row yields (rows in arm A, rows in arm B) per distinct row — the entire state the operation's count rule needs. Exported because the coordinator's fragment builder names the same columns in the emit operator's OpSpec.
const RowLocColumn = "__row_loc"
RowLocColumn is the synthetic column carrying (rgUnit ordinal << 32 | row-in-group) through the narrow phase. The "__" prefix keeps it out of user column namespaces (sanitizeScanNeeds already passes such names).
Variables ¶
var ( ReverseBloomThreshold int64 = 10_000_000 ReverseBloomInnerThreshold int64 = 50_000_000 )
ReverseBloomThreshold and ReverseBloomInnerThreshold gate the reverse-bloom optimization (see buildJoin). Declared as vars so regression tests can lower them to fire on tiny SF0.x datasets — TestTPCHReverseBloomForcedSF001 does exactly that — and so they can be raised at runtime to turn the optimization off without rebuilding.
These lines used to say the vars existed "to disable the optimization while we hunt the SF100 Q05 0-rows bug whose triggering code path is somewhere in this optimization", and that the semi/anti threshold stayed at 10M because there was "no evidence of bugs there yet". Both halves are settled now, and not in the direction the second one guessed.
A 0-rows MECHANISM in this optimization is identified and fixed (#543): reverseBloomBridge installed the bloom whether or not the key column had been found in the probe output, so a probeKey that did not resolve produced an EMPTY bloom that rejected every build row — a join answering over an empty build side, which is 0 rows for an inner or semi join. Forcing both thresholds to 100 over the SF0.01 corpus fires it on exactly one query, Q21, whose probeKey arrives alias-qualified as "l1.l_orderkey" against batches carrying "l_orderkey": on the parent commit Q21 returns 0 rows where the answer is 1 (and 0 where it is 100 at SF1). Init now refuses to install a bloom whose column never resolved or that received no keys.
Whether that mechanism is what produced the Q05 incident at SF100 was never reduced to a repro and is not claimed here: Q05's own reverse blooms resolve their columns at SF0.01, and the corpus-wide forced run shows Q21 as the only unresolved one. What IS claimed is that this optimization could return 0 rows for a reason that had nothing to do with the query, that the reason is now gone, and that a gate runs the whole corpus with both thresholds forced down so the next one cannot hide behind a production threshold.
The semi/anti threshold's "no evidence of bugs there yet" was wrong twice over: #543's key-encoding divergence was semi/anti-only in practice, since that is where string keys appear, and the empty-bloom mechanism above fires on a semi/anti query. The threshold stays at 10M for COST reasons.
Init reads WADJET_REVERSE_BLOOM_INNER_THRESHOLD if set, so the bench can disable the inner-join path on SF100 without rebuilding the binary.
var AggOverExchange atomic.Bool
AggOverExchange gates rewireAggOverRawExchange. Kill switch WADJET_AGG_OVER_EXCHANGE=0. Exported atomic.Bool (ExchangeSubsume pattern) so tests can pin either arm.
var AttachOnArrivalConsumesPlanned atomic.Int64
AttachOnArrivalConsumesPlanned counts converted consume edges, process-wide. A/B observability, same family as DynamicFiltersPlanned.
var BehaviorPreservingMode = true
BehaviorPreservingMode controls assertion hardness. When true (Phase 1 default), AssertExchangeConsistency logs violations at WARN and returns nil — every existing distributed plan continues to execute unchanged even if the property algebra rules in this file are wrong. When false (tests, and Phase 2 onward), violations are returned as errors and callers must handle them.
Phase 2 deletes this var and makes the assertion always strict — the EnsureDistribution rule guarantees no violation can survive into the emitted plan.
var DFAttachOnArrival atomic.Bool
DFAttachOnArrival gates applyAttachOnArrival. Kill switch WADJET_DF_ATTACH_ON_ARRIVAL=0 restores the barrier on every edge.
var DFGuardedReemit atomic.Bool
DFGuardedReemit gates the rule-1 relaxation: a RE-EMITTING consumer (cascade mid-scan) may also convert to attach mode when every one of its emits is an AtScan accumulator — the emit op then buffers rows scanned before the consumed bloom lands and retro-filters them at finalize (guarded re-emit), preserving downstream filter quality without the start barrier.
DEFAULT OFF (opt-in WADJET_DF_GUARDED_REEMIT=1). The SF100 pair 2026-08-07 (ctl 6c173cf 16:07 / trt 944e640 16:29) proved the guard mechanism itself sound (guard_wait_ms median 46ms, retro-filter at exact dim selectivity) but exposed the relaxation's structural blind spot: the barrier also protects the mid-scan's OUTPUT volume. At SF100 the mid's 1-2s scan always ends before the dim bloom arrives (2-4s), so its output ships 100% unfiltered — full supplier (8×240K rows) into the broadcast replicate + join build instead of the nation-filtered ~8% — costing Q05/Q07/Q21 +33-60% in both guarded arms. Re-emitters keep the barrier until a shape exists that starts the scan early WITHOUT shipping the unfiltered head (e.g. worker-side scan-start hold on the deferred bloom).
var DimensionCascade atomic.Bool
DimensionCascade gates markDimensionCascade. Kill switch WADJET_DIMENSION_CASCADE=0.
var DimensionCascadesPlanned atomic.Int64
DimensionCascadesPlanned counts cascade annotations, process-wide.
var DynamicFiltersPlanned atomic.Int64
applyDynamicFilters walks stages and adds Emit/Consume annotations + the stat-dep edge for every eligible hash_join. Returns the (possibly mutated) stages slice. Pure local mutation; no error path because all failures are degraded silently (the join simply doesn't get a dynamic filter, which is always safe).
Safe to call multiple times — already-annotated stages are detected by the presence of EmitDynamicFilters/ConsumeDynamicFilters and skipped. DynamicFiltersPlanned counts join annotations the dynamic-filter pass produced, process-wide. Observability: A/B runs use it (via the coordinator's dispatch logs) to prove the pass actually fired — the 2026-07-08 revisit pair was unverifiable without it, exactly the failure mode SortMergeJoinsPlanned exists to prevent.
var ElidedCoPartitionedExchanges atomic.Int64
ElidedCoPartitionedExchanges counts identity exchanges removed at plan time (observability; mirrors coordinator.SkewSplitsPlanned).
"correlated subquery requires per-row execution, which the stage DAG does not support")
ErrCorrelatedSubqueryDistributed marks a plan the stage DAG refuses because it contains a subquery correlated on the outer query's rows (#359). Such a subquery must re-execute once per outer row, and a worker fragment has no SubqueryRunner: before this refusal existed, a correlated EXISTS failed the task outright while a correlated SCALAR was mis-deferred to a producer stage whose dangling outer reference evaluated NULL — the query answered 0, silently, on a distributed deployment and correctly single-process.
Only a NON-EQUI correlation reaches this refusal: equality correlations are decorrelated into joins by the logical optimizer (TPC-H Q17/Q20/Q22), which is why the silent half went unnoticed.
The refusal is typed so the coordinator can route the query onto its local single-process pipeline — the engine that owns correlated-subquery semantics — instead of surfacing the error (see Coordinator.runCorrelatedLocal). A caller without a local engine reports it, which is still strictly better than a confident wrong answer. The real distributed algorithm for this shape is a dependent join / general decorrelation, a separate feature; this mirrors how INTERSECT/EXCEPT were refused (#346) until they grew distributed stages.
var ErrDistinctDistributed = errors.New(
"DISTINCT in this position has no distributed stage")
ErrDistinctDistributed marks a plan the stage DAG refuses because it carries a DISTINCT the DAG has no stage for.
walkStages treats NodeDistinct as a passthrough and emits nothing (#163). Two things compensate, and each covers only part of the space:
- logical.rewriteDistinctAsGroupBy turns Distinct(Project) into a GroupBy aggregate, wherever it sits, so it gets a real stage. It declines a projection it cannot turn into a group key — an aggregate projection (SELECT DISTINCT a, SUM(b) …) or one containing a subquery.
- The coordinator deduplicates the gather result when logical.ExtractMergeInfo reports HasDistinct, which it can only see for a Distinct on the ROOT path (below Limit/Sort/Project chains).
A Distinct that both decline is executed by nobody, and the DAG answers with every pre-dedup row: `SELECT COUNT(*) FROM (SELECT DISTINCT c FROM t) u` returned the raw count distributed and the right one single-process (#466). Refusing is the #308 position — a deterministic loud failure beats a silently different answer — and it is a refusal the rewrite is expected to make unreachable for every shape it handles.
The refusal is a HANDOFF, not the query's outcome: Coordinator.ExecuteSQL matches this error and answers on the coordinator-local single-process pipeline, which applies a Distinct wherever it sits (runDistinctLocal, the same move #359 makes for correlated subqueries). A caller with no local engine reports it. What the refusal buys either way is that nothing carrying DISTINCT semantics reaches walkStages.
var ErrGroupKeyDistributed = errors.New(
"this GROUP BY key needs a published name the stage cannot carry")
ErrGroupKeyDistributed marks a plan the stage DAG refuses because the value a GROUP BY key names reaches no fragment that could compute it.
A key has TWO names on the DAG (ADR-0026 §2): the PUBLISHED name every consumer above the aggregate reads (`Stage.GroupByCols`), and the RESOLUTION spelling the fragment that computes the key looks up in its own input (`Stage.GroupByResolve`). They are the same string for every ordinary `GROUP BY c` and every ordinary `GROUP BY c + 1`. They are different strings whenever the key names a derived table's alias — a join's stream carries `w` where the query wrote `x.w`, and `y.w` where the join qualified a duplicate, and the defining expression `a * 3` names a column the join does not carry at all.
While the two shared one field, four classes of shape were wrong or refused, and this error carried all of them. Each is now answered by construction:
- a key an aggregate DIRECTLY BELOW already publishes (`SELECT DISTINCT g + 1 AS k … GROUP BY g + 1`) resolves by that published COLUMN, not by re-deriving `g + 1` against a schema that no longer has `g`;
- a derived key whose published name an aggregate output also answers to resolves by its hidden `__gb_expr_N` slot and publishes under its canonical text, and the merge above it addresses the aggregates by ORDINAL (`mergeByPosition`) rather than by that shared name;
- a key naming a derived table's computed alias — window-wrapped or not — resolves against what the producing fragment really emits, decided after the projection passes by `resolveStageGroupKeys`;
- the merge boundary needs no agreement at all: a merge-mode aggregate reads a partial's OUTPUT, where every key is already a column under its published name, so it carries no resolution list (#794).
What remains refused is two classes, both stated by `resolveStageGroupKeys` and neither inferred from a node kind:
- a plan in which NOTHING emits the value. A derived arm whose inner ORDER BY / LIMIT stopped `attachScanSelectProjections` from materializing its alias, read through a join whose exchange manifest ships neither the alias nor the expression's columns — the stream model states it exactly and the error carries the columns the stream does have;
- a key whose expression contains an AGGREGATE or a WINDOW call, which a pre-aggregate PROJECTION cannot evaluate at all: the value that call names was computed by the operator below and published under a slot the expression does not spell. `SELECT DISTINCT g, COUNT(*) + 0 AS w … GROUP BY g` is where such a key comes from — the DISTINCT lowering makes every SELECT item a key, so the CALL becomes a key expression. Both of that key's names are that text and they AGREE; there is nothing for the carrier to separate, and the repair belongs to the lowering.
The refusal is a HANDOFF and not the query's outcome: `Coordinator. ExecuteSQL` routes it to the coordinator-local single-process pipeline, where the derived table's Project is a real operator and the alias is a real column, and the query answers PostgreSQL's rows. It shares that route with `ErrDistinctDistributed` and `ErrGroupingSetsDistributed`.
Its worst case is NOT "a slow correct answer": `runRefusedLocal` runs under a budget of 8× `localFastPathBytes`, so a routed query carrying a JOIN under a small `--local-fastpath-bytes` can fail LOUDLY where the DAG would have completed. Every shape that reaches it today is base-WRONG → routed-right, which is an improvement in kind; the residual risk is a base-RIGHT shape routing into that budget, which nothing in the corpus produces.
var ErrGroupingSetsDistributed = errors.New(
"GROUPING SETS / ROLLUP / CUBE has no distributed stage")
ErrGroupingSetsDistributed marks a plan the stage DAG refuses because it carries GROUPING SETS / ROLLUP / CUBE, which the DAG has no representation for at all.
`logical.buildGroupingSets` emits ONE Aggregate whose GroupBy is the UNION of every set's terms, with the sets themselves as node metadata. The single-process builder reads that metadata into `exec.HashAggregate. GroupingSets`, which inserts each row once per set under a set-prefixed key and leaves the out-of-set columns NULL. `walkStages` reads it nowhere: `Stage` has no field for it, `distributed.OpSpec` has no wire tag for it, and no worker sets `hashAgg.GroupingSets`. The information is destroyed where the stage's `GroupByCols` are copied, and is unreconstructible below that.
So the DAG ran the union of the terms as a PLAIN GROUP BY and returned it as the answer. Measured against PostgreSQL 17 over `collslot`:
GROUP BY GROUPING SETS ((g), (h)) PG 7 rows, DAG 12 — the CROSS PRODUCT GROUP BY ROLLUP (g) PG 4 rows, DAG 3 — no grand total
Silently, and for PLAIN column keys, which is wider than the filing said.
Refusing is the #308 position, and this is a HANDOFF rather than the query's outcome: Coordinator.ExecuteSQL matches this error and answers on the coordinator-local single-process pipeline, exactly as it does for ErrDistinctDistributed — the same class of defect one construct over, where walkStages drops a construct it has no stage for.
The refusal is deliberately UNCONDITIONAL rather than "only where the sets differ from the union". A single-set GROUPING SETS is a plain GROUP BY and would be safe to run on the DAG, but a predicate that has to be exactly right about which shapes are equivalent is the kind that drifts; and no such query is written by hand. When a Stage learns to carry the sets this refusal goes away with it.
var ErrInSubqueryDistributed = errors.New(
"IN subquery in this position has no distributed stage")
ErrInSubqueryDistributed marks a plan the stage DAG refuses because it carries an IN-subquery the planner could not materialize into a literal set. The coordinator routes it onto the local single-process pipeline, where expr.InSubquery resolves the set once under resolveMu and caches it.
var ErrScalarSubqueryProjectionDistributed = errors.New(
"scalar subquery in a SELECT-list item has no distributed lowering")
ErrScalarSubqueryProjectionDistributed marks a plan the stage DAG refuses because a SELECT-LIST item contains a subquery.
The DAG lowers a scalar subquery in a PREDICATE: walkStages replaces it with a `:scalar_N` placeholder, emits a producer stage for it, records the edge in Stage.ScalarDependencies, and the coordinator substitutes the producer's value into the filter text before dispatch (resolveFilterSubqueries → emitScalarProducerStages → substituteScalarDependencies). There is no such machinery for a PROJECTION: attachScanSelectProjections attaches the SELECT list verbatim, and the worker's expression compiler has no SubqueryRunner, so every task failed three times with
compile projection "(SELECT MAX(v) FROM c)": subqueries require a SubqueryRunner
for a query PostgreSQL answers and the single-process pipeline answers (#659). Loud, but the query HAS an answer and one engine in this process can compute it — so the planner refuses BEFORE stage generation and the coordinator routes it onto its local pipeline, exactly as it does for a correlated subquery (#359), an unstageable DISTINCT (#466) and an unmaterializable IN set (#524).
The refusal is not CTE-specific: the same failure reproduces for a subquery over a base table or a dimension. What it does NOT cover is a subquery in a WHERE or a HAVING, which the deferral machinery above really does lower — those keep running on the DAG.
var ErrUnreachableGatherOutput = errors.New(
"the stage DAG computed no SELECT list for this shape")
ErrUnreachableGatherOutput marks a plan whose gather renames a source no stage emits — a SELECT list that became nobody's job.
It is refused at PLAN time, not at dispatch, so the coordinator can route the query onto its local single-process engine and ANSWER it, the way it already does for a correlated subquery (#359), an unstageable DISTINCT (#466), an unmaterializable IN set (#524) and a SELECT-list subquery (#659). The alternative is what the DAG did before: hand the client the producer's raw columns under their source names — `[__win_0, n_nationkey]` for a query that asked for one column `x`.
var ExchangePartialAggMarked atomic.Int64
ExchangePartialAggMarked counts exchanges marked for sender-side partial aggregation at plan time (observability parity with ElidedCoPartitionedExchanges).
var ExchangeSubsume atomic.Bool
ExchangeSubsume gates dedupeSubsumedScanExchanges. Kill switch WADJET_EXCHANGE_SUBSUME=0. Exported atomic.Bool (ScalarAggSemijoin pattern) so tests can pin either arm.
var LateMatJoinsPlanned atomic.Int64
LateMatJoinsPlanned counts local-pipeline hash-join probes planned with late materialization enabled. Observability-first, mirroring SortMergeJoinsPlanned: dormancy tests assert it stays zero with the flag off, and A/B arms use it (with exec.LateMatBatchesEmitted) to prove the treatment engaged rather than inferring from wall-clock deltas.
var MetadataCountsPlanned atomic.Int64
MetadataCountsPlanned counts metadata-answered COUNT(*) plans (observability-first, mirrors TopNLateMatPlanned).
var MetadataMinMaxPlanned atomic.Int64
MetadataMinMaxPlanned counts metadata-answered MIN/MAX plans (observability-first, mirrors MetadataCountsPlanned).
var NullAwareAntiForcedBroadcasts atomic.Int64
NullAwareAntiForcedBroadcasts counts the joins whose build side was forced to REPLICATE against the size decision because the join is null-aware (#507): NOT IN's three-valued rule reads one fact off the WHOLE build, and a hash-partitioned build splits it.
Exported because it is the only way to see the trade from outside: the answers stay right either way, and what changes is that a build the broadcast threshold would have refused — including one refused explicitly by BroadcastBytesThreshold < 0 — is now replicated to every task. #539 tracks the shape that removes the need.
var ProbeSplitMinBytes int64 = 64 * 1024 * 1024
ProbeSplitMinBytes is the minimum size of the largest scan required to activate probe-split. Below this, the orchestration overhead exceeds the parallelism benefit. Exported so tests can lower it to exercise the distributed path on tiny datasets — otherwise every test silently runs the single-worker path and distributed-only bugs (like the SF100 build cache Q02 regression) never get caught.
var ReverseBloomsInstalled atomic.Int64
ReverseBloomsInstalled counts reverse-bloom filters actually pushed onto a build-side scan. A gate that means to exercise this path asserts on it: without it, a test can only prove the query answered, not that the optimization it was written for ever engaged.
var ScanFilterPushdowns atomic.Int64
ScanFilterPushdowns counts filters (conjuncts) pushed into scans.
var SemiAntiBuildFilter = optswitch.Register("semianti-build-filter", "WADJET_SEMIANTI_BUILD_FILTER",
"semi/anti build-side filtering: filter a shared build scan by the probe stage's key set")
SemiAntiBuildFilter gates markSemiAntiBuildFilters. Kill switch WADJET_SEMIANTI_BUILD_FILTER=0.
Registered with optswitch rather than kept as a bare atomic.Bool (the ExchangeSubsume pattern it used to follow). The pass drops build-side rows against a key set collected from another stage, so it can change the ANSWER — which is the definition of a switch the invariance oracle must enumerate (#287). An ad-hoc env var is invisible to optswitch.All(), so the oracle never ran a single corpus query with this optimization off.
var SemiAntiBuildFiltersPlanned atomic.Int64
SemiAntiBuildFiltersPlanned counts build-filter annotations the pass produced, process-wide. Mechanism marker for A/B runs (the DynamicFiltersPlanned convention).
var SemiAntiNE atomic.Bool
BuildSemiAntiFilter compiles a non-equality join filter string (e.g., "l_suppkey != l_suppkey") into a function that evaluates the condition on probe and build batch rows. Convention: left of operator = probe column, right = build column.
The returned closure lazily resolves column indices on first call and caches them, avoiding per-row ColumnByName lookups. Comparisons use typed dispatch (int32, int64, float64, string) instead of fmt.Sprint conversion.
HashJoin's probe runs in parallel — multiple workers call this filter concurrently against probe and build batches whose schemas are stable across the lifetime of the query (same logical plan → same projected columns). Use sync.Once to resolve indices safely on first call; later calls become a single relaxed atomic load on the once.done flag. SemiAntiNE gates the distinct-pair semi/anti build fast path (exec/join_semianti_ne.go). Kill switch WADJET_SEMIANTI_NE=0.
var ShapeOnlyColumnsPlanned atomic.Int64
ShapeOnlyColumnsPlanned counts columns handed to the scan for lengths-only decode. The optimization-invariance oracle asserts the corpus engages it.
var ShardedSortFinals atomic.Bool
ShardedSortFinals gates the shard-local sort/limit fold in fuseSortIntoPredecessor (sharded sort/limit finals). Kill switch WADJET_SHARDED_FINALS=0 restores the Singleton collapse. Exported atomic.Bool (the ScalarAggSemijoin pattern) so tests can pin either arm.
SharedSubplanDedup gates dedupeSharedSubplans. Kill switch WADJET_SHARED_SUBPLAN=0. Exported atomic.Bool (ScalarAggSemijoin pattern) so tests can pin either arm.
var SortMergeJoinsPlanned atomic.Int64
SortMergeJoinsPlanned counts joins routed through the sort-merge path, process-wide. Observability: the TPC-H forced-on gate test uses it to prove the route fired (and, on the default config, that it stayed dormant); ops dashboards can sample it the same way.
var StageFusion atomic.Bool
StageFusion gates fuseStageChains. Kill switch WADJET_STAGE_FUSION=0. Exported atomic.Bool (AggOverExchange pattern) so tests can pin either arm.
var StageFusionAgg atomic.Bool
StageFusionAgg gates the join→partial-aggregate absorb (step 2) on top of the join→join fusion. Sub-switch WADJET_STAGE_FUSION_AGG=0 isolates the two mechanisms for A/B; the master WADJET_STAGE_FUSION=0 kills both.
var TopNLateMatPlanned atomic.Int64
TopNLateMatPlanned counts top-N pipelines planned with late materialization. Observability-first (mirrors LateMatJoinsPlanned): dormancy tests assert zero with the switch off; engagement tests use it instead of inferring from wall clock.
Functions ¶
func AssertExchangeConsistency ¶
AssertExchangeConsistency walks every (producer, consumer, slot) edge in the stages slice and asserts that producer.Distribution.Satisfies( RequiredChildDistribution(consumer, slot)). Returns the first violation as an error, or nil if all edges are consistent.
In BehaviorPreservingMode, violations are logged at WARN and nil is returned — Phase 1 is purely additive and must not block any plan that the heuristic switch would otherwise accept.
Phase 2 promotes this to the satisfaction check that drives Exchange insertion: a violation triggers an Exchange stage being added, not a plan rejection.
func BuildAggregateShuffleSQL ¶
func BuildAggregateShuffleSQL(cand AggregateShuffleCandidate, stages []Stage) (string, error)
BuildAggregateShuffleSQL reconstructs the SQL text for the derived-aggregate pre-compute task from a candidate + the physical stage graph. The coordinator dispatches this SQL as a normal pipeline task so the workers run it via the same execution path that handles any other GROUP BY query; the output rows are written to S3 and later streamed into probe tasks as a pre-computed build input.
Phase 1 scope: single-scan-rooted aggregate with simple GROUP BY and supported aggregate functions. Scan filters push through unchanged. Any shape the reconstruction cannot represent causes a caller-level fallback to the existing in-pipeline execution — safety first, performance later.
func BuildJoinResidualFilter ¶
func BuildJoinResidualFilter(filter, buildAlias string) func(probe *batch.RecordBatch, probeRow int, build *batch.RecordBatch, buildRow int) bool
BuildJoinResidualFilter compiles an outer join's ON-clause residual — every conjunct that is not an equi-join key pair — into a predicate over the COMBINED row: the probe row plus one candidate build row (#358).
An outer join's ON runs BEFORE the NULL-padding, so this residual cannot be a filter above the join (that deletes the preserved rows) and cannot be pushed into a preserved side's scan (that deletes the rows the join owes unmatched). The executor evaluates it per key-matched candidate; see exec.HashJoin.Residual for the unmatched semantics it feeds.
BuildSemiAntiFilter is not reusable here: it only expresses `probeCol OP buildCol` and ignores NULLs, while a residual must take literals (`r.r_regionkey < 3`), arithmetic (`n.x = r.y + 3`) and SQL three-valued logic (a residual evaluating to NULL rejects the candidate, but NOT of it must not accept). This is a small AST interpreter instead: per-row and boxed, which is acceptable for a capability the planner previously refused outright — no existing plan shape gains this code path.
Column resolution against the two sides is by name, decided lazily on the first evaluated pair and cached: a qualified name is looked up verbatim in the probe then the build schema (self-join chains carry qualified columns); a qualifier equal to buildAlias forces the build side; otherwise the bare name resolves probe-first. An unresolvable column makes every evaluation UNKNOWN (candidate rejected) and logs once — the planner ships JoinFilter columns through NeededColumns, so a miss here is a plan bug, not user error.
Returns nil when the expression contains a shape the interpreter does not support; the caller must then refuse the plan loudly rather than drop the conjunct.
func BuildSemiAntiFilter ¶
func BuildSemiAntiFilter(filter string) func(probe *batch.RecordBatch, probeRow int, build *batch.RecordBatch, buildRow int) bool
func CanProbeSplit ¶
func CanProbeSplit(stages []Stage, workerCount int) (probeAlias string, probeFiles []string, ok bool)
CanProbeSplit returns the scan alias and file list for probe-split pipeline routing. Probe-split distributes the dominant probe table's files across workers while each worker scans build tables in full. This enables parallel execution for join-heavy queries where compute is the bottleneck.
Returns the probe scan alias, its file list, and true if probe-split is viable.
func CountJoinStages ¶
CountJoinStages returns the total number of joins in the stage list, including hash_join and broadcast_join stages plus fused joins that were absorbed into parent stages by fuseJoinStages().
func GatherOutputSchema ¶ added in v0.18.1
GatherOutputSchema returns the plan-declared output schema carried on a stage DAG's terminal gather, or nil when the plan could not declare one.
The coordinator calls it for the case its own answer cannot cover: a zero-row result has no batch to read a schema off, so `gatherSchema` over the gathered batches returns nil and pgwire falls back to declaring OID 25 (text) for every column. Names already survive that case through OutputRenames; this is the other half (#416).
func GatherOutputWireUnconstrainedDecimal ¶ added in v0.18.1
GatherOutputWireUnconstrainedDecimal is GatherOutputSchema's companion for the DECIMAL output columns whose PostgreSQL wire typmod must say "unconstrained" (-1) regardless of whether the result has rows — an aggregate function call, unlike a bare column reference (FIX 2, #457/#458 fold-in; see declaredWireUnconstrainedDecimal).
func HashPartitionCount ¶
HashPartitionCount is the one width rule for hash exchanges whose requirement doesn't pin a count: workerCount × 8, floor 16 — the same rule the join planner uses for its shuffle inputs (higher counts cut per-task hash state). Until 2026-08-03 the count-unpinned path (grouped final_aggregate and window inputs) defaulted to workerCount, so the REDUCE side ran node-count-wide while the map side ran core-scaled: SF100 Q20's 54.5M-group final_aggregate ran 3 tasks of ~23s and ~7.7GB tracked heap each, 4× slower than Trino on the same shape. Single- process planning (workerCount <= 1) keeps one partition — width there comes from morsel parallelism, not partition fan-out.
func IsPGSystemColumn ¶ added in v0.18.5
IsPGSystemColumn reports whether a name is one of those, for the DML doors. They do not go through this package's validation at all, and their own name-resolution step (#678) has to make the same allowance the query path makes — otherwise `DELETE ... WHERE ctid = '(0,1)'`, which PostgreSQL accepts and this engine deliberately answers by matching nothing, would become a 42703.
func NewComputedColumnsOp ¶ added in v0.18.3
func NewComputedColumnsOp(cols []exec.ProjectColumn) exec.UnaryOperator
NewComputedColumnsOp returns an operator that passes every input column through and appends the computed ones.
The type is aggPreProject, named for its first caller. It is exported through a constructor rather than moved because it has a second caller now with the same need and none of the aggregate's context: the window fragment, which must compute an expression PARTITION BY key before exec.Window can resolve it by name (#585) and which — like the pre- aggregate projection — cannot narrow the batch, since the window's output is every input column plus its own.
func ParseSemiAntiNE ¶
ParseSemiAntiNE recognizes a join filter that is EXACTLY one column-to-column not-equal condition ("l1.l_suppkey <> l2.l_suppkey"). That is the decorrelated-EXISTS self-inequality class the distinct-pair build serves; anything else (conjunctions, other operators, literals) returns ok=false and stays on the generic closure path.
func ProjectionOutputType ¶
ProjectionOutputType is inferProjectionType for callers outside this package. The worker's pre-aggregate projection compiles a derived GROUP BY key from its SQL TEXT and has no catalog to resolve the columns in it, so it needs the same rule the planner applies to a SELECT-list expression — the same reason distributed.AggSpec.InputType is carried on the spec.
It used to declare every derived key String, which is right only when the expression returns one: CAST(l_shipdate AS DATE) evaluates to an epoch-day number, and a String vector stored it as the DIGITS of that number, so the stage DAG grouped by "8039" where the single-process path grouped by 1992-01-05 (#340).
Only a DECIDED type is taken. A polymorphic declaration that answered with its own fallback (expr.Guessed) has decided nothing here, because the caller holds no column types for it to consult: COALESCE(n_name, n_comment) would answer Float64 from coalesce's numeric fallback, and a Float64 vector drops every string it is handed — 1 group where there are 25 (#331/#333). The caller's fallback stands in those cases, exactly as before.
func RefuseReservedSlotName ¶ added in v0.18.6
RefuseReservedSlotName is the 42939 refusal for a name a user is CREATING.
func RefuseReservedSlotNames ¶ added in v0.18.6
RefuseReservedSlotNames refuses the first colliding name in names.
func ReservedSlotFamily ¶ added in v0.18.6
ReservedSlotFamily returns the slot prefix name collides with, or "".
func SemiAntiBuildStoreCols ¶
SemiAntiBuildStoreCols returns the build-side columns a filtered semi/anti join must retain in stored build batches: the join keys (required to re-index spilled partitions and to survive FixKeyAssignment's rebuild) plus the JoinFilter's build-side columns. Returns nil when the filter is empty — unfiltered semi/anti builds are key-only and store nothing. Shared by the single-process planner and the worker fragment executor so both paths narrow their builds identically.
func SetExchangePartialAggEnabled ¶
SetExchangePartialAggEnabled toggles the pass (A/B tests run both arms in one process, where the env-var gate is already latched). Returns the previous value so callers can restore it.
func SlotName ¶ added in v0.18.6
func SlotName(family SlotFamily, n int) string
SlotName mints the Nth slot of a family.
func ValidateNativeDAGShape ¶
ValidateNativeDAGShape walks the stage list and returns an error describing the first stage whose shape the native-DAG executor cannot consume. Called by the coordinator before dispatch so plan-shape problems surface as a clear fail-fast at plan time, instead of as silent timeouts (Q01 SF10 2026-04-23 case) or mid-execution input-mapping errors (Q02 SF10 case).
Each branch encodes a contract the executor relies on:
- hash_join / broadcast_join: exactly 2 deps, no FusedJoins. buildTaskInputsForStage maps probe→[0] / build→[1]; >2 deps means the planner left a fused-broadcast tree the dispatcher can't unpack.
- exchange-repartition / replicate / gather: exactly 1 dep (Exchange stages bridge a single child distribution to the parent's required distribution).
- MergeGroupCount > 0: stage is the intermediate tier of a multi-level merge_aggregate / merge_sort tree. collapseMergeTreesForNativeDAG should have flattened it; if it slipped through, dispatch creates the SF10 N-stage thrash.
func WithManifestSnapshot ¶ added in v0.18.3
func WithManifestSnapshot(ctx context.Context, snap *ManifestSnapshot) context.Context
WithManifestSnapshot attaches snap to ctx. A coordinator entry point that handles one statement end to end but builds several Planner instances for it — each construction is a separate physical.NewPlanner call, so a Planner-instance-scoped snapshot alone cannot span them — calls this ONCE near the top, before any planning begins, and passes the resulting context to everything downstream. NewPlannerForContext is the pairing half: every Planner built from that context onward shares snap.
Types ¶
type AggSpec ¶
type AggSpec struct {
Func string
InputCol string
OutputCol string
// InputExpr is the SQL text of a derived input expression, e.g.
// "l_extendedprice * (1 - l_discount)". Empty when InputCol is a
// bare column reference. Distributed workers compile this into a
// Project operator before the aggregate so HashAggregate sees a
// column whose name matches InputCol.
InputExpr string
// OutputType is the plan-time output type of this aggregate, mirrored
// onto distributed.AggSpec at dispatch. Undeclared — OutputTypeKnown
// false — is only produced for a MIN/MAX-family aggregate whose input
// column does not resolve to a catalog type; see aggSpecOutputType.
OutputType parquet.TypeID
// OutputTypeKnown distinguishes a DECLARED OutputType from the zero
// value, which TypeBool shares: BOOL_AND/BOOL_OR always declare BOOL,
// and since #392 so does MIN_BY over a BOOL column. Reading that zero
// as "undeclared" is the #354/#371 shape — a declaration dropped on
// one dispatch path and re-guessed by the worker.
OutputTypeKnown bool
// OutputPrecision/OutputScale carry a DECIMAL OutputType's (p,s), for
// InputPrecision/InputScale's reason one direction over: the .wshf header
// a partial task writes carries half of every DECIMAL value it holds
// (ADR-0010), and the one output row nothing observed — the identity row
// an ungrouped aggregate emits when its filter matched no rows — has no
// input vector to read the pair from. Declaring (0,0) there made the
// aggregate merging that file read every OTHER partial's unscaled integer
// as unscaled-at-zero: SUM(a) WHERE id < 5 answered 3824.00 for 38.24
// (#685). Zero for every non-DECIMAL aggregate.
OutputPrecision int
OutputScale int
// InputType is the plan-time type of the vector InputExpr evaluates
// into, mirrored onto distributed.AggSpec at dispatch. Zero when
// there is no derived input. The worker hardcoded Float64 here, which
// is the projection-typing defect of #310/#333 living in a second
// place: MAX(COALESCE(a, b)) over two string columns wrote strings
// into a Float64 vector and the aggregate saw zeros.
InputType parquet.TypeID
// InputPrecision/InputScale carry a DECIMAL InputType's (p,s), for
// Stage.GroupByDecimal's reason: the materialized input vector is built
// from the declaration alone.
InputPrecision int
InputScale int
// InputCol2, Separator and Percentile carry the aggregate arguments
// past the first one — the second column of CORR/COVAR_*/MIN_BY/MAX_BY,
// STRING_AGG's delimiter, PERCENTILE_CONT/DISC's fraction. They are
// mirrored onto distributed.AggSpec at dispatch. Before #353 nothing
// carried them at all: the parser kept only Args[0], so MIN_BY had no
// ordering column and answered NULL, STRING_AGG ignored the separator
// the query asked for, and PERCENTILE_CONT read its fraction as 0.
InputCol2 string
Separator string
Percentile float64
// Distinct is SQL's `AGG(DISTINCT x)` for every aggregate but COUNT,
// which travels as the Func string "count_distinct" instead. It is
// mirrored onto distributed.AggSpec at dispatch and read back into
// exec.AggColumn.Distinct by the worker; without it a distributed
// SUM(DISTINCT x) was a plain SUM (#703). Every DISTINCT aggregate
// already forces the one-level RawInputAggregate shape (hasDistinctAgg),
// so the worker's final stage sees raw rows and the set is exact.
Distinct bool
}
AggSpec defines an aggregation in a stage.
type AggregateShuffleCandidate ¶
type AggregateShuffleCandidate struct {
JoinStageID string // outer join whose build side is a derived aggregate
AggregateStageID string // aggregate stage directly feeding the join (through any shuffles)
InputScanID string // base-table scan feeding the aggregate's input
InputScanAlias string // scan alias (e.g. "lineitem:1" for Q17's inner scan)
InputScanBytes int64 // EstimatedBytes of the aggregate's input scan
GroupByKeys []string // the aggregate's GROUP BY columns (= partition keys)
JoinBuildKeys []string // the outer join's keys on this side (must be ⊆ GroupByKeys)
JoinProbeKeys []string // the outer join's keys on the probe side
}
AggregateShuffleCandidate describes a join in the plan whose build side is a derived aggregate subplan (e.g. Q17's decorrelated scalar subquery aggregate over full lineitem). When the aggregate's input scan is large enough that broadcasting the whole subplan to every probe-split worker would cause memory pressure, the coordinator can dispatch a distributed partial-then- merge aggregate stage shuffled by the GROUP BY keys — mirroring the shape PickShuffleCandidate returns for base-table builds.
Phase 1 detection: aggregate(GROUP BY K)(scan(T)) feeds a hash_join, and K ⊇ join keys (so the partitioning lines up with the join).
func PickAggregateShuffleCandidate ¶
func PickAggregateShuffleCandidate(stages []Stage, thresholdBytes int64) (AggregateShuffleCandidate, bool)
PickAggregateShuffleCandidate scans stages for a join whose build side is a derived aggregate over a scan larger than thresholdBytes. Returns the first such candidate found. Phase 1: single candidate per query (matches PickShuffleCandidate's single-candidate contract).
The function is conservative: it only returns a candidate when the aggregate's GROUP BY columns include the join's equi-keys for this side. If they don't, shuffling by GROUP BY keys would not align the aggregate output with the probe side's partitioning, and the join would be incorrect. In that case we return !found and let the caller fall back to the existing probe-split or broadcast path.
type AggregateShuffleDiag ¶
type AggregateShuffleDiag struct {
Candidate AggregateShuffleCandidate
Reason AggregateShuffleRejectReason
// ObservedScanBytes holds the input scan size of the closest-matching
// rejected candidate (populated when we got past followToScan). Useful
// for tuning the threshold on real data.
ObservedScanBytes int64
// JoinStageID / InputScanAlias of the closest-matching rejected join, when
// available. Empty for NoJoin / BuildNotAggregate paths.
JoinStageID string
InputScanAlias string
}
AggregateShuffleDiag records the best observed rejection for telemetry. When a candidate IS found, Candidate is populated and Reason == None.
func PickAggregateShuffleCandidateDiag ¶
func PickAggregateShuffleCandidateDiag(stages []Stage, thresholdBytes int64) AggregateShuffleDiag
PickAggregateShuffleCandidateDiag is the diagnostic variant that also returns the reason for rejection (or Candidate + Reason=None for success). Use this when you want to log why detection declined — essential for threshold tuning on real production data.
type AggregateShuffleRejectReason ¶
type AggregateShuffleRejectReason int
AggregateShuffleRejectReason explains why a join stage was not chosen as an aggregate-shuffle candidate. Used by PickAggregateShuffleCandidateDiag so callers can log exactly which gate fired for visibility on real workloads. The set of reasons is intentionally coarse (one per gate) — finer-grained diagnostics belong in the caller's log line.
const ( AggShuffleRejectNone AggregateShuffleRejectReason = iota AggShuffleRejectNoJoin // no hash_join/broadcast_join stages at all AggShuffleRejectBuildNotAggregate // join's right dep chain doesn't terminate in an aggregate AggShuffleRejectAggNotScanRooted // aggregate isn't rooted in a single base scan AggShuffleRejectBelowThreshold // input scan bytes ≤ threshold AggShuffleRejectScanHasFilters // input scan has pushed predicates (Phase 2 scope) AggShuffleRejectKeysNotCovered // aggregate GROUP BY keys don't cover join build keys AggShuffleRejectKeyNameIsNotItsSpelling // a key's PUBLISHED name is not the spelling the scan can evaluate )
func (AggregateShuffleRejectReason) String ¶
func (r AggregateShuffleRejectReason) String() string
String renders a reject reason as a short tag suitable for logs.
type ChainedJoinSpec ¶
type ChainedJoinSpec struct {
JoinType string
JoinLeftKeys []string
JoinRightKeys []string
JoinKeyTypes []parquet.TypeID // see Stage.JoinKeyTypes (#615)
BuildDepStage string // stage providing build-side data
BuildTableAlias string
BuildColOrigins map[string]string
JoinFilter string
// FilterExprs are the absorbed stage's residual post-join filters —
// emitted as an OpFilter immediately after this chained probe.
FilterExprs []string
// BuildFilterExprs filter the build input rows before hash-table
// construction (exchange-subsume flag filters on the absorbed stage).
BuildFilterExprs []string
QualifyAllBuildCols bool
// Columns is the absorbed stage's output projection; applied as the
// chained probe's OutputFilter so the fused stage emits exactly what
// the absorbed stage emitted.
Columns []string
// JoinBuildSchema is the absorbed join's declared build columns, read
// only when that build turns out to be empty (#348).
JoinBuildSchema []parquet.Column
// Partitioned marks a hash-partitioned 1:1 build input (the absorbed
// stage was a hash_join): task i reads build partition i. False means
// a replicated broadcast build read whole by every task.
Partitioned bool
}
ChainedJoinSpec describes a 1:1 downstream join absorbed into an upstream hash_join stage by fuseStageChains. It carries everything the dispatcher needs to emit the join as a post-primary probe op in the fused fragment.
type ComputedCol ¶
ComputedCol is one appended expression column on a shuffle payload.
type DecimalCoercion ¶ added in v0.18.3
DecimalCoercion is one column that must arrive as DECIMAL(Precision, Scale).
type DistKind ¶
type DistKind int
DistKind is the kind of partitioning a stage's output has.
const ( DistSingleton DistKind = iota // single worker has all rows DistBroadcast // every worker has all rows DistHashPartitioned // rows partitioned by hash(Keys) % Count // DistRoundRobin: multiple parallel tasks, no key clustering. Used to // model multi-task partial aggregates / multi-task scans where each task // emits its own subset of the input rows. Unlike DistSingleton, this kind // does NOT trivially satisfy RequiredClusteredOn — downstream consumers // that need keys co-located must shuffle. The Phase 1 spec deferred this // label to Phase 2/3; needed once the executor wires real fan-out into // the property graph (e.g. dispatchScanAggregateStage at workerCount > 1). DistRoundRobin )
type Distribution ¶
type Distribution struct {
Kind DistKind
Keys []string // for DistHashPartitioned
// KeyTypes[i] is the type Keys[i] was HASHED at — the join key pair's
// resolved common type where one applies (#615, Stage.JoinKeyTypes).
// nil, or exec.KeyTypeUnresolved in a slot, means "the column's own
// type", which is what every same-type shuffle carries. Two shuffles
// of the same column at two different types are NOT interchangeable:
// the whole point of the resolved type is that it sends equal values to
// one partition, and a reuse across the boundary would silently unmatch
// them.
KeyTypes []parquet.TypeID
Count int // for DistHashPartitioned
}
Distribution describes how a stage's output is partitioned across workers.
func OutputDistribution ¶
func OutputDistribution(stage Stage, deps map[string]Distribution, workerCount int) Distribution
OutputDistribution computes the partitioning a stage's output has, given the resolved distributions of its dependencies. Pure function over stage fields + dep map + cluster size. Rules track how today's planner emits stages; see the Phase 1 spec §"OutputDistribution" for the per-stage table.
workerCount is needed to distinguish single-task (Singleton) from multi-task (RoundRobin) variants of stages whose dispatcher fans out at runtime (e.g. dispatchScanAggregateStage). Phase 1 punted on this and labeled everything Singleton (see spec Risk #2); Phase 3 wires the real label so the property algebra correctly forces hash-shuffles ahead of grouped finals.
func (Distribution) Equals ¶
func (d Distribution) Equals(other Distribution) bool
Equals reports whether two Distributions are identical.
func (Distribution) Satisfies ¶
func (d Distribution) Satisfies(req RequiredDistribution) bool
Satisfies reports whether this distribution meets a consumer's required distribution. Single mechanical predicate that mirrors Spark's Partitioning.satisfies(Distribution). The truth table is documented in the Phase 1 spec §"The property algebra".
RequiredAny: always true.
RequiredSingleton: only DistSingleton.
RequiredBroadcast: only DistBroadcast.
RequiredClusteredOn(K): DistBroadcast yes; DistSingleton yes;
DistHashPartitioned iff Keys==K;
DistRoundRobin no (multi-task, unclustered).
RequiredHashPartitionedOn(K, N): only DistHashPartitioned with Keys==K
and Count==N.
func (Distribution) SatisfiesJoinKeys ¶
func (d Distribution) SatisfiesJoinKeys(joinKeys []string) bool
SatisfiesJoinKeys reports whether this distribution allows a co-located join on the given keys without re-shuffling. Preserved as a thin wrapper over Satisfies for existing callers.
type DynamicFilterConsume ¶
type DynamicFilterConsume struct {
FilterID string
SourceStageID string
TargetColumn string
KeyType string
// AttachOnArrival marks this consume as non-blocking: the stat-dep edge
// is removed, the consumer dispatches immediately, and its tasks install
// the bloom mid-scan when the emitter's merged artifact lands at the
// deterministic staged key. Set only by applyAttachOnArrival under its
// structural rules; drop-only bloom semantics keep results identical.
AttachOnArrival bool
}
DynamicFilterConsume is the planner-side spec attached to a probe-side leaf scan stage. SourceStageID names the build-scan stage that emits the corresponding stats; the planner also appends SourceStageID to this stage's Dependencies so the stage DAG serializes them.
type DynamicFilterEmit ¶
type DynamicFilterEmit struct {
FilterID string
KeyColumn string
KeyType string // "int32" | "int64" | "date"
BloomBits int // total bloom-bitset size; identical across all tasks so union = bitwise OR
// AtOutput accumulates over the stage's OUTPUT stream (pre-sink) rather
// than the scan source — required when the emitting stage is a join or
// filtered scan whose output, not input, defines the key set
// (markSemiAntiBuildFilters).
AtOutput bool
// LateAttach forces the coordinator to stage the merged filter to its
// deterministic S3 key regardless of inline size — an attach-on-arrival
// consumer polls that key, so it must exist even for tiny blooms
// (applyAttachOnArrival; docs/design/attach-on-arrival-dynamic-filters.md).
LateAttach bool
// GuardConsumes lists the FilterIDs of this stage's OWN attach-mode
// consumes that must retro-filter this emit's buffered head rows at
// finalize (guarded re-emit — applyAttachOnArrival rule-1 relaxation).
// The worker's emit op buffers (emit-key, guard-column) pairs for rows
// scanned before those blooms install and drops non-matching pairs
// before the partial uploads, keeping the emitted bloom exactly as
// tight as under the start barrier.
GuardConsumes []string
// InFlow marks an emitter whose tasks must ride NORMAL scheduling
// instead of the priority lane. The lane's contract is planner-bounded
// tiny tasks (extra slots above MaxConcurrent are memory-safe only
// because dimension scans are tiny) and its purpose is overtaking bulk
// work that is ALREADY consuming the filter attach-mode. An in-flow
// cascade mid (e.g. 15M-row customer) violates the first and doesn't
// need the second: its consumer is WAIT-blocked on a stat-dep, so
// ordinary slots serve it correctly (docs/design/dimension-cascade.md
// §In-flow mid emitters).
InFlow bool
}
DynamicFilterEmit is the planner-side spec attached to a build-side leaf scan stage. Mirrors distributed.DynamicFilterEmit (separate copy keeps the physical package free of the wire-format dependency direction; the dispatcher converts at the boundary).
type ExchangeStage ¶
type ExchangeStage struct {
Keys []string // Repartition only
// KeyTypes[i] is the type Keys[i] must be HASHED at — the join key
// pair's resolved common type where one applies (#615,
// Stage.JoinKeyTypes). nil means "each column's own type", which is
// every shuffle whose two sides already agree. Hashing a cross-width
// join's two sides at their own widths sends equal values to different
// partitions, and the shuffle join then matches none of them.
KeyTypes []parquet.TypeID
Count int // Repartition only
Ordering []SortKeySpec // Gather only (optional sort-merge gather)
BuildAlias string // Repartition, Replicate
ProbeAlias string // Repartition, Replicate
BuildBytes int64 // Repartition (for logging / threshold checks)
// ComputedCols are expression columns APPENDED to the shuffle payload
// (after the projected scan columns). Set by dedupeSubsumedScanExchanges
// so one raw exchange can serve a dropped filtered sibling: the
// sibling's scan filter ships as a cheap computed flag (1 byte/row)
// instead of a second full scan+shuffle of the table. Workers evaluate
// Expr per batch and append the result under Name.
ComputedCols []ComputedCol
// ExtraReadCols are columns the shuffle's source scan must READ so the
// ComputedCols expressions can evaluate, but which are NOT part of the
// shipped payload — the worker drops them after computing the flags.
ExtraReadCols []string
// PartialAggGroupBy/PartialAggSpecs mark this Repartition for
// sender-side partial aggregation (markExchangePartialAgg): the
// shuffle task pre-combines rows on PartialAggGroupBy, shipping
// name-preserving SUM/MIN/MAX partials (OutputCol == InputCol)
// instead of raw rows. Set only when every consumer is proven
// merge-compatible; empty means ship raw.
PartialAggGroupBy []string
PartialAggSpecs []AggSpec
}
ExchangeStage carries the per-variant payload attached to an Exchange Stage. Stored on Stage.Exchange (pointer) so non-Exchange stages pay no memory cost.
Keys, Count are Repartition-only. Ordering is Gather-only. BuildAlias, ProbeAlias, BuildBytes are populated by EnsureDistribution on Repartition and (BuildAlias/ProbeAlias only) Replicate stages, so the coordinator lowering pass can synthesize ShuffleCandidate without calling PickShuffleCandidate.
type FilterAliasSpec ¶ added in v0.18.5
type FilterAliasSpec struct {
// Expr is the predicate as the query wrote it — naming the OUTPUT
// columns of the Projects between the Filter and its producer. Empty
// when the resolved spelling is the only one.
Expr string
// Names are those Project outputs, lowercased: the names that have to be
// on the producing fragment for Expr to be the evaluable spelling.
Names []string
}
FilterAliasSpec is the alternate, query-written spelling of one predicate. See Stage.FilterAliases.
type FusedJoinSpec ¶
type FusedJoinSpec struct {
JoinType string
JoinLeftKeys []string
JoinRightKeys []string
JoinKeyTypes []parquet.TypeID // see Stage.JoinKeyTypes (#615)
BuildDepStage string // stage providing build-side data
BuildTableAlias string
BuildColOrigins map[string]string // bare build col → owning scan alias (multi-table builds only)
JoinFilter string
FilterExprs []string
// JoinBuildSchema is the absorbed join's declared build columns, read
// only when that build turns out to be empty — an absorbed LEFT join
// owes the same NULL-padded columns a standalone one does (#348).
JoinBuildSchema []parquet.Column
}
FusedJoinSpec describes a broadcast join absorbed into a parent join stage.
type GroupKeyResolution ¶ added in v0.18.13
type GroupKeyResolution struct {
// Expr is the spelling the computing fragment resolves the key by: a
// column of its input when Computed is false, an expression over columns
// of its input when Computed is true.
Expr string
// Computed marks Expr as an EXPRESSION the fragment must evaluate into a
// hidden slot, rather than a name it can look up. The planner decides it;
// nothing downstream re-derives it by parsing the text, because the text
// cannot say (`GROUP BY "g + 1"` names a column and `GROUP BY g + 1` is
// arithmetic, and both are recorded as `g + 1` — ADR-0026 §2c).
Computed bool
// Alias is the key as the query wrote it when it names a derived table's
// COMPUTED alias, and "" for every other key. Planner-only.
Alias string
// Def is that alias's defining expression, re-spelled into the columns
// the derived table's own input carries. Planner-only.
Def string
// Decl is that definition's declared type, resolved in the scope the
// definition is SPELLED IN — the derived table's own input — rather than
// in the aggregate's, which cannot name its columns. Planner-only, and
// read by stageGroupKeyDecls in place of the walk that scope defeats
// (ADR-0026 §5).
Decl expr.DeclType
}
GroupKeyResolution is one GROUP BY key's resolution spelling: what the fragment computing the key looks up in its input.
Alias and Def are the planner's own deferred decision and never reach the wire. A key that names a derived table's COMPUTED alias has two candidate spellings — the alias, and the expression that defines it — and which one the producing fragment emits is decided by `attachScanSelectProjections` and `absorbWindowArmProjection`, which run AFTER `walkStages`. Emission records both candidates; `resolveStageGroupKeys` settles it at the end of planning against the producer's real output, exactly as `resolveFilterAliasSpelling` settles a predicate's spelling and `resolveDerivedAliasSortKeys` a sort key's (ADR-0025).
type ManifestSnapshot ¶ added in v0.18.3
type ManifestSnapshot struct {
// contains filtered or unexported fields
}
ManifestSnapshot pins each table's manifest, and its aggregated column stats, to ONE catalog read apiece for the life of one statement (#502) — regardless of how many scan nodes name that table (a self-join, two subqueries) or how many logical.Optimize passes re-annotate the plan.
Without it, AnnotateScanColumns, walkStages, estimateSubtreeBytes and a local-fastpath scan's Init each call catalog.GetManifest independently, and AnnotateScanColumns and a dynamic-filter NDV lookup each call catalog.AggregateColumnStats independently too — the #483 review measured 9 NATS-KV reads for a single-table SELECT, because NATSKVAdapter implements no RevisionReader, so Catalog's own revision-validated cache (manifestWithRevision) can never serve a hit against it and every one of those calls pays a full manifest fetch (694KB / 0.8ms for a 600-file manifest, +60% on a pgwire point-query micro).
The floor this snapshot reaches is TWO reads per table per statement, not one: GetManifest and AggregateColumnStats are separate Catalog operations pinned separately, and AggregateColumnStats reads the manifest a SECOND time internally (to key its own revision-validated cache) rather than accepting an already-fetched one — Catalog has no API for that today. Sharing the manifest object between the two would need one, which is a Catalog-level change past this fix's scope; filed as #540. What this DOES fix, completely, is the N-scales-with-scan-nodes-and-passes growth: 9 reads for one table's SELECT, or 2×(distinct tables) for any statement naming more than one table, however many times each is scanned or re-annotated.
It is also a correctness fix, not only a performance one (#491's review): a table scanned by more than one node in the same statement — a self-join, two subqueries — can have its scans straddle a concurrent write when each reads the manifest independently. The first scan's read can land before a DELETE commits and the second's after, and collectStageDeletes (internal/coordinator/delete_markers.go) unions the two snapshots FIRST-WINS on a file both saw — keeping the STALE, smaller marker set for every task that reads it, so rows the second scan's manifest already knew were deleted come back. Pinning the manifest per table per statement makes every scan node of that table share one ScanDeletes snapshot, so the union is over identical maps and first-wins is a genuine no-op rather than a race window.
A watch-based revision cache was considered and rejected in the issue that asked for this fix: NATS watch delivery is asynchronous relative to a Put's return, which reintroduces #483's staleness window with a smaller (but still real) gap, and fails that fix's own tests.
Callers attach one to every Planner instance built for a statement (Planner.ManifestSnapshot) before planning begins. NewPlanner gives every new Planner its own fresh snapshot, so a caller that builds exactly one Planner per statement (the embedded wadjet.DB path, the worker's local executor, the HTTP server) gets the pin for free; forSubquery's shallow copy shares the parent's snapshot with every child/subquery planner for the same reason it shares the catalog and the memory budget. A caller that builds SEVERAL Planner instances for one statement — the coordinator's scan-annotation passes, which construct a fresh Planner on every logical.Optimize iteration, and its main distributed/local-fastpath planner — must explicitly assign the SAME *ManifestSnapshot to each one; otherwise each keeps its own default and the pin only holds within a single Planner's own calls, not across the whole statement.
func ManifestSnapshotFromContext ¶ added in v0.18.3
func ManifestSnapshotFromContext(ctx context.Context) *ManifestSnapshot
ManifestSnapshotFromContext returns the snapshot WithManifestSnapshot attached, or nil if ctx carries none.
func NewManifestSnapshot ¶ added in v0.18.3
func NewManifestSnapshot() *ManifestSnapshot
NewManifestSnapshot returns an empty snapshot ready to pin a statement's first read of each table it touches.
func (*ManifestSnapshot) AggregateColumnStats ¶ added in v0.18.3
func (m *ManifestSnapshot) AggregateColumnStats(ctx context.Context, cat *catalog.Catalog, table string) (map[string]catalog.TableColumnStats, error)
AggregateColumnStats returns table's aggregated per-column stats, reading them from cat on the first call for that table within this snapshot's lifetime and returning the SAME result to every later call — the AggregateColumnStats counterpart to Get, for the same statement-pin reason (see cachedColStatsEntry).
func (*ManifestSnapshot) Get ¶ added in v0.18.3
func (m *ManifestSnapshot) Get(ctx context.Context, cat *catalog.Catalog, table string) (*catalog.PartitionManifest, error)
Get returns table's manifest, reading it from cat on the first call for that table within this snapshot's lifetime and returning the SAME manifest object (and error) to every later call for that table, regardless of which caller or which Planner instance makes it.
Concurrency-safe: a local-fastpath scan's Init can run from a parallel pipeline worker, so two goroutines racing to be the first reader of one table both complete their catalog reads, but only one result is kept — matching Catalog's own manifestWithRevision, where every caller of a race is handed the winner rather than blocked behind it.
type OutputRename ¶
type OutputRename struct {
From string
To string
Expr plansql.Node
// IsAgg marks a rename whose source is an AGGREGATE output column rather
// than a group key. The producer emits all group keys before all
// aggregates, so when an aggregate shares a name with a group key their
// select order is not their output order; the gather uses this to pair
// each rename with the column of its own class (#575).
IsAgg bool
}
OutputRename pairs a worker-emitted column name with the SELECT-list alias the user wrote. The coordinator rewrites batch and result schemas after Gather using these pairs. When Expr is non-nil, the coordinator compiles and evaluates the expression per row instead of doing a name rename — used for wrapped aggregates ("SUM(x)/7.0 AS y" emits Expr=BinaryOp{ColRef("__agg_0"), /, 7.0} because the logical planner replaces nested aggregates with refs to their synthetic OutputCol). From is the primary input column (used as the existence check); other column refs in Expr resolve via ColumnIndexFallback.
type PhysicalPlan ¶
type PhysicalPlan struct {
Pipeline *exec.Pipeline
Stages []Stage // for distributed execution
Cleanup func() // optional: called after pipeline finishes to clean up spill files
// OutputSchema is the PLAN-DERIVED output schema: the SELECT list's
// column names with the types the catalog says they carry. It answers
// the question a zero-row result leaves open, since every other source
// of a result schema in this engine reads it off a batch that never
// arrived (#416, declaredOutputSchema). Advisory: a consumed batch
// always wins.
OutputSchema []parquet.Column
}
PhysicalPlan represents an executable query plan.
func (*PhysicalPlan) PrettyPrint ¶
func (p *PhysicalPlan) PrettyPrint() string
PrettyPrint returns a formatted string representation of the physical plan.
type Planner ¶
type Planner struct {
MemoryBudget int64 // per-query memory budget in bytes (0 = unlimited)
SpillDir string // directory for spill files (empty = os temp dir)
// ManifestSnapshot pins each table's manifest to one catalog read for
// this statement (#502). NewPlanner sets a fresh one; forSubquery's
// shallow copy shares it with every child/subquery planner. A caller
// that builds several Planner instances for one statement must assign
// the SAME snapshot to each — see ManifestSnapshot's doc.
ManifestSnapshot *ManifestSnapshot
// SharedTracker / SharedSpillMgr (if set) are used in place of per-query
// Tracker+SpillManager creation. Workers set these to point at the
// executor-level pool so concurrent tasks on the same worker compete for
// ONE budget and spill cooperatively under pool pressure, matching the
// Trino/Spark unified memory manager model. When nil, getSpillManager()
// falls back to creating a per-query pool as before.
QueryLimits *config.QueryLimits // cost-based query guard (nil = no limits)
WorkerCount int // number of distributed workers (for shuffle partitioning)
// BroadcastBytesThreshold is the maximum estimated build-side size for
// a join to be planned as broadcast_join. Builds above this become
// hash_join (hash-shuffle), eliminating the N× build-cache duplication
// every worker pays under broadcast.
//
// Zero = use absolute default (100 MB), preserving legacy behavior for
// embedded callers that don't set this. Distributed callers should set
// this from per-worker pool budget — e.g., budget * 0.3 capped at
// 200 MB — so the broadcast/shuffle decision adapts to cluster memory
// instead of trusting an absolute constant. Negative = never broadcast
// (force every join to hash_join).
//
// Architectural rule the threshold encodes: broadcast is sound when
// every worker can comfortably hold the full build state in memory
// alongside concurrent hash tables. As cluster width or per-worker
// budget shrinks, the threshold shrinks too — without changing the
// planner's join-type logic.
BroadcastBytesThreshold int64
// SortMergeJoinBytes gates the sort-merge join path for big-vs-big inner
// equi-joins (docs/design/sort-merge-join.md). A join takes SMJ only when
// BOTH sides' estimated post-selectivity bytes reach this threshold —
// small builds keep the strictly-better hash/broadcast paths. Zero (the
// shipped default) disables SMJ entirely; the planner behaves exactly as
// before. Distributed callers should derive it from per-worker pool
// budget (the broadcast-threshold pattern) so "too big to sit resident"
// tracks cluster memory.
SortMergeJoinBytes int64
// LateMaterialization emits inner/left hash-join output as view
// (dictionary) columns over the probe input and build batches; the
// gather is deferred to the first consumer needing owned storage
// (docs/design/late-materialization.md). Off by default.
LateMaterialization bool
// DynamicFiltersEnabled gates the Trino-style dynamic-filter planner
// pass. When true, applyDynamicFilters annotates eligible hash_join
// build/probe leaf scans with Emit/Consume specs and adds the stat-dep
// edge from build-scan to probe-scan. Off by default for v1 rollout;
// distributed callers flip this on after the local-harness gate passes.
DynamicFiltersEnabled bool
// MaterializedInputs holds pre-scanned data for scan-split pipeline mode.
// When populated, buildScan uses these batches instead of reading from the
// object store, allowing parallel scan I/O with single-worker compute.
// Keyed by scan alias: "table" or "table:N" for self-joins.
MaterializedInputs map[string][]*batch.RecordBatch
// StreamingSources holds lazy sources for scan-split pipeline mode.
// Unlike MaterializedInputs, these yield batches on demand without
// materializing all data upfront. Checked before MaterializedInputs.
// Keyed by scan alias: "table" or "table:N" for self-joins.
StreamingSources map[string]exec.Source
// ScanFileFilter restricts which files each scan alias reads. Used in
// probe-split pipeline mode where the probe table is partitioned across
// workers while build tables read all files. Keyed by scan alias.
ScanFileFilter map[string][]string
// contains filtered or unexported fields
}
Planner converts logical plans to physical plans.
func NewPlanner ¶
NewPlanner creates a new physical planner.
func NewPlannerForContext ¶ added in v0.18.3
NewPlannerForContext is NewPlanner plus: if ctx carries a ManifestSnapshot (WithManifestSnapshot), the new Planner shares it instead of getting the fresh, private one NewPlanner otherwise gives it. Prefer this over NewPlanner at any coordinator entry point that builds more than one Planner for the same statement (#502) — a bare NewPlanner call there silently opts that Planner instance out of the statement's pin.
func (*Planner) AnnotateScanColumns ¶
AnnotateScanColumns walks the logical plan tree and populates ScanColumns on Scan nodes from the catalog. This enables the logical optimizer to resolve unqualified column references for filter pushdown through joins.
func (*Planner) AttachedFilterExprs ¶ added in v0.18.5
AttachedFilterExprs lists the predicates the last PlanDistributed's stage emission attached to a stage. Every one of them must still be readable off some stage once every rewriting pass has run — the CONSERVATION half of the #656 gate (TestStageDAGCarriesEveryFilterAndProjection).
An entry holding a NUL separates the predicate's two spellings: either may be the one resolveFilterAliasSpelling settles on.
func (*Planner) AttachedProjectionOutputs ¶ added in v0.18.5
AttachedProjectionOutputs lists the projection OUTPUT names that were on a stage the moment the last PlanDistributed finished emitting stages. Every one must still be emitted by some stage in the final plan — the projection half of the conservation gate, which a pass that deletes a projection's carrier breaks exactly as it breaks a predicate's.
func (*Planner) EstimatePlanScanBytes ¶
EstimatePlanScanBytes walks a logical plan and sums the catalog bytes (compressed parquet, after partition-filter pruning) of every scan. Returns ok=false when the plan's input size cannot be resolved from the catalog — unknown table (table functions, virtual sources) or a residual subquery expression whose scans are not visible in the tree. Callers use this to route small queries onto the coordinator-local fast path; the only safe failure mode is over-estimation, so every unknown is "too big".
func (*Planner) ExpandFederatedScans ¶
ExpandFederatedScans checks if scan stages reference tables that exist on multiple clusters. If so, it splits the scan into per-cluster scan stages and rewrites downstream dependencies. Returns stages unchanged if only one cluster has the table (or if federation lookup fails).
func (*Planner) PlanDistributed ¶
PlanDistributed generates a stage DAG for distributed execution. Returns stages with dependency ordering suitable for coordinator dispatch.
func (*Planner) ValidateColumns ¶
ValidateColumns checks that every column reference in a SELECT statement resolves to a column available in its scope, returning an error naming the first unresolvable reference. This is plan-time name binding: it runs over the SQL AST (where query-block boundaries — FROM sources, derived tables, CTEs, subqueries — are still explicit) rather than the inlined logical tree.
The binder is deliberately conservative: it errors only when a reference provably resolves to no column in any reachable source. Whenever a scope is uncertain — an open-schema source is present (table function, recursive CTE, SELECT *, a table absent from the catalog), an expression fails to parse, or a qualifier can't be matched — it skips rather than risk rejecting a valid query. A false positive breaks a working query; a false negative merely lets a typo through to the existing runtime check.
type PreComputedAggregateMeta ¶
type PreComputedAggregateMeta struct {
InputTable string
GroupByCols []string
AggSpecs []AggSpec
CacheFiles []string
}
PreComputedAggregateMeta travels on physical.Stage to tell task creation which cache files back a derived aggregate subtree the worker should substitute. Distinct from distributed.PreComputedAggregate (the wire type) — the coordinator converts between them when assembling tasks.
type ProjectExprSpec ¶
type ProjectExprSpec struct {
Expr string
Name string
Type parquet.TypeID
// TypeKnown distinguishes a DECLARED Type from the zero value, which
// TypeBool shares — the same shape as AggSpec.OutputTypeKnown (#354,
// #371). A computed BOOLEAN expression (a comparison, LIKE, IS NULL, a
// boolean literal — anything inferProjectionTypeCols resolves to
// TypeBool) otherwise reads as "not set": projectOpFromSpecs drops it
// off the wire, and the worker's buildSelectProjection then guesses
// STRING for a column that IS a bool, so a pgwire client asking for the
// true OID gets a boxed "true"/"false" string instead (#445).
TypeKnown bool
// Precision and Scale carry a computed DECIMAL's declaration alongside
// Type, for the reason DecimalCoercion carries the same pair: a DECIMAL
// is an unscaled integer plus a scale, and a worker that learns only the
// TypeID builds the output vector at scale 0 and reads every value back
// a hundredfold out (ADR-0024 item 2; #529, #555).
Precision int
Scale int
}
ProjectExprSpec is one SELECT-list item a scan fragment must emit: Name is the output column, Expr the SQL text the worker compiles and evaluates (bare column references become passthrough copies). Type is the plan-time inferred output type for computed expressions (inferProjectionTypeCols) — the worker cannot resolve it from the input schema because the output column doesn't exist there. A bare passthrough leaves Type at its zero value and the worker never consults it there (a ColRef resolves by DirectCopy instead).
type QueryCost ¶
type QueryCost struct {
TotalBytes int64
TotalRows int64
TotalFiles int
HasFilter bool
HasLimit bool
}
QueryCost summarizes the estimated cost of a query across all scan stages.
type RequiredDistribution ¶
type RequiredDistribution struct {
Kind RequiredKind
Keys []string
// KeyTypes is the resolved common type each key must be HASHED at — see
// Distribution.KeyTypes (#615). nil means "the columns' own types".
KeyTypes []parquet.TypeID
Count int
}
RequiredDistribution describes what a consumer stage requires of each input. Derived from existing stage fields (JoinLeftKeys, JoinRightKeys, GroupByCols, ShuffleKeys) by RequiredChildDistribution; never stored on Stage.
func RequiredChildDistribution ¶
func RequiredChildDistribution(stage Stage, slot int) RequiredDistribution
RequiredChildDistribution returns the per-slot required distribution for a stage's input. `slot` indexes into the stage's logical input list:
- For joins: slot 0 is the probe (LeftDepStage), slot 1 is the build (RightDepStage).
- For unary stages: slot 0 is the sole input.
- For stages with no inputs (scan, dual): RequiredAny is returned for any slot.
Never stored on Stage; recomputed by AssertExchangeConsistency. Rules are derived from how walkStages already implicitly constructs the plan; see the Phase 1 spec §"RequiredChildDistribution" for the per-stage table.
Unknown stage types return RequiredAny (no constraint asserted). New stage types added to the planner must add their rule here or accept the no-op default.
type RequiredKind ¶
type RequiredKind int
RequiredKind enumerates the partitioning a consumer needs from each input. Mirrors Spark's Distribution trait subclasses; see the Phase 1 spec (docs/archive/specs/2026-04-20-distribution-property-phase-1.md) §"The property algebra" for the satisfaction truth table.
const ( RequiredAny RequiredKind = iota // no constraint RequiredSingleton // exactly one partition (final result, coordinator merge) RequiredBroadcast // every worker has every row RequiredClusteredOn // co-partitioned on Keys, any partition count RequiredHashPartitionedOn // hash-partitioned on Keys with exactly Count partitions )
func (RequiredKind) String ¶
func (r RequiredKind) String() string
String renders a RequiredKind for log lines and assertion error messages. Stable text identifiers — do not change without checking telemetry consumers.
type ShuffleCandidate ¶
type ShuffleCandidate struct {
JoinStageID string // the join stage to be served by shuffled inputs
BuildAlias string // which scan stage produces the build side
ProbeAlias string // which scan stage produces the probe side (the largest scan)
BuildKeys []string // build-side join keys (the join's JoinRightKeys)
ProbeKeys []string // probe-side join keys (the join's JoinLeftKeys)
JoinKeys []string // canonical (build-side) join key names for partitioning
BuildBytes int64 // EstimatedBytes of the build scan (for logging)
}
ShuffleCandidate describes a join in the plan whose build side is large enough to warrant the shuffle execution path instead of broadcast.
func PickShuffleCandidate ¶
func PickShuffleCandidate(stages []Stage, thresholdBytes int64) (ShuffleCandidate, bool)
PickShuffleCandidate identifies the largest non-probe scan above thresholdBytes as the shuffle candidate — the table that would otherwise be broadcast-duplicated as the runtime build side — and returns the join stage that connects it to the probe.
The approach deliberately does NOT read BuildTableAlias on the join stage because in probe-split mode the planner's logical build/probe assignment is inverted at runtime: the planner labels the largest scan as the build (e.g. "lineitem"), but probe-split partitions that scan across workers, making the second-largest scan (e.g. "orders") the actual broadcast hash table. Shuffling orders instead of broadcasting it is the correction.
Algorithm:
- probeAlias = largest scan (matches CanProbeSplit's heuristic).
- candidate = largest non-probe scan above thresholdBytes.
- Walk join stages to find one that directly references the candidate scan (via LeftDepStage or RightDepStage) or via a FusedJoin entry whose BuildTableAlias matches the candidate alias.
- Extract build/probe keys from the matching join or fused-join entry.
Phase 1: returns the single best candidate. Phase 2 (chained shuffles) will return all candidates.
type SlotFamily ¶ added in v0.18.6
type SlotFamily = plansql.SlotFamily
The reserved slot namespace lives in the PARSER package, not here.
It has to: the names are minted in three packages — the logical builder (`__win_N`), the logical optimizer (`__tl_N`) and this one (`__winkey_N`, `__sortkey_N`, `__gb_expr_N`, `__agg_expr_N`, `__scalar_N`) — and `internal/planner/logical` cannot import `internal/planner/physical`, because the dependency runs the other way. A table of reserved prefixes that half the minting sites cannot reach is a table that drifts, which is exactly what happened: `SlotCovarState` read "__covar_stat" while the reservation and `worker/var_fold.go` used "__covar_state", latent only because nothing called the constructor.
`internal/planner/sql/reserved_slots.go` is therefore the one copy, and it is what a caller outside this package should import. These aliases exist so the code already written against them keeps reading naturally.
type SortKeySpec ¶
type SortKeySpec struct {
Column string
Desc bool
NullsLast bool
// SourceExpr, SourceColumn and SourceType describe what MATERIALIZES a
// synthetic ORDER BY key — a term the SELECT list does not carry, which
// logical.resolveOrderBy named __sortkey_N. Nothing on the DAG computes
// a Project, so unless some pass puts that name on the producing stage
// the sort has no such column to key on (#424). They ride the key
// itself rather than the stage because every pass that moves a sort's
// ordering somewhere else — fuseSortIntoPredecessor's fold onto a
// join/aggregate, emitMergeSortTree, the gather's Exchange.Ordering —
// copies the SortKeySpec slice wholesale, so the definition travels with
// the key for free.
//
// SourceExpr is the term's expression text; SourceColumn is non-empty
// only when the term is a plain column reference, in which case the
// producer already emits that column under its own name and the key can
// simply be renamed to it. SourceType is the declared type a computed
// term's materialized column carries; SourceTypeKnown distinguishes a
// DECLARED SourceType from the zero value TypeBool shares (the same
// ProjectExprSpec.TypeKnown shape, #445/#472) — without it a genuinely
// BOOL sort key reads as "not set" and the materialized projection drops
// its type off the wire.
//
// Empty on every ordinary key, and read by exactly one pass
// (resolveHiddenSortKeys) — sortKeysEqual compares ORDERING, so these
// are deliberately outside that comparison.
SourceExpr string
SourceColumn string
SourceType parquet.TypeID
SourceTypeKnown bool
// SourcePrecision/SourceScale carry a materialized DECIMAL key's (p,s),
// which SourceType alone cannot: the fragment builds the key's vector
// from this declaration and a DECIMAL one with no scale reads every
// value back at 10^0 (ADR-0024 item 2).
SourcePrecision int
SourceScale int
// AliasSource is the column the producing stream carries for a key that
// names a DERIVED TABLE's SELECT-list alias — the non-synthetic sibling
// of SourceColumn above (#467, #468).
//
// `SELECT k FROM (SELECT s_suppkey AS k FROM supplier ORDER BY
// s_suppkey DESC) x` sorts on "k", because logical.resolveOrderBy binds
// an ORDER BY term to the SELECT list's OUTPUT name — which is also what
// PostgreSQL does, and is the whole point when the alias SHADOWS a base
// column of the same relation (`s_acctbal AS s_suppkey ... ORDER BY
// s_suppkey` means the alias). On the DAG that name exists nowhere
// unless attachScanSelectProjections materialized it, and only the
// OUTERMOST SELECT list gets that treatment: a derived table's sort
// either failed loud (`sort: key column "k" does not exist in the input
// schema`) or — with a shadowing alias — silently keyed on the WRONG
// column, ordering by base s_suppkey where PostgreSQL orders by
// s_acctbal.
//
// Set by annotateDerivedAliasSortKey at stage-emission time, where the
// logical Projects are still in hand, and consumed by
// resolveDerivedAliasSortKeys once planning can see whether the alias
// was materialized after all. Empty on every ordinary key, and outside
// SameOrdering for the same reason the fields above are.
AliasSource string
}
SortKeySpec defines a sort key in a stage.
func (SortKeySpec) SameOrdering ¶ added in v0.18.1
func (k SortKeySpec) SameOrdering(o SortKeySpec) bool
SameOrdering reports whether two keys impose the same order. It compares what the sort actually does — column, direction, NULL placement — and ignores the materialization fields, which say where a synthetic key comes from rather than how it sorts.
type Stage ¶
type Stage struct {
ID string
Type string // see exchange.go constants (scan, aggregate, sort, hash_join, broadcast_join, window, pipeline, exchange-repartition, exchange-replicate, exchange-gather)
ClusterID string // target cluster for routing ("" = local/coordinator's cluster)
Dependencies []string
Tasks int
// Scan metadata
TableName string
ScanAlias string // unique scan identity: "table" or "table:N" for Nth duplicate
Columns []string
// ScanSchema is the CATALOG's declared schema for TableName — the whole
// table's columns, with the parameters a bare TypeID does not carry
// (DECIMAL precision/scale, VECTOR dimension).
//
// The DAG worker's scan otherwise takes its column TYPES from the FILE,
// and a parquet file cannot express nine of this engine's types. Files
// written from v0.18.0 on stamp their declared types into their own
// footer; files written before it do not, and on those the DAG answered
// an IPv4 as 167772165 where the single-process engine (which reads the
// catalog) answered 10.0.0.5 — #396's symptom, on existing data (#423).
//
// Declared at PLAN time rather than looked up by the worker for the same
// reason as AggSpec.OutputType and the join-side schemas: one catalog
// read, one revision, one answer for every task of the query. A worker
// resolving it itself could see a different revision from its peers, and
// two tasks of one stage would type the same column differently.
//
// The WHOLE table, not the read set: the worker's projection guard
// reverts to full width whenever a requested name is missing from the
// file, and a column read under that fallback still has to be typed.
ScanSchema []parquet.Column
// ScanDeletes is the table's merge-on-read DELETE state at plan time:
// data-file path → the file-absolute row indices a DELETE removed and
// compaction has not yet folded in (catalog.PartitionManifest.
// DeleteMarkers). Read from the SAME manifest that produced ScanFiles,
// which is not an accident: markers only ever grow until a compaction
// replaces the file they name, so a marker set read AFTER the file list
// can be missing the markers of a file the list still holds, and those
// deleted rows come back. Reading both from one manifest object makes
// the pair a snapshot.
//
// Rides to the worker as Task.DeleteMarkers, stamped per task from the
// files that task actually reads. Declared at plan time for the reason
// ScanSchema is: one catalog revision for every task of the query, so
// two tasks of one stage cannot disagree about which rows exist (#491).
// Nil for every table with no deletes.
ScanDeletes map[string][]int64
// OutputColumns, when non-empty, narrows the stage's EMITTED columns
// to this set (worker inserts a zero-copy ColumnPrune before the
// sink). Columns stays the READ set — a scan must read its pushed
// filter columns but must not ship them: Q13's orders scan read
// o_comment for the NOT LIKE and then materialized+shuffled it,
// 68.8 B/row where 16 were consumed (~16 GB excess at SF100).
// Set by pruneScanOutputColumns from consumer declarations.
OutputColumns []string
PartitionFilter map[string]string
ScanFiles []string // files to distribute across scan tasks
// ScanFileSizes aligns 1:1 with ScanFiles (catalog SizeBytes). Feeds
// byte-balanced affinity fan-outs (coordinator scan_affinity.go);
// empty/misaligned degrades to count-based splitting.
ScanFileSizes []int64
FilterExprs []string // SQL filter expressions pushed down to scan
// FilterAliases is FilterExprs' second spelling, index-aligned with it:
// the predicate as the QUERY wrote it, naming a Project's OUTPUT
// columns, where FilterExprs[i] holds the same predicate re-spelled into
// the source columns those Projects read. A zero entry means the two
// spellings are the same and there is nothing to choose between.
//
// Both are needed because which one a stage can evaluate is not known
// when walkStages attaches the predicate. A Project emits no stage of
// its own, so the usual answer is the source spelling — but
// attachScanSelectProjections may later put an alias-naming OpProject on
// the producing fragment, and then the stream carries the ALIAS and not
// the source column. resolveFilterAliasSpelling settles it at the end of
// planning, once that pass has run, exactly as resolveDerivedAliasSortKeys
// settles the same question for a sort key (#656, #467).
FilterAliases []FilterAliasSpec
// Aggregate metadata
//
// GroupByCols is what the aggregate PUBLISHES each GROUP BY key as — the
// name every consumer ABOVE it reads. It is `plansql.GroupKeyName`, the
// same text the single-process planner feeds `exec.HashAggregate`, so
// both engines' aggregate output schemas are one schema (ADR-0026 §2b).
// GroupByResolve beside it is what the fragment that COMPUTES the key
// resolves it BY, against its own input.
GroupByCols []string
// GroupByResolve is GroupByCols' second spelling, index-aligned with it:
// the name or expression the COMPUTING fragment resolves each key by,
// against the columns its input carries. A stage whose fragment does not
// compute the keys — every merge-mode aggregate, whose input is a
// partial's output where the key is already a column under its published
// name — carries no list at all, and that is what makes the merge
// boundary correct by construction rather than by agreement (#794).
//
// The two are one field's worth of information only when they are the
// same string, which is every ordinary `GROUP BY c` and every ordinary
// `GROUP BY c + 1`. They are different strings whenever the key names a
// derived table's alias: the join stream carries `w` where the query
// wrote `x.w`, and the defining expression `a * 3` names a column the
// join does not carry at all. `Stage.GroupByCols` used to be both at
// once, and the worker re-derived "is this key derived?" by PARSING it —
// which is why every shape in that class answered one NULL group
// (ADR-0026 §2, §4a; #736, #777, #781, #794, #795).
//
// Index-aligned with whichever group-key list the stage carries:
// GroupByCols, FusedAggGroupBy on a fused scan-aggregate, or
// ChainedAggGroupBy on a join that absorbed one. A stage carries exactly
// one of the three; `stageGroupKeyList` is that rule, and
// TestStageCarriesOneGroupKeyList asserts it.
GroupByResolve []GroupKeyResolution
// GroupByTypes is the plan-time output type of each DERIVED (non-bare)
// GROUP BY key expression, keyed by the exact GroupByCols text — the
// same inferProjectionTypeCols answer the single-process pre-aggregate
// projection types its synthetic key columns with. Dispatch ships it as
// OpSpec.GroupByTypes so the worker's buildAggInputProjection declares
// the same vector type instead of inferring from the expression text
// with no catalog (#379: COALESCE(l_extendedprice, 0) inferred Int64
// from the literal and truncated every float group key). Bare column
// keys are absent — their vectors come from the input schema.
GroupByTypes map[string]parquet.TypeID
// GroupByDecimal is GroupByTypes' companion for the (p,s) of its DECIMAL
// entries — the part a bare TypeID cannot carry, and without which the
// worker's key vector comes out at scale 0 and truncates every value
// (ADR-0024 item 2, #379's shape one type over).
GroupByDecimal map[string]logical.DecimalMeta
AggSpecs []AggSpec
// GroupByAll marks a keys-only hash aggregate over EVERY input column —
// the DISTINCT shape. The key set is resolved at runtime from the input
// schema (no plan-time column list), matching exec.HashAggregate.GroupByAll
// and the single-process buildDistinct path.
GroupByAll bool
// Sort metadata
SortKeys []SortKeySpec
// Limit is the row bound this stage's Sort/TopN carries — meaningful
// only when HasLimit is true. A companion bool rather than a -1
// sentinel on Limit itself: Stage is built via dozens of `Stage{...}`
// literals across the planner/coordinator that never touch Limit, and
// every one of them must keep meaning "unbounded" by leaving both
// fields at their zero value. Before HasLimit existed, Limit's own 0
// doubled as that same "unbounded" sentinel, so a stage carrying a
// real `ORDER BY ... LIMIT 0` was indistinguishable from one with no
// limit at all (#481) — every reader of Limit below must consult
// HasLimit, never `Limit > 0`/`Limit == 0` alone.
// A StageLimit carries its LIMIT here too — same meaning, same
// HasLimit guard — paired with Offset below.
Limit int
HasLimit bool
// Offset is the rows a StageLimit SKIPS before it starts emitting.
// Meaningful only on that stage type: everywhere else the OFFSET is
// applied once by the coordinator over the gathered result, and a
// stage that skipped rows on its own would skip them twice. No
// companion bool — 0 rows skipped and no OFFSET are the same thing,
// unlike LIMIT, where 0 rows kept and no LIMIT are opposites.
Offset int
// RowLimit bounds how many rows this stage's tasks EMIT, for a LIMIT with
// no ORDER BY. Distinct from Limit, which is a top-N applied after a sort:
// this one lets a scan stop pulling batches once satisfied.
//
// Set only when nothing between the scan and the LIMIT can change
// cardinality (no join, aggregate, distinct or sort), so each task may
// stop at n independently. k tasks then emit up to k*n rows and the
// coordinator's gather limit trims to n — which is well-defined precisely
// because a bare LIMIT does not specify WHICH rows it returns. An
// `ORDER BY ... LIMIT` must never use this path; it goes through the
// sort/TopN stages, where Limit above applies.
RowLimit int
// SortShardLocal marks a grouped final_aggregate whose SortKeys/Limit
// are SHARD-LOCAL: the stage fans out across disjoint group-key shards
// (each computes exact aggregates for its groups, then sorts and
// applies Limit locally) and a surviving downstream Singleton sort
// stage merges the N sorted ≤Limit-row outputs. Distribution rules
// treat such a stage like a sort-free grouped final (input clustered
// on GroupByCols, output mirrors the input partitioning) — without
// this flag SortKeys/Limit force the Singleton collapse. Set only by
// fuseSortIntoPredecessor's shard-local fold.
SortShardLocal bool
// Join metadata
JoinType string // inner, left, right, full, cross
JoinLeftKeys []string
JoinRightKeys []string
// JoinKeyTypes[i] is the resolved COMMON type of the pair
// (JoinLeftKeys[i], JoinRightKeys[i]) — resolveJoinKeyTypes, #615. Both
// sides' key bytes and the exchange's partition hash are built at it.
// Nil means no pair needs widening, which is every same-type join.
JoinKeyTypes []parquet.TypeID
LeftDepStage string // stage providing probe (left) side
RightDepStage string // stage providing build (right) side
BuildTableAlias string // build-side table alias for column disambiguation in self-joins
// BuildColOrigins maps each bare build-output column (lowercased) to the
// scan alias that owns it. Only set when the build subtree spans multiple
// tables (bushy shapes) — nil for single-scan builds, where
// BuildTableAlias is already exact. The join executor qualifies duplicate
// build columns with the OWNING alias instead of BuildTableAlias.
BuildColOrigins map[string]string
JoinFilter string // semi/anti join inequality filter (e.g., "l2.l_suppkey != l1.l_suppkey")
// NullAwareAnti carries logical.Node.NullAwareAnti to the worker: this
// anti join came from a NOT IN and owes its three-valued rule, not the
// two-valued "did nothing match" an anti join asks on its own (#507).
NullAwareAnti bool
// BuildFilterExprs are row predicates applied to the BUILD input before
// hash-table insertion. Set by dedupeSubsumedScanExchanges when this
// join's build was rewired from a filtered exchange to a subsuming raw
// exchange: the dropped exchange's scan filter (or its computed
// __subsume flag) must now run at build-read time. Semantically
// identical to filtering at the dropped scan.
BuildFilterExprs []string
// JoinProbeSchema / JoinBuildSchema are the plan-declared columns of each
// join side (physical.declaredJoinSchema). The worker reads them only for
// the side that turns out to be empty, where there is no batch to learn a
// schema from and an outer join still owes the rows that side shapes
// (#348/#352).
JoinProbeSchema []parquet.Column
JoinBuildSchema []parquet.Column
// Fused broadcast joins absorbed into this stage (avoids separate
// shuffle+join stages for small dimension tables like nation, region).
FusedJoins []FusedJoinSpec
// ChainedJoins are 1:1 downstream joins absorbed into this stage by
// fuseStageChains (docs/design/stage-chain-fusion.md). Unlike
// FusedJoins (broadcast probes applied BEFORE the primary join), these
// run AFTER it, in order — the fragment pipes the primary's output
// through each chained probe in-process, eliding the per-link
// materialization the separate stages paid.
ChainedJoins []ChainedJoinSpec
// ChainedAgg* describe a downstream PARTIAL aggregate absorbed as the
// chain's terminal step (fuseStageChains step 2): the fragment runs
// OpHashAggregate (raw mode) after the chained joins, so the join
// output collapses to partials in-process instead of materializing
// for a separate round-robin aggregate stage. Partial aggregation is
// partition-agnostic, so the fused stage keeps ITS OWN distribution
// and task count — it just emits N partial outputs instead of the
// dropped stage's fan-out count; finals merge either way.
ChainedAggGroupBy []string
ChainedAggSpecs []AggSpec
// UnionArms describes each arm of a StageUnion, in SQL order. Arm i is
// produced by Dependencies[i] and dispatched as task i; Projections is
// the OpProject that normalizes that arm's output onto the result
// column names. Empty on every other stage type.
UnionArms []UnionArm
// Window metadata
WindowCols []WindowColSpec
// WindowKeyExprs are the PARTITION BY / window ORDER BY terms the window
// fragment must COMPUTE before it can key on them — an expression key
// (`PARTITION BY id % 3`) names no column any upstream stage emits, and a
// window that cannot find its key used to answer over one partition
// (#585). Each spec's Name is the term's own text, which is also the name
// WindowColSpec.PartitionBy/OrderBy carry, so the worker's projection and
// the operator agree without a second naming convention.
//
// Nothing ABOVE the window reads these columns, which is what keeps them
// clear of #558: the gather projects to the visible SELECT list and a
// consumer stage reads the window's own outputs.
WindowKeyExprs []ProjectExprSpec
// JoinPartitionCount is the number of partitions for a hash-join stage
// that was preceded by repartition exchanges. Zero means the join is
// not partitioned (broadcast or single-partition). Exchange stages carry
// their partition count on Exchange.Count instead.
JoinPartitionCount int
// Fused scan-aggregate: partial aggregation is performed at the scan
// level, eliminating the scan→aggregate S3 round-trip. Workers produce
// partial aggregate results instead of raw rows.
FusedAggGroupBy []string
FusedAggSpecs []AggSpec
// RawInputAggregate marks a final_aggregate whose input is RAW rows
// from an exchange hash-partitioned on the group keys, not partial
// aggregates — set by rewireAggOverRawExchange when it rewires the
// final from a duplicate fused scan-agg leg onto a sibling raw
// exchange, and by emitSetOpCountingStage (whose input is the raw
// tagged concatenation). Partition-disjoint keys make per-partition
// raw aggregation exact, so the dispatcher builds the fragment with
// MergeMode=false (no InputCol→OutputCol remap, no COUNT→SUM
// rewrite). AggSpecs carry the raw form (the dropped scan's
// FusedAggSpecs).
RawInputAggregate bool
// SetOp marks a final_aggregate that computes an INTERSECT or EXCEPT
// (#346): the stage GROUP BYs the full result row and SUMs the two
// per-arm tag columns (SetOpLeftCountCol / SetOpRightCountCol), and
// its fragment appends an emit operator that turns each distinct
// row's (countA, countB) into the operation's answer — one copy when
// the distinct form's membership rule holds, min(countA, countB) /
// max(0, countA−countB) copies for the ALL forms — and drops the tag
// columns. Values: "intersect", "except"; "" on every other stage.
SetOp string
// SetOpAll distinguishes the multiset (ALL) form. Meaningful only
// when SetOp is set.
SetOpAll bool
// Probe-split pipeline: partition the probe table's files across workers.
// Each worker scans build tables in full and probes its file partition.
ProbeSplitAlias string // scan alias to partition (e.g., "lineitem")
ProbeSplitFiles []string // full file list to split across tasks
// BuildCachePreScans holds pre-scanned result file paths for large build
// tables. When populated by the coordinator (after pre-scanning them once),
// workers load these cached files via PreScannedInputs instead of scanning
// the large source table N times — eliminating the N× build-side duplication
// that causes OOM on Q09 at SF100. Keyed by scan alias (e.g., "orders").
BuildCachePreScans map[string][]string
// PreComputedAggregates holds signatures + cache paths for derived-
// aggregate builds that were computed once by the coordinator before
// dispatch. Each probe-split task carries the same list; the worker's
// plan-rewrite pass matches logical Aggregate subtrees against the
// signatures and replaces them with synthetic scans of the cache files.
// Spec: 2026-04-18-shuffle-distributed-aggregate.md.
PreComputedAggregates []PreComputedAggregateMeta
// Multi-level merge: partitions upstream results among parallel merge groups.
// When MergeGroupCount > 0, this stage processes only the MergeGroup-th
// fraction of its dependency results. Independent merge groups run on
// different workers for parallel merging.
MergeGroup int // 0-based index of this merge group
MergeGroupCount int // total groups (0 = not grouped, process all results)
// Cost estimation (populated at plan time from manifest metadata)
EstimatedBytes int64
EstimatedRows int64
// Distribution describes how this stage's output is partitioned.
// Default zero value is {Kind: DistSingleton} which is correct for
// most existing stages (single-worker output). Shuffle stages set this
// to DistHashPartitioned with Keys and Count populated. Broadcast pre-scans
// (build cache) set Kind: DistBroadcast.
Distribution Distribution
// Exchange carries per-variant metadata for StageExchange* stages.
// nil for non-Exchange stages.
Exchange *ExchangeStage
// ScalarDependencies maps placeholder names (e.g. ":scalar_1") to
// producer stage IDs that emit a single-row, single-column output. The
// native-DAG coordinator awaits each producer, extracts the scalar from
// its stage output, and string-substitutes the placeholder in this
// stage's FilterExprs / AggSpecs.InputExpr before dispatching tasks.
// This lets CTE-referencing scalar subqueries share the distributed
// float-accumulation path with the filter-carrying stage's upstream,
// eliminating the single-process vs distributed bit-pattern divergence
// that caused Q15 to return 0 rows at SF0.1.
ScalarDependencies map[string]string
// OutputRenames is the SELECT-list alias map applied by the coordinator
// to the Gather stage's result schema. walkStages currently passes
// NodeProject through without applying its projections, so without this
// the final result schema carries raw worker column names ("n1.n_name",
// "substr(l_shipdate, 1, 4)") instead of the user's aliases
// ("supp_nation", "l_year"). Only populated on the Gather stage.
OutputRenames []OutputRename
// OutputSchema is the PLAN-DERIVED result schema — the same column list
// OutputRenames names, with the types the catalog says they carry. Only
// populated on the Gather stage, and read only when the gathered batches
// cannot answer: a zero-row result (#416, declaredOutputSchema).
OutputSchema []parquet.Column
// OutputWireUnconstrainedDecimal names the DECIMAL columns in
// OutputSchema whose PostgreSQL wire typmod must say "unconstrained"
// (-1) even though OutputSchema itself (and the executed result, when
// there is one) carries their real (p,s) — an aggregate function call
// never keeps its argument's typmod on live PostgreSQL. Only populated
// on the Gather stage, and unlike OutputSchema's zero-row-only role,
// consulted for every result (FIX 2, #457/#458 fold-in; see
// declaredWireUnconstrainedDecimal).
OutputWireUnconstrainedDecimal map[string]bool
// ProjectExprs, set on a leaf scan stage whose output feeds the gather
// directly, makes the scan fragment compute the SELECT list (worker-side
// exec.Project after scan+filter). Without it a bare expression SELECT
// over a scan reaches the gather as raw scan columns — the gather's
// applyOutputRenames can rename/drop but not evaluate (#169). Expression
// entries are named by the lowercased expression text, matching the
// convention extractOutputRenames already expects for worker-computed
// expressions; bare columns are passthrough entries so the fragment
// output is exactly the SELECT-list inputs.
ProjectExprs []ProjectExprSpec
// SecurityProjectExprs is the ABAC security barrier absorbed from a
// SecurityBarrier logical Project wrapping this scan
// (absorbSecurityBarrier): visible columns pass through, masked columns
// are literal expressions, denied columns are absent. Applied as the
// FIRST projection in the scan fragment — before ProjectExprs and
// before any aggregate — so restricted values never leave the worker.
SecurityProjectExprs []ProjectExprSpec
// Dynamic filter (Trino-style semi-join pushdown) annotations.
// EmitDynamicFilters is set on a build-side leaf scan stage; each task
// computes a partial KeyRange+Bloom and uploads as a sideband artifact.
// ConsumeDynamicFilters is set on a probe-side leaf scan stage; the
// coordinator unions the upstream partials and injects the result into
// each scan task's OpSpec.DynamicFilters.
//
// The stat-dep edge (emit stage ID appended to the consume stage's
// Dependencies) makes execute_stage_dag.go serialize them via the
// existing dependency mechanism — no new edge type or async broker.
EmitDynamicFilters []DynamicFilterEmit
ConsumeDynamicFilters []DynamicFilterConsume
// QualifyAllBuildCols, when true, instructs the join executor to always
// emit build-side columns under their qualified name
// ("BuildTableAlias.col_name") instead of the default behavior of
// qualifying only on probe-collision. Set by the planner when the same
// source table is scanned more than once and the scans co-path into the
// same join chain (Q07's "nation n1" + "nation n2"). Without this flag
// the FIRST self-join leaves its column unqualified, the SECOND qualifies
// only its own copy, and references to the FIRST alias resolve to NULL
// downstream.
QualifyAllBuildCols bool
// ConsumerScoped marks a stage carrying a Filter or a Project that
// belongs to ONE of its consumers — a predicate written above a CTE
// REFERENCE rather than inside the CTE's body. A stage like that must
// never acquire a second consumer, because the second would read the
// filtered stream (#656 follow-up); assertNoConsumerScopedFilterOn-
// SharedStage is the check, and ValidateNativeDAGShape runs it.
ConsumerScoped bool
}
Stage represents a unit of distributed work with metadata for task creation.
func EnsureDistribution ¶
EnsureDistribution walks the stage DAG and inserts Exchange stages wherever a child's OutputDistribution does not satisfy its parent's RequiredChildDistribution. Returns a new []Stage; does not mutate the input slice.
func LargeBuildScans ¶
LargeBuildScans returns scan stages that are build-side (not the probe alias) and whose estimated size exceeds the given threshold. These are candidates for the build-side broadcast cache: the coordinator pre-scans them once, caches the result in S3, and each worker loads the shared cache instead of independently scanning the large source table N times.
The cache provides two wins for queries with selective build-side filters or wide build tables:
- Avoids decoding the source parquet on every worker (parquet decode is CPU-expensive; the cached WSHF format is essentially raw typed bytes and reads in a fraction of the time).
- Lets the planner overlap the slow source scan with the rest of the query once instead of N times.
We previously gated this on len(large) >= 2 ("only cache when multiple large builds would compound a worker's hash table footprint"), reasoning that single-large-build queries can fit one hash table in memory and the cache only adds spill+upload latency. SF100 deploy disproved that: Q07's historical 3m13s was caching orders, and skipping it pushed the same query past 19 minutes (workers stuck spilling/scanning parquet 3 times). The win from caching orders comes mostly from amortising parquet decode, not from memory deduplication.
type UnionArm ¶
type UnionArm struct {
DepStage string
Projections []ProjectExprSpec
// DecimalCoercions names the result columns this arm must MOVE into the
// set operation's output DECIMAL(p,s) — after the projection above has
// put them under the result names — before its rows join the union
// stream. Empty for an arm that already carries the output type.
//
// It is a separate list rather than a field on ProjectExprSpec because
// it is not a projection at all: a DECIMAL value is an unscaled integer
// plus a declared scale, and making two arms agree means multiplying the
// integer, which no CAST expression in this engine does exactly (the
// cast evaluator's DECIMAL destination produces a float64). See
// exec.DecimalCoerce and issue #533.
DecimalCoercions []DecimalCoercion
}
UnionArm is one arm of a StageUnion: the stage producing it, and the projection that puts its output under the set operation's result column names. The projection is what makes the arms concatenable — without it each arm reaches the union under its own names (and a raw-parquet pass-through scan arm reaches it carrying every column of its table).
type WindowColSpec ¶
type WindowColSpec struct {
Func string
// InputCol is the column the function reads, alone: the offset, default
// and N that share a SQL argument list are parsed out into the fields
// below at plan time (logical.WindowExpr.InputCol keeps the raw list).
InputCol string
OutputCol string
OutputType parquet.TypeID
// PartitionBy is what makes a window distributable: rows of one
// partition can be windowed without seeing any other partition, so a
// hash exchange on these keys turns the stage into N independent tasks.
// Empty = a global window, which needs every row in one place.
PartitionBy []string
OrderBy []SortKeySpec
Frame *logical.WindowFrameSpec
// Function-specific arguments (see InputCol).
LagLeadOffset int
LagLeadDefault any
NtileBuckets int
NthValueN int
}
WindowColSpec defines a window function column in a stage. Every field is resolved by windowExecColumn — the same resolution the single-process pipeline compiles into exec.WindowColumn — so the stage carries a spec the worker can execute without a catalog or a logical plan.
Source Files
¶
- agg_output_projection.go
- agg_over_exchange.go
- agg_rename_retarget.go
- agg_whole_input.go
- aggregate_shuffle.go
- carrier_assert.go
- carrier_schema.go
- colref_rewrite.go
- correlated_refusal.go
- decimal_arith_type.go
- derived_alias.go
- dimension_cascade.go
- distinct_refusal.go
- distribution.go
- dynamic_filter.go
- dynamic_filter_attach.go
- elide_copartitioned_exchange.go
- ensure_distribution.go
- exchange.go
- exchange_partial_agg.go
- exchange_subsume.go
- filter_carrier.go
- fuse_join_shuffle.go
- fuse_scan_aggregate_shuffle.go
- fuse_scan_shuffle.go
- fuse_stage_chains.go
- fused_agg_read_set.go
- group_key_carrier.go
- group_key_identity.go
- group_key_refusal.go
- group_key_resolution.go
- grouping_sets_refusal.go
- hidden_sort_key.go
- in_subquery_set.go
- join_carried_columns.go
- join_declared_schema.go
- join_input_projection.go
- join_key_types.go
- join_residual.go
- late_mat.go
- load_gate.go
- manifest_snapshot.go
- metadata_count.go
- metadata_minmax.go
- native_dag_rewrite.go
- output_declared_schema.go
- output_rename_resolve.go
- plan.go
- project_stage_insert.go
- real_list_refusal.go
- reserved_slots.go
- scalar_projection_refusal.go
- scan_declared_schema.go
- scan_delete_markers.go
- scan_estimate.go
- scan_filter_pushdown.go
- scan_output_prune.go
- semi_anti_build_filter.go
- set_op_arm_decls.go
- set_op_decimal.go
- set_op_key.go
- set_op_schema.go
- set_op_stages.go
- shared_cte_producer.go
- shared_subplan_dedup.go
- slot_collision.go
- sort_merge_join.go
- stage_stream_model.go
- subtree_naming.go
- table_func.go
- topn_late_mat.go
- util.go
- validate.go
- validate_literal.go
- window_alias_respell.go
- window_keys.go