physical

package
v0.15.0-aggregation-en... Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: AGPL-3.0 Imports: 39 Imported by: 0

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

View Source
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"

	// 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"
)
View Source
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

View Source
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, AND so they can be raised at runtime to disable the optimization while we hunt the SF100 Q05 0-rows bug whose triggering code path is somewhere in this optimization. The semi/anti threshold is left at 10M because we have no evidence of bugs there yet.

Init reads WADJET_REVERSE_BLOOM_INNER_THRESHOLD if set, so the bench can disable the inner-join path on SF100 without rebuilding the binary.

View Source
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.

View Source
var AttachOnArrivalConsumesPlanned atomic.Int64

AttachOnArrivalConsumesPlanned counts converted consume edges, process-wide. A/B observability, same family as DynamicFiltersPlanned.

View Source
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.

View Source
var DFAttachOnArrival atomic.Bool

DFAttachOnArrival gates applyAttachOnArrival. Kill switch WADJET_DF_ATTACH_ON_ARRIVAL=0 restores the barrier on every edge.

View Source
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).

View Source
var DimensionCascade atomic.Bool

DimensionCascade gates markDimensionCascade. Kill switch WADJET_DIMENSION_CASCADE=0.

View Source
var DimensionCascadesPlanned atomic.Int64

DimensionCascadesPlanned counts cascade annotations, process-wide.

View Source
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.

View Source
var ElidedCoPartitionedExchanges atomic.Int64

ElidedCoPartitionedExchanges counts identity exchanges removed at plan time (observability; mirrors coordinator.SkewSplitsPlanned).

View Source
var ExchangePartialAggMarked atomic.Int64

ExchangePartialAggMarked counts exchanges marked for sender-side partial aggregation at plan time (observability parity with ElidedCoPartitionedExchanges).

View Source
var ExchangeSubsume atomic.Bool

ExchangeSubsume gates dedupeSubsumedScanExchanges. Kill switch WADJET_EXCHANGE_SUBSUME=0. Exported atomic.Bool (ScalarAggSemijoin pattern) so tests can pin either arm.

View Source
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.

View Source
var MetadataCountsPlanned atomic.Int64

MetadataCountsPlanned counts metadata-answered COUNT(*) plans (observability-first, mirrors TopNLateMatPlanned).

View Source
var MetadataMinMaxPlanned atomic.Int64

MetadataMinMaxPlanned counts metadata-answered MIN/MAX plans (observability-first, mirrors MetadataCountsPlanned).

View Source
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.

View Source
var ScanFilterPushdowns atomic.Int64

ScanFilterPushdowns counts filters (conjuncts) pushed into scans.

View Source
var SemiAntiBuildFilter atomic.Bool

SemiAntiBuildFilter gates markSemiAntiBuildFilters. Kill switch WADJET_SEMIANTI_BUILD_FILTER=0. Exported atomic.Bool (ExchangeSubsume pattern) so tests can pin either arm.

View Source
var SemiAntiBuildFiltersPlanned atomic.Int64

SemiAntiBuildFiltersPlanned counts build-filter annotations the pass produced, process-wide. Mechanism marker for A/B runs (the DynamicFiltersPlanned convention).

View Source
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.

View Source
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.

View Source
var SharedSubplanDedup atomic.Bool

SharedSubplanDedup gates dedupeSharedSubplans. Kill switch WADJET_SHARED_SUBPLAN=0. Exported atomic.Bool (ScalarAggSemijoin pattern) so tests can pin either arm.

View Source
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.

View Source
var StageFusion atomic.Bool

StageFusion gates fuseStageChains. Kill switch WADJET_STAGE_FUSION=0. Exported atomic.Bool (AggOverExchange pattern) so tests can pin either arm.

View Source
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.

View Source
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

func AssertExchangeConsistency(stages []Stage) error

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 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

func CountJoinStages(stages []Stage) int

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 HashPartitionCount

func HashPartitionCount(workerCount int) int

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 ParseSemiAntiNE

