Documentation
¶
Overview ¶
Package logical provides logical query plan representation and optimization.
Index ¶
- Constants
- Variables
- func AggScopePreservingWrapper(t NodeType) bool
- func ExpandStarProjections(n *Node)
- func HasHiddenProjection(projs []Projection) bool
- func HasRemainingSubqueries(n *Node) bool
- func HasStarProjection(n *Node) bool
- func IsHiddenSortColumn(name string) bool
- func NodeColumnRefs(n *Node) []string
- func ResolveFilterThroughProjects(pred Predicate, child *Node) (ast plansql.Node, aliases []string, ok bool)
- func SetSemiPushdownEnabled(on bool) bool
- func SubstitutePreComputedAggregates(root *Node, sigs []PreComputedAggregate) (map[string]bool, error)
- type AggExpr
- type ColumnPolicy
- type DecimalMeta
- type DecorrelatedKey
- type HistogramStats
- type InnerKeyRef
- type MergeInfo
- type Node
- func AggregateBelowProject(n *Node) *Node
- func AggregateOverGroupRows(n *Node) *Node
- func BuildFromSelect(info *plansql.SelectInfo) (*Node, error)
- func BuildFromSelectWithCTEs(info *plansql.SelectInfo, ctes []plansql.CTEDef) (*Node, error)
- func InjectColumnPolicies(plan *Node, tableName string, policies []ColumnPolicy) *Node
- func InjectRowFilter(plan *Node, tableName, filterSQL string) *Node
- func NewAggregate(child *Node, groupBy []string, aggs []AggExpr) *Node
- func NewDistinct(child *Node) *Node
- func NewExcept(left, right *Node, all bool) *Node
- func NewFilter(child *Node, predicates []Predicate) *Node
- func NewIntersect(left, right *Node, all bool) *Node
- func NewJoin(left, right *Node, joinType, condition string) *Node
- func NewLimit(child *Node, limit, offset int) *Node
- func NewProject(child *Node, projections []Projection) *Node
- func NewScan(table, alias string) *Node
- func NewSort(child *Node, orderBy []OrderExpr) *Node
- func NewUnion(left, right *Node, all bool) *Node
- func NewWindow(child *Node, exprs []WindowExpr) *Node
- func Optimize(plan *Node, annotators ...func(*Node)) *Node
- func StripTopSortLimit(plan *Node) *Node
- type NodeType
- type OrderExpr
- type PreComputedAggregate
- type Predicate
- type Projection
- type RelStats
- type ScanColumnStats
- type WindowBound
- type WindowExpr
- type WindowFrameSpec
Constants ¶
const NoLimit = -1
NoLimit is the LimitVal of a Limit node that only skips rows: `OFFSET n` with no LIMIT, which is what a paginating client sends for the last page. Every row past the offset passes. Consumers must test `LimitVal != NoLimit` before using it as a row bound — never `LimitVal > 0`, which mistakes a real `LIMIT 0` for "unbounded" and was the root cause of #481 (`ORDER BY ... LIMIT 0` returning every row). An unbounded node has no bound to push down; a LimitVal of exactly 0 is a real, meaningful bound.
const RowCountOnlyColumn = "__rowcount_only__"
RowCountOnlyColumn is the sentinel required-column emitted for scans that only need a row count (bare COUNT(*) / literal-only projections). The physical scan drops it when any real column is required and otherwise projects the narrowest schema column instead of all columns. The "__" prefix keeps it flowing through sanitizeScanNeeds and makes the distributed worker's all-or-nothing projection guard fall back to full width (safe) if it ever reaches that path.
Variables ¶
var BushyJoinReorder atomic.Bool
BushyJoinReorder enables bushy subset-partition transitions in the DP join reorder (docs/design/bushy-join-cbo.md §3.2). Process-wide, set once at startup from --bushy-join-reorder / wadjet.Config; default off. Read at plan time, so tests may toggle it around a planning call.
var BushyJoinsPlanned atomic.Int64
BushyJoinsPlanned counts queries whose FINAL chosen join order contains at least one bushy join (a join of two composite intermediates). Mechanism marker for A/B runs: a nonzero count proves the enumeration actually changed a plan; dormancy tests assert it stays zero with the flag off.
var ScalarAggSemijoin atomic.Bool
ScalarAggSemijoin gates reduceDecorrelatedScalarAggs. Kill switch: WADJET_SCALAR_AGG_SEMIJOIN=0 (mirrors WADJET_SCALAR_DEFER / WADJET_EXCHANGE_ELIDE). Default on. Exported as an atomic (the BushyJoinReorder pattern) so tests that exercise the UNreduced decorrelated shape — aggregate-shuffle and dynamic-filter fixtures built on Q17/Q20 — can flip it with a defer-restore.
Functions ¶
func AggScopePreservingWrapper ¶ added in v0.18.9
AggScopePreservingWrapper reports whether a node standing between an aggregate and a consumer above it leaves the aggregate's OWN output columns visible, under their own names.
This is THE list. ADR-0026 §4 states it once and names the walks that read it, because every one of them is asking a consumer's version of the same question and every one of them had grown its own answer:
- physical.aggregateUnderOutput — the gather's OutputRenames;
- physical.findAggregateAncestor — the single-process projection;
- physical.groupKeysPublishedBelow — whether an aggregate DIRECTLY BELOW already publishes a key, so the one above must not re-materialize it;
- AggregateOverGroupRows (this package) — whether a Project's INPUT rows are one per GROUP, which decides whether a predicate above it may be substituted below (#774).
It lives here rather than in `physical` because the fourth reader is in this package and `physical` imports `logical`, not the other way round. The physical package's `aggScopePreservingWrapper` is a thin delegation, and `TestAggScopePreservingWrapperIsReadByEveryWalk` drives all four.
A WINDOW is on the list: exec.Window APPENDS its output to its input and renames nothing, so every column the aggregate published is still there under its own name. A Filter (HAVING), a Sort and a LIMIT are on it for the same reason — they drop or reorder ROWS and rename no column.
An Aggregate is not: it replaces its child's schema with its own keys and outputs, which is why it is the walks' TARGET rather than a wrapper. Neither is a Project: what a Project does to the schema is the caller's own question, so each walk keeps its own rule for it. NodeDistinct is deliberately absent — rewriteDistinctAsGroupBy lowers a DISTINCT above a grouped query into a second Aggregate, so the node does not stand there, and admitting a kind no fixture produces would put an untested path on the default route (correctness protocol, method 10).
func ExpandStarProjections ¶
func ExpandStarProjections(n *Node)
ExpandStarProjections rewrites every `*` / `alias.*` select item into one projection per column of the star's source, in schema order.
A star that shares its SELECT list with another item — `SELECT t.*, ctid` is how DataGrip opens a table — reaches the planner as a Projection carrying the literal expression "*" and no column reference at all. That is why it must be expanded HERE, before computeRequiredColumns: the pruner collects the columns each Project names, the star named none, so the scan below was narrowed to the SIBLING item's columns and every column the star contributed read back NULL. The single-process path returned those NULLs (#315); the distributed path only escaped because an unknown name trips the worker's all-or-nothing parquet projection guard, which falls back to full width.
A star's source columns come from the scan's catalog-annotated schema (ScanColumns, populated by physical.AnnotateScanColumns), so this resolves only when a single base-table scan sits below the projection — the shape clients send. A star over a join or a derived table is left alone: its column set is not knowable here, and guessing it would silently change which columns a query returns.
func HasHiddenProjection ¶
func HasHiddenProjection(projs []Projection) bool
HasHiddenProjection reports whether any of projs was materialized for a sort key rather than selected by the user.
func HasRemainingSubqueries ¶
HasRemainingSubqueries walks an optimized logical plan and returns true if any filter predicate still contains un-decorrelated subquery references (EXISTS, NOT EXISTS, IN (SELECT), scalar subqueries). After successful decorrelation these become semi/anti join nodes and the predicates are removed. If this returns false, the scan-node structure is deterministic and safe for distributed execution.
func HasStarProjection ¶
HasStarProjection reports whether node is a Project that still carries an unexpanded `*` or `alias.*` select item.
func IsHiddenSortColumn ¶
IsHiddenSortColumn reports whether name is a materialized ORDER BY term.
func NodeColumnRefs ¶
NodeColumnRefs returns the column names referenced by a node's own expressions (filter predicates, sort keys, projections, ...) — the same collection column pruning uses, exported for the physical planner's top-N late-materialization rewrite.
func ResolveFilterThroughProjects ¶ added in v0.18.5
func ResolveFilterThroughProjects(pred Predicate, child *Node) (ast plansql.Node, aliases []string, ok bool)
ResolveFilterThroughProjects re-spells a predicate that sits ABOVE one or more Projects into the names their INPUT carries. It is the stage DAG's half of the question the Filter-Project swap answers for the single-process pipeline, and it exists because the two paths lower a Project differently.
pushdownPredicates SWAPS a Filter below a Project and substitutes each reference to a renamed or computed output with its defining expression. It DECLINES that swap for a Project tagged with a CTEName — a materialization fence, because the single-process planner replays ONE cached result for every reference of a CTE and a predicate pushed inside it would apply to all of them — and it never applies at all when the Filter's child is a JOIN, whichever kind of subquery the rename came from. Declining is right in both cases: the predicate does not move.
What is wrong on the DAG is the SPELLING. An ordinary Project emits NO STAGE there (docs/internals/native-dag-execution.md §Derived-table aliases), so the predicate walkStages attaches to the producing stage is evaluated against a schema carrying SOURCE column names. A reference to the alias resolves to nothing, `expr.ColRef.Eval` answers nil, the predicate is UNKNOWN on every row, and a WHERE that admits only TRUE drops all of them — silently, for every type (#653). Every other consumer of a derived name on the DAG has a resolver for exactly this reason; the filter had none.
So the predicate stays where it is and only its spelling changes, which is sound whatever the Project is tagged with: substitution evaluates the exact defining expression the Project would have produced, NULLs included. The walk descends a join ONE ARM AT A TIME with that arm's scope names, so a reference qualified to the other arm is left alone (projRefs), and it stops at the first Project whose output the substitution cannot express — an aggregate output, a volatile function — because a stage that emits such a column emits it under the alias, which is the name the predicate already carries.
It also stops at a Sort or a LIMIT. Those DO emit stages, carrying the names above them, so a predicate re-spelled past one would name a column the stage below the Project has and the stage the filter lands on does not.
AMBIGUITY is not this pass's to report. A bare name two relations in scope both carry is rejected by physical.validate before any of this runs ("column reference %q is ambiguous", 42702, on both paths), so a decline here can only be a shape the resolver leaves alone — never a name the query failed to disambiguate.
Returns (nil, false) when nothing changed; the caller then ships the predicate exactly as it did before. aliases lists the OUTPUT names the rewrite substituted away, lowercased — the spellings the predicate carried before this pass touched it. The DAG needs them to decide whether the producing fragment carries the alias or the source column (physical.resolveFilterAliasSpelling, #656).
func SetSemiPushdownEnabled ¶
SetSemiPushdownEnabled toggles the rewrite (tests that pin plan shapes downstream of the legacy join order run with it off). Returns the previous value so callers can restore it.
func SubstitutePreComputedAggregates ¶
func SubstitutePreComputedAggregates(root *Node, sigs []PreComputedAggregate) (map[string]bool, error)
SubstitutePreComputedAggregates walks the plan tree and replaces each Aggregate node whose shape matches one of sigs with a synthetic Scan. Returns the set of substitutions actually performed (alias → signature), so the caller can verify every pre-computed input was consumed. A signature that never matched is logged by the caller; this pass does not error on unmatched signatures (the worker may have re-planned a shape that no longer contains the aggregate, in which case falling back to the in-pipeline execution is correct).
Matching is conservative: a signature must match on GroupByCols (order and content) AND on the full set of AggExpr output names. Any other cases fall through untouched.
Types ¶
type AggExpr ¶
type AggExpr struct {
Func string // sum, count, min, max, avg
InputCol string
OutputCol string
Distinct bool // COUNT(DISTINCT col)
InputExpr plansql.Node // AST for aggregate argument (nil for simple column refs)
// InputCol2, Separator and Percentile hold the arguments after the
// first, for the functions that take more than one: the second column
// of CORR/COVAR_SAMP/COVAR_POP and the ordering column of
// MIN_BY/MAX_BY, STRING_AGG's separator literal, and
// PERCENTILE_CONT/PERCENTILE_DISC's fraction. See parseAggExtraArgs.
//
// InputCol stays a single column name throughout: column pruning and
// requiredColumns read it as one, and InputCol2 is registered beside
// it rather than packed into the same string.
InputCol2 string
Separator string
Percentile float64
}
AggExpr is an aggregation expression.
type ColumnPolicy ¶
type ColumnPolicy struct {
Column string
Denied bool // true = column excluded from results
MaskExpr string // non-empty = column replaced with this value (e.g., "'***'", "0")
}
ColumnPolicy describes a column-level security policy for plan-level enforcement.
type DecimalMeta ¶ added in v0.18.1
DecimalMeta carries a DECIMAL column's declared precision and scale — the two facts a bare parquet.TypeID cannot express. See Node.ScanColDecimal.
type DecorrelatedKey ¶ added in v0.18.3
type DecorrelatedKey struct {
}
DecorrelatedKey is one conjunct of a decorrelated semi/anti join: a probe-side term the rewrite already spelled correctly (it names the OUTER query's columns, which no inner reordering can move), an operator, and a build-side reference whose spelling only reorderJoins can settle.
type HistogramStats ¶
type HistogramStats interface {
SelectivityLE(v any) float64
SelectivityLT(v any) float64
SelectivityRange(lo, hi any) float64
SelectivityEQ(v any) float64
}
HistogramStats is the interface a per-column histogram must implement to participate in selectivity estimation. catalog.Histogram satisfies it. Kept as an interface so the logical package stays free of a catalog dependency.
type InnerKeyRef ¶ added in v0.18.3
type InnerKeyRef struct {
Qualifier string // relation alias, or table name when unaliased; "" = unqualified
Column string // source column, unqualified
Text string // the spelling the rewrite wrote, and the repair's fallback
}
InnerKeyRef is one reference into a decorrelated subquery's own relations, recorded the way the subquery spelled it.
Text is what the rewrite wrote into the plan when it built the node. It is what the repair keeps when it cannot resolve the reference — an un-annotated Scan (no ScanColumns) is the reachable case — so a plan that never reaches the repair reads exactly as it did before this machinery existed.
type MergeInfo ¶
type MergeInfo struct {
GroupBy []string
AggExprs []AggExpr
OrderBy []OrderExpr
// Limit is the top-level LIMIT value; meaningful only when HasLimit is
// true. HasLimit is false both when the statement has no LIMIT at all
// and when it has only an OFFSET (NoLimit) — a companion bool rather
// than folding NoLimit into Limit itself, because the many test
// literals that construct a MergeInfo{} directly (never touching
// Limit) must keep meaning "unbounded" by leaving both fields at their
// zero value. Before HasLimit existed, `Limit > 0` doubled as that
// same test, so a probe-split merge for `... LIMIT 0` silently kept
// every row instead of zero (#481) — test HasLimit, never `Limit > 0`.
Limit int
HasLimit bool
// Offset is the top-level OFFSET. Rows to keep are [Offset, Offset+Limit)
// — a merge that truncates to Limit before skipping Offset returns the
// first page for every page (#337).
Offset int
HasAggregate bool
HasDistinct bool // DISTINCT requires deduplication across partials
}
MergeInfo describes how to merge probe-split partial results.
func ExtractMergeInfo ¶
ExtractMergeInfo extracts the top-level aggregate and sort/limit information needed to merge probe-split partial results. Returns nil if the plan doesn't have a top-level aggregate (probe-split merge not needed).
func (*MergeInfo) KeepRows ¶
KeepRows is the number of rows a merge step must hold on to before the offset is applied: everything up to Offset+Limit. Returns NoLimit (-1) when unbounded — never 0, which is itself a real, meaningful "keep nothing" answer for a top-level `LIMIT 0` (#481). Callers must test `KeepRows() >= 0`, never `KeepRows() > 0`.
type Node ¶
type Node struct {
Type NodeType
Children []*Node
// Scan
TableName string
TableAlias string
// DerivedAliases are the DERIVED TABLE aliases whose scope this scan sits
// inside, outermost last — `x` for the supplier scan in `(SELECT
// s_suppkey AS k FROM supplier s1) x`. They are recorded ALONGSIDE
// TableAlias rather than in it because the two answer different
// questions: TableAlias is which relation the scan IS, and the derived
// alias is which derived table's scope it is IN.
//
// Collapsing them cost a wrong answer. setSubtreeAlias used to overwrite
// TableAlias, so `(SELECT n1.n_name AS a, n2.n_name AS b FROM nation n1
// JOIN nation n2 ON …) u` planned as two scans BOTH aliased `u`, the join
// could no longer tell its two sides apart by name, and `a` and `b` came
// back as the same column — 25 groups where PostgreSQL 17 answers 5
// (#489). The scope question still has to be answerable, because that is
// what lets `u.a` drop its qualifier inside this subtree and nowhere else
// (physical.derivedScopeBareName), so it is recorded rather than dropped.
DerivedAliases []string
ScanColumns []string // column names available from this scan (populated by physical planner)
RequiredColumns []string // columns actually needed from this scan (set by optimizer column pruning)
PartitionFilter map[string]string // extracted partition key filters (year, month, day, hour)
ScanPredicates []Predicate // pushed-down filter predicates for row group pruning
ScanRowEstimate int64 // estimated row count from manifest (0 = unknown)
ScanColStats map[string]ScanColumnStats // aggregated column stats from catalog (nil = unavailable)
// ScanColTypes maps this scan's lower-cased column names to their
// catalog types (populated by physical.AnnotateScanColumns alongside
// ScanColumns). It is what lets the planner declare a MIN/MAX output
// type, which follows the input column rather than the function.
ScanColTypes map[string]parquet.TypeID
// ScanColDecimal maps this scan's lower-cased column names to their
// DECIMAL precision/scale — entries exist only for TypeDecimal columns.
// Populated by physical.AnnotateScanColumns alongside ScanColTypes. It is
// what lets a zero-row DECIMAL result declare the same PostgreSQL typmod
// a non-empty one does: ScanColTypes alone carries the bare TypeID, which
// left every declared-schema DECIMAL column at Precision=0 (typmod -1,
// "unconstrained") even when the underlying column had a real (p,s)
// (#458).
ScanColDecimal map[string]DecimalMeta
// ScanColFields maps this scan's lower-cased ROW column names to their
// declared FIELDS. It is what types a field PATH (`rw.c`): `c` is not a
// column of anything, so ScanColTypes cannot carry it and every lookup
// keyed by column name misses. Without it a field path is declared
// STRING — `SELECT rw.n` over an INT64 field returned string("9"),
// `ORDER BY rw.c` sorted a CIDR field by its stored text, and the wire
// reported OID 25 for both (#568). Populated by
// physical.AnnotateScanColumns alongside ScanColTypes.
ScanColFields map[string][]parquet.Column
FilterOnlyColumns []string // columns needed ONLY by the filter directly above this scan (candidates for scan-level filter evaluation without materialization)
ShapeOnlyColumns []string // byte-array columns whose EVERY use in the plan reads shape, not contents (LENGTH/IS NULL/= ”/COUNT) — the scan decodes them as lengths, see shape_only_columns.go
SampleMethod string // TABLESAMPLE method: BERNOULLI, SYSTEM
SamplePercent float64 // percentage for TABLESAMPLE (0-100)
// Table Function (e.g., read_json, read_csv, unnest)
IsTableFunc bool // true if this scan reads from a table function
FuncName string // function name (e.g., "read_json")
FuncArgs []string // positional arguments (e.g., URL/path)
FuncNamedArgs map[string]string // named arguments (e.g., delimiter="|")
WithOrdinality bool // UNNEST(...) WITH ORDINALITY
FuncColAliases []string // AS alias(col1, col2, ...)
// Filter
Predicates []Predicate
// Project
Projections []Projection
// SecurityBarrier marks a projection injected by ABAC column-policy
// enforcement (InjectColumnPolicies): masked columns replaced with
// literal expressions, denied columns absent. The physical planner
// must APPLY it at the scan (distributed walkStages treats ordinary
// Projects as passthrough — a dropped barrier would leak raw values).
SecurityBarrier bool
// Aggregate
// PreservesAggOutputs marks a synthetic Project inserted by a rewrite
// directly above an Aggregate that passes every aggregate output
// through under its original name (possibly adding finalizations,
// e.g. the two-level AVG division). The physical builder's
// aggregate-ancestor resolution walks through such projections so
// SELECT-list aggregate references still resolve by output name.
PreservesAggOutputs bool
// ScanStrictIntCols marks scan columns whose vectors are plain
// Int64/Int32 at runtime — exactly the set expr.BinOpNumeric resolves
// to integer arithmetic. Planner-side int typing must stay a subset
// of the runtime rule (a declared-int column over a float-mode expr
// would read as NULL through the typed getter), so this is
// deliberately narrower than ScanIntCols' int-class set.
ScanStrictIntCols map[string]bool
// ScanIntCols marks scan columns whose types land on the typed
// integer aggregation paths (set by physical.AnnotateScanColumns;
// consumed by the two-level distinct rewrite's cost gate).
ScanIntCols map[string]bool
GroupBy []string
GroupByExprs []plansql.Node // AST for GROUP BY expressions (may be nil)
// InnerGroupRefs parallels GroupBy on an Aggregate the IN decorrelation
// built inside a subquery's plan: what each group term MEANS, for
// repairDecorrelatedSpelling to spell once the inner join order is
// final. An aggregate's output column IS its group key's text, so a key
// named from write order moves the #526 mismatch one node up rather
// than removing it. Nil elsewhere, and nil again after the repair.
InnerGroupRefs []InnerKeyRef
AggExprs []AggExpr
GroupingSetNulls []string // columns that should be NULL in this grouping set (legacy, per-node)
GroupingSets [][]string // single-pass grouping sets: each entry lists the columns in that set
// Sort
OrderBy []OrderExpr
// Limit
LimitVal int
OffsetVal int
// Join
JoinType string // inner, left, right, full, cross, semi, anti
JoinCond string
JoinFilter string // non-equality join conditions for semi/anti join
LeftKeys []string
RightKeys []string
NeededColumns []string // columns the parent needs from this join's output (set by optimizer)
// InnerKeys and InnerFilterKeys record what a semi/anti join produced by
// the IN / EXISTS decorrelations MEANS by its build-side references —
// the relation qualifier and source column the subquery wrote — for
// repairDecorrelatedSpelling to turn back into JoinCond / JoinFilter
// text once reorderJoins has settled which relation's columns the inner
// join emits bare. Nil on every other join, and nil again after the
// repair has run. See inner_key_spelling.go (#526, #527).
InnerKeys []DecorrelatedKey
InnerFilterKeys []DecorrelatedKey
// NullAwareAnti marks an anti join the NOT IN rewrite produced, which
// must answer NOT IN's three-valued rule rather than the two-valued
// question an anti join asks on its own: a NULL probe key, or a NULL
// anywhere in the subquery's result, is UNKNOWN and not TRUE (#507).
// Set only for an UNCORRELATED NOT IN — with a correlation the "the list
// held a NULL" fact is per correlation group, which a single flag on the
// operator cannot express, so that shape is left alone. NOT EXISTS is a
// different predicate with its own (already correct) semantics and never
// carries this. Consumed by exec.HashJoin.NullAwareAnti.
NullAwareAnti bool
// Window
WindowExprs []WindowExpr
// Union
UnionAll bool // true = UNION ALL, false = UNION (dedup)
// CTEs — stored on the root node so the physical planner can resolve
// CTE references inside scalar subqueries (e.g., Q15's HAVING/WHERE
// subquery that references a CTE defined in the outer WITH clause).
CTEs []plansql.CTEDef
// CTEName — set on the root of a CTE sub-plan so the physical planner
// can detect and materialize multi-referenced CTEs. It is also the
// SCOPE NAME the enclosing query qualifies this subtree's output columns
// by — the CTE's answer to a derived table's alias, which
// physical.subtreeNamesRelation reads so `c.gk` resolves to the SELECT
// item `gk` names (#653).
CTEName string
// CTERefAlias is the name ONE reference gives that scope — `x` in
// `FROM c AS x`, which PostgreSQL makes the only spelling the enclosing
// query may use. It sits beside CTEName rather than replacing it because
// the CTE cache is keyed on the definition's name and every reference
// shares it.
CTERefAlias string
// DerivedAlias is a DERIVED table's own name, on the root of its
// sub-plan: `q` in `FROM (SELECT …) q`. It is the derived spelling of
// CTEName, and it is recorded on the SUBTREE ROOT for the same reason —
// setSubtreeAlias stamps the alias onto the SCANS below, so a subtree
// with more than one scan (an arm that is itself a JOIN) and a subtree
// with a derived table INSIDE it (which stamps its own alias first, and
// setSubtreeAlias then declines to overwrite) both leave the scans
// answering to a name the enclosing query never wrote. `joinArmAlias`
// reads it so a join qualifies an arm's duplicate columns by the name
// the QUERY calls that arm (#751, #773).
DerivedAlias string
// decorrelateScalarSubqueries (children[1] is the grouped aggregate
// materializing the subquery result). reduceDecorrelatedScalarAggs
// uses it after predicate pushdown to semijoin-reduce the aggregate's
// input by the outer plan's key-source branch.
ScalarDecorrelated bool
// BuildSideDedup marks a Distinct the PLANNER inserted, not one the
// user wrote. Two passes create them: dedupSemiAntiBuildSide bounds a
// semi/anti join's build hashtable by NDV, and scalar_agg_semijoin
// builds a decorrelated semijoin's key source. Neither carries
// user-visible semantics — a semi/anti join's result does not depend
// on whether its build side has duplicates — and the physical planner
// has dedicated handling for the shape (estimateDistinctKeyBytes sizes
// Distinct(Project) subtrees by key count; the distinct-pair semi/anti
// build fast path matches on it).
//
// rewriteDistinctAsGroupBy therefore leaves a marked Distinct alone and
// rewrites every UNMARKED one — those are user SELECT DISTINCTs, and
// each is an answer the engine has to actually compute (#466).
BuildSideDedup bool
}
Node is a node in the logical query plan tree.
func AggregateBelowProject ¶ added in v0.18.5
AggregateBelowProject returns the Aggregate whose OUTPUT rows this Project reads AND whose stage it sits directly on, or nil when it reads something else.
A HAVING is a Filter between the two, and it changes nothing about the rows: it drops whole groups, it does not restore the aggregate's input columns. Stopping at it is what made a CTE with both a HAVING and an outer WHERE on a computed group key answer zero rows (#656 shape f with a HAVING), so the walk descends through it.
It descends through a HAVING and NOTHING ELSE, which is deliberate and is why it is not AggScopePreservingWrapper's fourth reader. Both callers — physical.aggregateProjectionTarget and physical.aggregateGroupKeyName — go on to map this Project's SELECT list onto the aggregate's own STAGE, and a Sort, a LIMIT or a WINDOW between the two emits a stage of ITS own that the projection would then be carried past. The question "are these rows one per group", which has no such constraint, is AggregateOverGroupRows'.
func AggregateOverGroupRows ¶ added in v0.18.9
AggregateOverGroupRows returns the Aggregate whose GROUP rows this Project's input carries, or nil when the input is something else.
It is NOT AggregateBelowProject, and the difference is the fourth reader #774 was hiding in. AggregateBelowProject answers "which aggregate STAGE does this Project sit directly on top of", for two consumers that then map the SELECT list onto that stage — and a Sort or a WINDOW between the two emits a stage of its own, so stopping at one is right there.
The question HERE is only about the ROWS: below any of the wrappers above, there is still exactly one row per group and the aggregate's input columns are gone. `SELECT g + 1 AS k, ROW_NUMBER() OVER (ORDER BY g + 1) AS rn FROM t GROUP BY g + 1` wrapped in a derived table and filtered `WHERE k > 3` substituted `k` away to `(g + 1)` and pushed it below the Project, where it met the WINDOW's output — which carries the key under its published NAME and no `g` at all. The predicate was UNKNOWN on every row and a filter admits only TRUE: zero rows on all four arms where PostgreSQL answers four (#774).
func BuildFromSelect ¶
func BuildFromSelect(info *plansql.SelectInfo) (*Node, error)
BuildFromSelect constructs a logical plan from a parsed SELECT query.
func BuildFromSelectWithCTEs ¶
BuildFromSelectWithCTEs constructs a logical plan, resolving CTE references to inline sub-plans instead of table scans.
func InjectColumnPolicies ¶
func InjectColumnPolicies(plan *Node, tableName string, policies []ColumnPolicy) *Node
InjectColumnPolicies walks the logical plan and inserts a security projection above Scan nodes for the given table. Denied columns are removed and masked columns are replaced with literal expressions. This ensures restricted data never enters the execution pipeline.
func InjectRowFilter ¶
InjectRowFilter walks the logical plan tree and wraps Scan nodes for the given table with an additional Filter node containing the row filter predicate. This is used by row-level security policies to restrict which rows a role can see.
func NewAggregate ¶
NewAggregate creates an aggregate node.
func NewExcept ¶
NewExcept creates an except node. Returns rows from left that are not in right. If all is true (EXCEPT ALL), preserves duplicates; otherwise deduplicates.
func NewIntersect ¶
NewIntersect creates an intersect node. Returns only rows present in both sides. If all is true (INTERSECT ALL), preserves duplicates; otherwise deduplicates.
func NewProject ¶
func NewProject(child *Node, projections []Projection) *Node
NewProject creates a projection node.
func NewUnion ¶
NewUnion creates a union node. If all is true, it represents UNION ALL (no deduplication); otherwise it represents UNION (with deduplication).
func NewWindow ¶
func NewWindow(child *Node, exprs []WindowExpr) *Node
NewWindow creates a window node.
func Optimize ¶
Optimize applies logical optimizations to the plan tree.
An optional ScanAnnotator function may be provided to populate scan metadata (ScanColumns) on newly created scan nodes after IN-to-SemiJoin conversion. This enables subsequent scalar subquery decorrelation to resolve unqualified column references. Without an annotator, scalar decorrelation may fail for subqueries that use unqualified outer column references.
func StripTopSortLimit ¶
StripTopSortLimit removes the outermost Sort and Limit nodes from a logical plan. Used by probe-split pipeline: each worker produces partial aggregates without ordering or truncation; the coordinator merges and applies final sort + limit.
func (*Node) OuterTableID ¶ added in v0.18.3
OuterTableID is the single name an enclosing scope calls this scan by: the OUTERMOST derived table it sits inside when there is one, else its own alias, else its table name.
The outermost wins because that is the only one visible from outside: in `(SELECT … FROM (… FROM nation n1) x) y` the enclosing query can write `y.` and nothing else. Where a scan is in no derived table this is exactly its alias, which is what every caller had before Node.DerivedAliases existed.
func (*Node) PrettyPrint ¶
PrettyPrint returns a formatted string representation of the plan tree.
func (*Node) ScopeNames ¶ added in v0.18.3
ScopeNames lists every name an ENCLOSING scope may use to qualify a column of this scan: its table name, its own alias, and every derived table it sits inside. Empty for a node that is not a scan.
This is the question "could `x.` in a predicate mean something in here", which is what the correlated-subquery collectors ask, and it is NOT the same question as "which relation is this scan" — a scan inside `(SELECT … FROM nation n1 …) u` answers to n1 AND to u, for different purposes. Collapsing the two is what #489 fixed for the join arms and what regressed here: once TableAlias stopped being overwritten with the derived alias, `u` was no longer a name any collector knew, `WHERE EXISTS (… WHERE t.k = u.did)` was no longer recognized as CORRELATED, and the subquery was left per-row — silently 0 rows on the single-process pipeline and loud on the DAG.
type OrderExpr ¶
type OrderExpr struct {
Column string
Desc bool
NullsFirst *bool // nil = default, true = NULLS FIRST, false = NULLS LAST
}
OrderExpr is a sort expression.
type PreComputedAggregate ¶
type PreComputedAggregate struct {
InputTable string
GroupByCols []string
AggOutputCols []string // OutputCol names from each AggSpec, in order
SyntheticAlias string // unique scan alias to substitute
}
PreComputedAggregate is a signature describing a derived aggregate whose result is already materialized elsewhere. When a logical-plan Aggregate node matches a signature, SubstitutePreComputedAggregates replaces it with a Scan node bearing SyntheticAlias; the caller (worker) is expected to wire SyntheticAlias → CacheFiles into the physical planner's StreamingSources map so the scan reads the cached rows directly.
Phase 1 matches only aggregates that are GROUP BY GroupByCols over a single Scan of InputTable (no filters on the scan, no nested joins). More general shapes are rejected and left in-plan.
type Predicate ¶
type Predicate struct {
Column string
Op string // =, !=, <, <=, >, >=, is_null, is_not_null, in, between
Value any
// ValueText is a numeric Value's exact source text. Value is boxed for
// arithmetic and a float64 cannot hold a DECIMAL past ~15-16 significant
// digits, so the text is what the scan's prune and the row-at-a-time
// filter convert at the column's own scale (#452). Empty when Value did
// not come from a numeric literal.
ValueText string
Raw string // raw SQL expression
ASTExpr plansql.Node // compiled AST expression node
// PruneOnly marks predicates attached solely for storage-level pruning
// (row-group stats / dictionary probes). The cardinality estimator
// ignores them: attaching AST-decomposed conjuncts must not shift
// distributed plan choices (Q08's orders join flipped shuffle →
// broadcast off a 0.33^n selectivity guess the moment they appeared).
// Estimate-visible attachment is a separate, SF100-validated change.
PruneOnly bool
}
Predicate is a filter condition.
func SplitConjunctsForPushdown ¶
SplitConjunctsForPushdown decomposes an AST filter into top-level AND-conjuncts and partitions them: structured `column <op> literal` comparisons (as Predicates) and everything else (residual ASTs). The physical planner uses this to push eligible conjuncts into the scan and compile only the residue as the exec filter.
type Projection ¶
type Projection struct {
Column string // column reference
Alias string // output name
Expr string // raw expression
IsAgg bool
ASTExpr plansql.Node // compiled AST expression node (nil for aggregates)
// SlotSource names the planner's HIDDEN SLOT this projection reads, and
// is empty for every projection that reads a real column. It exists
// because the two are otherwise indistinguishable: a query over a table
// that already stores a `__win_0` column has a projection reading THAT and
// a projection reading the window's slot of the same name, and the pass
// that renumbers the slot past the stored column (physical.
// renameCollidingSlots) must move exactly one of them.
SlotSource string
// Hidden marks a projection the planner added for its own use rather
// than one the user selected: the materialized value of an ORDER BY term
// the SELECT list does not carry (#320). It computes and sorts like any
// other projection, and is dropped before the rows reach the client —
// extractOutputRenames leaves it out of the DAG's output schema, and
// hiddenSortTrimOp drops it on the single-process pipeline.
Hidden bool
}
Projection is a column expression in a SELECT.
func VisibleProjections ¶
func VisibleProjections(projs []Projection) []Projection
VisibleProjections returns projs without the columns the planner materialized for its own use. Returns projs itself when there are none, so the common plan costs no allocation.
type RelStats ¶
type RelStats struct {
Rows float64
ColNDV map[string]float64 // lowercase unqualified column name → estimated NDV
// ColHist carries opaque histogram pointers (*catalog.Histogram) for
// columns that have one in the catalog. Used by estimatePredSelectivity
// to compute range/equality selectivity from real data distributions.
// Stored as any to keep the logical package free of catalog imports.
ColHist map[string]any
}
RelStats holds estimated statistics for a plan subtree.
func RelStatsOf ¶
RelStatsOf returns CBO-derived statistics for the given subtree. Exported so the physical planner can consult cardinality-driven estimates when making structural decisions (broadcast vs shuffle, dynamic-filter eligibility, etc.) without re-implementing the recursion.
type ScanColumnStats ¶
type ScanColumnStats struct {
MinValue any
MaxValue any
NullCount int64
TotalRows int64
// NDV is the catalog's merged HyperLogLog estimate of distinct values
// across all files of this column. Zero means HLL wasn't collected
// (legacy files / pre-ANALYZE state); planner falls back to the
// min/max-range heuristic. When >0 it's preferred — orders of
// magnitude more accurate for sparse-int / string columns where the
// range overstates true cardinality.
NDV int64
// Histogram is the catalog's equi-depth histogram for this column,
// merged across files from reservoir samples. Nil when not collected.
// Used by estimatePredSelectivity to compute range/equality
// selectivity from real value distributions instead of hardcoded
// fractions (0.33 for <, 0.1 for =). The opaque any allows the
// logical package to receive *catalog.Histogram without importing
// the catalog package (avoids circular import).
Histogram any
}
ScanColumnStats holds aggregated column statistics from the catalog.
type WindowBound ¶
type WindowBound struct {
Type string // "unbounded_preceding", "preceding", "current_row", "following", "unbounded_following"
Offset int
}
WindowBound describes one end of a window frame.
type WindowExpr ¶
type WindowExpr struct {
Func string // row_number, rank, dense_rank, sum, count, avg, min, max
InputCol string // the argument list, verbatim — see InputColumn
OutputCol string
PartitionBy []string
OrderBy []OrderExpr
Frame *WindowFrameSpec
}
WindowExpr is a window function expression.
func (WindowExpr) InputColumn ¶
func (w WindowExpr) InputColumn() string
InputColumn returns the COLUMN argument of a window expression.
InputCol carries the whole argument list as one string, so LAG, LEAD and NTH_VALUE spell their column alongside an offset, a default or an N — "l_quantity, 2, 0". The column is everything before the first comma; everything after belongs to the function, not to any table. NTILE's single argument is a bucket count and names no column at all.
Every consumer needs the same answer: the column-pruning rule needs it to keep the column readable, and the physical planner needs it to type the output. Taking the raw string for a column name pruned the real one out of the scan, and the window operator then found no input vector and nil-dereferenced it — a crash, not a wrong answer.
type WindowFrameSpec ¶
type WindowFrameSpec struct {
Mode string // "rows" or "range"
Start WindowBound
End WindowBound
}
WindowFrameSpec describes a window frame specification.
Source Files
¶
- agg_extra_args.go
- agg_scope_wrapper.go
- builder.go
- comma_join_lift.go
- const_arith_agg.go
- count_distinct_rewrite.go
- distinct_rewrite.go
- filter_project_pushdown.go
- inner_key_spelling.go
- join_predicates.go
- optimizer.go
- order_by_keys.go
- plan.go
- precomputed_aggregate.go
- scalar_agg_semijoin.go
- semi_anti_dedup.go
- semi_pushdown.go
- shape_only_columns.go
- star_expansion.go
- stats.go