Documentation
¶
Overview ¶
Package logical provides logical query plan representation and optimization.
Index ¶
- Constants
- Variables
- func AggScopePreservingWrapper(t NodeType) bool
- func CheckPolicyPlanOrder(plan *Node, policed func(table string) []ColumnPolicy) error
- func ContextWithColumnPolicies(ctx context.Context, tp TablePolicies) context.Context
- func ContextWithPolicyEnforced(ctx context.Context) context.Context
- func ContextWithPolicyLookup(ctx context.Context, l PolicyLookup) context.Context
- 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 PlanCarriesPolicyEnforcement(n *Node) bool
- func PolicedScanTables(n *Node) []string
- func PolicyEnforced(ctx context.Context) bool
- func RefuseUnresolvedOrdinalSortKeys(n *Node) error
- func ResolveFilterThroughProjects(pred Predicate, child *Node) (ast plansql.Node, aliases []string, ok bool)
- func ResolveOrdinalSortKeys(n *Node)
- func ScanPredicateIsRestricted(pred Predicate, restricted map[string]bool) bool
- func SetSemiPushdownEnabled(on bool) bool
- func SubstituteMaskedColumns(expr plansql.Node, relation string, policies []ColumnPolicy) (plansql.Node, bool)
- func SubstitutePreComputedAggregates(root *Node, sigs []PreComputedAggregate) (map[string]bool, error)
- func TextNamesRestricted(text string, restricted map[string]bool) (string, bool)
- type AggExpr
- type ColumnPolicy
- type DecimalMeta
- type DecorrelatedKey
- type GroupingCall
- 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, schemaColumns []string) (*Node, int)
- 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 PolicyLookup
- type PreComputedAggregate
- type Predicate
- type Projection
- type RelStats
- type ScanColumnStats
- type TablePolicies
- func (tp TablePolicies) Apply(plan *Node, columnsOf func(table string) []string) (*Node, int)
- func (tp TablePolicies) ApplyToNewScans(plan *Node, columnsOf func(table string) []string) (*Node, int)
- func (tp TablePolicies) ApplyToNewScansWithLookup(plan *Node, columnsOf func(table string) []string, lookup PolicyLookup) (*Node, int, error)
- func (tp TablePolicies) DeniedColumns() map[string]map[string]bool
- func (tp TablePolicies) For(table string) []ColumnPolicy
- 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 ErrColumnPolicyUnenforceable = errors.New("column policy could not be enforced on this plan: refusing to answer unmasked")
ErrColumnPolicyUnenforceable is returned when a column policy names a table the plan reads but the security projection could not be built for it — no catalog schema and no scan annotation to build the projection from, or every column denied.
It is an ERROR and not a silently unmasked answer on purpose: a security control that cannot be applied refuses, the way an unreadable `columns:` action refuses to load (#802). Loud beats plausible.
var ErrPolicyOrderUnrepresentable = errors.New(
"this query is not available for this identity: a column security policy applies and a " +
"predicate could not be placed above the security projection, where it must read the " +
"mask rather than the stored column")
ErrPolicyOrderUnrepresentable is the refusal for a plan whose predicates cannot be placed above the security projection they must read through.
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.
var ScanColSanitizeSwitch = optswitch.Register("scan-col-sanitize", "WADJET_SCAN_COL_SANITIZE",
"drop alias-qualified and foreign-relation names from a scan's required-column list")
ScanColSanitizeSwitch gates the DROPPING half of sanitizeScanNeeds — the pollution A/B, and nothing else. It deliberately does NOT gate the schema-spelling half; see the comment on that arm for why an optimization switch must not decide which columns a scan reads.
It is REGISTERED, which is what puts it under the optimization-invariance oracle: the oracle runs the corpus with each switch individually disabled and requires identical results, and that is exactly the property this switch lacked. Until #731's follow-up it changed 30 of the CamelCase battery's 63 cells when disabled — a switch load-bearing for correctness, which inverts the doctrine registration exists to enforce. Registering it is how the property stays true rather than being true today.
It is EXPORTED because the gate that can actually see it lives in another package: the optimization-invariance oracle sweeps every registered switch over TPC-H, whose columns are all lower case, and there the folded reference and the schema spelling are the SAME STRING — disabling this switch on that corpus cannot change a row by construction. The corpus that can see it is the CamelCase invariance battery in internal/coordinator, and it drives both states through this handle rather than reading the env var, so one run covers both.
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 CheckPolicyPlanOrder ¶ added in v0.18.38
func CheckPolicyPlanOrder(plan *Node, policed func(table string) []ColumnPolicy) error
CheckPolicyPlanOrder is the invariant every arm must satisfy, asserted on the FINAL logical plan:
- every Scan of a policed relation carries that relation's security projection directly above it; and
- no Filter between that projection and the scan references a policed column, unless it is the POLICY's own row filter.
(1) is ADR-0033 decision 1 taken literally, and it is the question the earlier stage-level check could not ask: a stage that scans a policed table with NO projection at all was invisible to a check that only inspected stages which HAVE one — which is exactly the shape a decorrelated semi-join's inner side had (#859 round 3). (2) is decision 6.
It refuses rather than repairs, because by this point the repair passes have run: reaching here means a shape this planner cannot express safely, and the branch's doctrine for that is 0A000, never a pin over a leak.
func ContextWithColumnPolicies ¶ added in v0.18.38
func ContextWithColumnPolicies(ctx context.Context, tp TablePolicies) context.Context
ContextWithColumnPolicies returns ctx carrying the column policies in force. A nil or empty map returns ctx unchanged, so an unpoliced query costs nothing and every reader can test for absence with len().
func ContextWithPolicyEnforced ¶ added in v0.18.38
ContextWithPolicyEnforced marks a context whose query had ANY policy applied — a column projection or a row filter. A column policy is discoverable from ColumnPoliciesFromContext; a row-filter-only policy is not, and a dispatch site that ships a statement's TEXT to a worker has to refuse for both.
func ContextWithPolicyLookup ¶ added in v0.18.38
func ContextWithPolicyLookup(ctx context.Context, l PolicyLookup) context.Context
ContextWithPolicyLookup returns ctx carrying the per-table policy lookup.
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 over a derived table whose own FROM is a join — 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 PlanCarriesPolicyEnforcement ¶ added in v0.18.38
PlanCarriesPolicyEnforcement reports whether this plan carries anything a policy put there: a security projection, or a row filter injected by row-level security.
A caller that is about to hand a query to something that will RE-PLAN it from the SQL TEXT — the coordinator's async door dispatches a TaskTypePipeline task carrying `SQLText`, and the worker parses, builds and optimizes it again with no policy in reach — has to ask this first. The enforced plan does not survive that hop, and answering from the re-planned one hands the caller the stored values.
func PolicedScanTables ¶ added in v0.18.38
PolicedScanTables lists, once each, the base table every Scan in the plan reads — the relations a policy has to decide about.
This is the plan, not the statement: `plansql.SelectInfo.Tables` carries a DERIVED table under its own subquery TEXT, a CTE reference under the CTE's name, and nothing at all for the arms of a UNION, so a policy layer driven by it polices none of those (#859) — and default-denies the two names that are not tables. Every one of those shapes reaches the same base-table Scan here.
func PolicyEnforced ¶ added in v0.18.38
PolicyEnforced reports whether any policy shaped this query's plan.
func RefuseUnresolvedOrdinalSortKeys ¶ added in v0.18.21
RefuseUnresolvedOrdinalSortKeys reports the first select-list ordinal ResolveOrdinalSortKeys could not answer, as the error the client sees.
Called once from each plan entry point — Planner.Plan and Planner.PlanDistributed — so BOTH engines refuse the same shape with the same class and the same wording. Refusing inside the sort BUILDER would have covered the single-process path only, and the DAG would have gone on to spell a stage over a key named "1" and failed three task attempts later with a message about an input schema.
Loud, never quiet: the alternative is to sort on the numeric constant, which is the same value in every row and returns the input untouched — right rows, arbitrary sequence, no error. That is the failure the whole hidden-sort-key pass exists to end (#320).
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 ResolveOrdinalSortKeys ¶ added in v0.18.21
func ResolveOrdinalSortKeys(n *Node)
ResolveOrdinalSortKeys rewrites every deferred positional sort key against the projection below its Sort.
It is safe to run more than once and on a plan with no deferred keys: the walk exits on the first node with nothing to do.
func ScanPredicateIsRestricted ¶ added in v0.18.38
ScanPredicateIsRestricted reports whether a predicate attached to a scan reads a policed column — its structured column, or any identifier its expression names.
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 SubstituteMaskedColumns ¶ added in v0.18.38
func SubstituteMaskedColumns(expr plansql.Node, relation string, policies []ColumnPolicy) (plansql.Node, bool)
SubstituteMaskedColumns rewrites every reference to a MASKED column of `relation` with that column's mask expression, and returns the result.
It is the security projection applied to an expression the planner never sees. A DML predicate is COMPILED, not planned (ADR-0031): `DELETE FROM t WHERE ssn = '<stored value>'` never meets a Scan, so no projection can sit under it, and the comparison read the row as stored — a probe oracle for the masked column, and a destructive one. Substituting the mask into the expression is that projection, done where this statement can carry it: the predicate then compares '***' and matches nothing, and `SET dept = ssn` writes the mask.
ok=false means the rewrite could not be done soundly (an aggregate output, a subquery, a node the substituter cannot see through) and the caller must refuse rather than run an expression that reads the stored row.
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.
func TextNamesRestricted ¶ added in v0.18.38
textNamesRestricted looks for a policed column among an expression's identifier tokens, ignoring anything inside single quotes. TextNamesRestricted is textNamesRestricted, exported for the stage-level twin of this check in the physical planner.
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 GroupingCall ¶ added in v0.18.15
GroupingCall is one GROUPING(a[, b, ...]) call in a SELECT list over GROUPING SETS / ROLLUP / CUBE.
Args are the group-key spellings the call asks about, IN ARGUMENT ORDER, because the order is the answer: PostgreSQL returns a bitmask whose leftmost argument is the most significant bit, so GROUPING(g, h) and GROUPING(h, g) differ (2 vs 1 on a row that groups h but not g — verified against PostgreSQL 17).
OutputCol is the hidden aggregate output slot the bitmask is published under; the SELECT-list projection reads that slot by name, the way a window column reads its own slot.
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
// PolicyFilter marks a Filter injected by row-level security
// (InjectRowFilter) rather than written by the client. It is the
// counterpart of SecurityBarrier below: together they are how a caller
// asks "does this plan carry enforcement I must not throw away"
// (PlanCarriesPolicyEnforcement).
PolicyFilter bool
// 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
// GroupingCalls is one entry per distinct GROUPING(...) call at this
// query level — SELECT list, HAVING, or nested inside either — in the
// order the slots were allocated.
//
// Set whenever the query has a GROUP BY, sets or not: a plain GROUP BY
// answers 0 for every call (every key is grouped in every row) but takes
// the SAME hidden slot rather than a constant fold of its own, so a call
// nested in a larger expression has one substitution to make on either
// shape. See builder.go's allocGroupingSlot (#804).
GroupingCalls []GroupingCall
// 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, schemaColumns []string) (*Node, int)
InjectColumnPolicies walks the logical plan and inserts a security projection above every Scan of the given table. Denied columns are removed and masked columns are replaced with their mask expression, so restricted data never enters the execution pipeline — every consumer above the scan (WHERE, GROUP BY, an aggregate, a join key, a window, a derived table, `SELECT *`) sees the masked value and never the true one.
schemaColumns is the TABLE's declared column list, from the catalog. It is the authority, not the scan's ScanColumns: those carry whatever the builder or a later pruning pass happens to have put there, and for `SELECT *`, an aggregate-only SELECT list or a derived table they can be empty — which is how #859's projection came to be skipped for exactly the queries that most need it. A security control never degrades to a grant, so when neither the catalog nor the scan can name the columns the scan is reported UNPROTECTED (the second return value) and the caller refuses the query rather than answering it unmasked.
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.
It goes on directly above the SCAN and therefore BELOW the security projection a column policy adds (auth.EnforcePlanPolicies injects the projection first for exactly this reason). That is PostgreSQL's RLS ordering: the POLICY's predicate sees the row as stored, so a row filter written against a masked column compares the TRUE value, while a predicate the USER writes sits above the projection and compares the MASK.
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
// Position is a select-list ordinal the parser could not count, because
// the list carries a `*` at or before it and a star's width is a catalog
// question (#810). Non-zero means Column is not yet a name;
// ResolveOrdinalSortKeys fills it in after ExpandStarProjections and
// clears this. A key that reaches the physical planner still carrying one
// is refused loudly rather than sorted on the constant, which would be a
// silent no-op.
Position int
// SlotPos is the 1-based select-list POSITION this key was written as,
// kept even after Column has been resolved to a name. Unlike Position it
// is not a "not yet resolved" marker: it is the ADDRESS, for the case
// where the name is not one because two output columns share it (#557).
SlotPos int
}
OrderExpr is a sort expression.
type PolicyLookup ¶ added in v0.18.38
type PolicyLookup func(table string) (cols []ColumnPolicy, rowFilter string, err error)
PolicyLookup answers, for one base table, what this identity's policy does to it: the column obligations, the row filter, and an error when the identity may not read the table at all.
It rides the context beside the RESOLVED policies because the resolved set is only what the plan showed AT ENFORCEMENT TIME, and the plan grows. A table named only inside an `IN (SELECT … )` is not in the plan when auth.EnforcePlanPolicies runs — the subquery is still SQL TEXT — so it was never policed at all, and when the optimizer decorrelated that subquery into a semi-join the inner scan came out with NO security projection and its predicate read the STORED column. `… IN (SELECT id FROM t WHERE bal > 300)` over a `bal` masked to 0 returned exactly the rows above that threshold, and the client picks the threshold (#859 round 3).
A lookup makes the invariant reachable at every pass that can mint a scan: EVERY scan of a policed relation in the FINAL plan carries that relation's projection.
func PolicyLookupFromContext ¶ added in v0.18.38
func PolicyLookupFromContext(ctx context.Context) PolicyLookup
PolicyLookupFromContext returns the per-table policy lookup, or nil.
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
// FromPolicy marks a predicate the POLICY itself put there — a column
// policy's row filter, which reads the row AS STORED by design (ADR-0033
// decision 6). It is the one predicate allowed to sit on a policed scan
// below the security projection, and CheckPolicyPlanOrder refuses every
// other one: a scan predicate feeds row-group PRUNING against the stored
// column's statistics, so a user predicate that reaches the scan answers
// "is there a row whose STORED value satisfies this" however the plan
// above it is ordered.
FromPolicy 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
// PublishedName is the name the CLIENT is told this column has, where
// that differs from Alias — PostgreSQL's `FigureColname`
// (plansql.OutputColumnName, #732). `SELECT g + 1` publishes `?column?`,
// `SELECT COUNT(*)` publishes `count`, `SELECT CAST(g AS bigint)`
// publishes `g`.
//
// It is a SECOND name and not a rewrite of Alias, for the reason ADR-0026
// §2 gives group keys a pair: an aggregate's `OutputCol` IS its Alias and
// every consumer inside the planner — GROUP BY, HAVING, ORDER BY, the
// stage's rename source — resolves against it, while two unaliased
// aggregates legally publish ONE name (`SELECT COUNT(*), COUNT(g)` is two
// columns called `count`). Collapsing the two would make the resolution
// spelling ambiguous to fix a name.
//
// Empty means "the same as Alias", which is every projection the planner
// itself mints. Only the OUTPUT projection's copy is consumed: it names
// what leaves the engine, and a nested block's names are what the block
// above resolves against.
PublishedName 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 TablePolicies ¶ added in v0.18.38
type TablePolicies map[string][]ColumnPolicy
TablePolicies is the set of column policies in force for one query, keyed by the FOLDED base-table name.
It travels on the context because a query is planned in more than one place. The statement's own plan is enforced by auth.EnforcePlanPolicies before the optimizer runs, but an expression subquery — `(SELECT MAX(ssn) FROM t)`, an IN set, an EXISTS — is a WHOLE SECOND QUERY that the physical planner parses, builds and optimizes on its own (physical.buildSubqueryPipeline), and it never passed through the enforcement path at all. Before #859 that second path answered from the raw column while the first one masked.
The context is the only carrier both paths already share.
func ColumnPoliciesFromContext ¶ added in v0.18.38
func ColumnPoliciesFromContext(ctx context.Context) TablePolicies
ColumnPoliciesFromContext returns the column policies in force, or nil.
func (TablePolicies) Apply ¶ added in v0.18.38
Apply injects the security projection for every policed table into plan. columnsOf resolves a table's declared column list (the catalog); it may return nil, in which case the scan falls back to its own annotations and, failing that, is reported unprotected. The second return value is the number of scans left UNPROTECTED — never zero-and-ignored: a caller that cannot protect a scan must refuse the query.
func (TablePolicies) ApplyToNewScans ¶ added in v0.18.38
func (tp TablePolicies) ApplyToNewScans(plan *Node, columnsOf func(table string) []string) (*Node, int)
ApplyToNewScans injects the security projection over any policed scan that is NOT already under one.
The optimizer MINTS SCANS. `decorrelateInSubqueries`, `decorrelateExists` and `decorrelateScalarSubqueries` re-parse a subquery from its SQL text and build a fresh Scan for its FROM item, and those scans are created AFTER enforcement has run — so before this pass a decorrelated `WHERE a.ssn IN (SELECT ssn FROM t b)` compared the outer's MASK against the inner's STORED column and answered 0 where both sides masked answer every row. No value escaped (a semi-join emits only outer columns), but the answer was wrong, and the same seam is the one a future rewrite could make leak.
A scan already beneath a SecurityBarrier is skipped, so the pass is safe to run after every Optimize.
func (TablePolicies) ApplyToNewScansWithLookup ¶ added in v0.18.38
func (tp TablePolicies) ApplyToNewScansWithLookup(plan *Node, columnsOf func(table string) []string, lookup PolicyLookup) (*Node, int, error)
ApplyToNewScansWithLookup is ApplyToNewScans for a plan that may name a relation the resolved set never saw — the inner of a decorrelated semi-join, whose table lived in SQL text when enforcement ran. `lookup`, when given, answers for such a table; its row filter goes in BELOW the projection, the order ADR-0033 decision 6 fixes.
func (TablePolicies) DeniedColumns ¶ added in v0.18.38
func (tp TablePolicies) DeniedColumns() map[string]map[string]bool
DeniedColumns maps each policed table (folded) to its denied columns (folded). These are the columns that, for this identity, DO NOT EXIST: a reference to one is 42703, not a NULL and not a mask.
func (TablePolicies) For ¶ added in v0.18.38
func (tp TablePolicies) For(table string) []ColumnPolicy
For returns the policies for one table, matched case-insensitively.
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_placement.go
- agg_scope_wrapper.go
- builder.go
- column_policy.go
- comma_join_lift.go
- const_arith_agg.go
- const_arith_agg_typed.go
- count_distinct_rewrite.go
- decorrelated_inner_plan.go
- distinct_rewrite.go
- filter_project_pushdown.go
- grouping.go
- inner_key_spelling.go
- join_predicates.go
- lateral_empty_input.go
- optimizer.go
- order_by_keys.go
- ordinal_sort_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