func ParseSemiAntiNE(filter string) (probeCol, buildCol string, ok bool)

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 SemiAntiBuildStoreCols

func SemiAntiBuildStoreCols(rightKeys []string, joinFilter string) []string

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

func SetExchangePartialAggEnabled(on bool) bool

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 ValidateNativeDAGShape

func ValidateNativeDAGShape(stages []Stage) error

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.

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
}

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
)

func (AggregateShuffleRejectReason) String

String renders a reject reason as a short tag suitable for logs.

type ChainedJoinSpec

type ChainedJoinSpec struct {
	JoinType        string
	JoinLeftKeys    []string
	JoinRightKeys   []string
	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
	// 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

type ComputedCol struct {
	Name string
	Expr string
}

ComputedCol is one appended expression column on a shuffle payload.

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
	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
	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 FusedJoinSpec

type FusedJoinSpec struct {
	JoinType        string
	JoinLeftKeys    []string
	JoinRightKeys   []string
	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
}

FusedJoinSpec describes a broadcast join absorbed into a parent join stage.

type OutputRename

type OutputRename struct {
	From string
	To   string
	Expr plansql.Node
}

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
}

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)

	// 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.
	SharedTracker  *memory.Tracker
	SharedSpillMgr *memory.SpillManager
	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

func NewPlanner(cat *catalog.Catalog) *Planner

NewPlanner creates a new physical planner.

func (*Planner) AnnotateScanColumns

func (p *Planner) AnnotateScanColumns(ctx context.Context, node *logical.Node)

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) EstimatePlanScanBytes

func (p *Planner) EstimatePlanScanBytes(ctx context.Context, n *logical.Node) (int64, bool)

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

func (p *Planner) ExpandFederatedScans(stages []Stage) []Stage

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) Plan

func (p *Planner) Plan(ctx context.Context, node *logical.Node) (*PhysicalPlan, error)

Plan converts a logical plan to a physical plan for local execution.

func (*Planner) PlanDistributed

func (p *Planner) PlanDistributed(ctx context.Context, node *logical.Node) ([]Stage, error)

PlanDistributed generates a stage DAG for distributed execution. Returns stages with dependency ordering suitable for coordinator dispatch.

func (*Planner) ValidateColumns

func (p *Planner) ValidateColumns(ctx context.Context, info *plansql.SelectInfo) error

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
}

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 (inferProjectionType) — the worker cannot resolve it from the input schema because the output column doesn't exist there; zero means "resolve from source column" (bare refs).

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.

func EstimateCost

func EstimateCost(stages []Stage, node *logical.Node) QueryCost

EstimateCost computes the aggregate cost of a set of stages.

type RecordBatch

type RecordBatch = batch.RecordBatch

RecordBatch type alias for convenience

type RequiredDistribution

type RequiredDistribution struct {
	Kind  RequiredKind
	Keys  []string
	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:

  1. probeAlias = largest scan (matches CanProbeSplit's heuristic).
  2. candidate = largest non-probe scan above thresholdBytes.
  3. 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.
  4. 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 SortKeySpec

type SortKeySpec struct {
	Column    string
	Desc      bool
	NullsLast bool
}

SortKeySpec defines a sort key in a stage.

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
	// 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

	// Aggregate metadata
	GroupByCols []string
	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    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
	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")
	// 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

	// 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

	// Window metadata
	WindowCols []WindowColSpec

	// 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. 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

	// 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

	// 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
}

Stage represents a unit of distributed work with metadata for task creation.

func EnsureDistribution

func EnsureDistribution(stages []Stage, workerCount int) ([]Stage, error)

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

func LargeBuildScans(stages []Stage, probeAlias string, thresholdBytes int64) []Stage

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:

  1. 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).
  2. 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 WindowColSpec

type WindowColSpec struct {
	Func        string
	InputCol    string
	OutputCol   string
	PartitionBy []string
	OrderBy     []SortKeySpec
	Frame       *logical.WindowFrameSpec
}

WindowColSpec defines a window function column in a stage.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL