Documentation
¶
Overview ¶
Package logical holds the Phase 3 (TODO.md §"Phase 3 — Semantic analysis") logical-operator hierarchy. A LogicalOperator describes WHAT a SQL query is doing — scan this table, filter on that predicate, project these columns, sort by those columns — without committing to HOW. Semantic analysis (porting of Java's `SemanticAnalyzer`) produces a LogicalOperator tree from the parse tree. Phase 4 Cascades consumes the tree and emits physical plans; until Cascades lands, the naive Generator can consume LogicalOperator directly through a thin translator.
Naming mirrors Java's `fdb-relational-core/recordlayer/query/LogicalOperator.java` + siblings, trimmed to the core operator set we actually need for the current yamsql corpus:
- LogicalScan — `FROM tbl` (single-table read)
- LogicalFilter — `WHERE pred`
- LogicalProject — `SELECT a, b, expr AS n`
- LogicalSort — `ORDER BY …`
- LogicalLimit — `LIMIT n OFFSET m`
- LogicalAggregate — `GROUP BY … + agg(…)`
- LogicalJoin — `INNER / LEFT / RIGHT JOIN`
- LogicalUnion — `UNION [ALL]`
- LogicalInsert — `INSERT INTO tbl VALUES / INSERT SELECT`
- LogicalUpdate — `UPDATE tbl SET col = expr WHERE …`
- LogicalDelete — `DELETE FROM tbl WHERE …`
- LogicalDDL — `CREATE / DROP` passthrough (no tree shape)
- LogicalCTE — `WITH name AS (…) SELECT …` (non-recursive + recursive)
**Phase 3 scope:** this package is the TARGET SHAPE only. The semantic analyzer that translates parse tree → LogicalOperator and the translator that turns LogicalOperator → executable Plan are separate Phase 3 deliverables. Committing to the shape here lets them land incrementally without churning all of the pkg/ relational tree at once.
**Java alignment.** Most operators map 1:1 to a Java counterpart:
LogicalFilter ↔ LogicalFilter / QueryPredicate-carrying child LogicalProject ↔ LogicalProjectionExpression LogicalSort ↔ LogicalSortExpression LogicalAggregate ↔ GroupByExpression LogicalUnion ↔ LogicalUnionExpression LogicalInsert ↔ InsertExpression LogicalUpdate ↔ UpdateExpression LogicalDelete ↔ DeleteExpression
Two deliberate divergences:
- `LogicalScan` does not exist in Java-Cascades; Java represents a FROM-source as a Cascades `FullUnorderedScanExpression` wrapped by a `Quantifier`. RFC-022 argues that Phase 3 should own a pure logical-plan representation distinct from Cascades; LogicalScan is the scan-stand-in at the logical level.
- `LogicalJoin` does not exist in Java-Cascades as a discrete type; Java encodes joins via a `SelectExpression` binding multiple `Quantifier`s. Same rationale: keeping join as an explicit logical operator separates Phase 3 tree-building from Phase 4 Cascades translation.
Both divergences are documented in RFC-023 / TODO Phase 3+4.
Explicit predicate / expression representation is deferred. For now LogicalFilter / LogicalProject etc. carry parse-tree handles (antlr IExpressionContext). RFC-021 Phase 2 replaces those with `Value` nodes from the Cascades Value hierarchy. RFC-023 committed to non-generic interfaces + `any`; the seed lives in `pkg/recordlayer/query/plan/cascades/`. As that package grows this one will migrate its text-handle predicates to real Values.
Index ¶
- func FindOuterScanTable(op LogicalOperator, alias string) string
- func IsUnnestOrdinalAlias(op LogicalOperator, alias string) bool
- func OuterSourceIsDerivedTable(op LogicalOperator, alias string) bool
- type AggregateCall
- type AggregateOutputSlot
- type Assignment
- type ColumnRef
- type CorrelatedScalarSubquery
- type ExistsSubquery
- type GroupKey
- type JoinKind
- type LogicalAggregate
- type LogicalCTE
- type LogicalDDL
- type LogicalDelete
- type LogicalDistinct
- type LogicalFilter
- type LogicalInlineValues
- type LogicalInsert
- type LogicalJoin
- type LogicalLimit
- type LogicalOperator
- type LogicalProject
- type LogicalScan
- type LogicalSort
- type LogicalUnion
- type LogicalUnnest
- type LogicalUpdate
- type LogicalValues
- type ScalarSubquery
- type SortDir
- type SortKey
- type TraversalOrder
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func FindOuterScanTable ¶
func FindOuterScanTable(op LogicalOperator, alias string) string
FindOuterScanTable resolves a lateral unnest's outer source alias to the scanned table name by matching a LogicalScan whose source alias is `alias` (case-insensitive) among the VISIBLE FROM-scope sources of the outer leg `op`. It is used to resolve the outer source of a lateral unnest (`FROM t, t.arr AS x` → the scan of `t`) so its proto descriptor can be inspected for the array field.
The walk MUST NOT descend into a CTE / derived-table BODY. A derived table `(SELECT … FROM T1) AS d` lowers to a `LogicalCTE{Body: <…scan of T1…>, Main: Scan(d)}`; only `d` is a visible source — `T1` is hidden inside the body and out of scope. Descending into `Body` would match the hidden `T1` scan and explode a correlated array against a source the query can't see (a silent-wrong / mis-classification). So at a `LogicalCTE` we resolve ONLY against its `Main` (the visible alias projection), never its `Body`. This mirrors Java's `resolveCorrelatedIdentifier` resolving against `getLogicalOperatorsIncludingOuter()` — the in-scope quantifiers, not nested query bodies.
Returns "" when no VISIBLE matching scan is found (a non-scan outer, or a name hidden behind a derived-table boundary), so the caller falls back to the table path.
func IsUnnestOrdinalAlias ¶
func IsUnnestOrdinalAlias(op LogicalOperator, alias string) bool
IsUnnestOrdinalAlias reports whether `alias` names a prior lateral unnest's AT ORDINAL alias (`t.arr AS x AT o` → `o`) in the outer sub-plan `op`. That alias binds a scalar INTEGER ordinal, so a field access on it (`o.sub`) is a resolution error, not a chainable source — the caller surfaces the honest UNDEFINED_COLUMN Java produces instead of the generic unnest fallback. Same walk as FindOwnerUnnest (no CTE-Body descent), matching the AT alias only.
func OuterSourceIsDerivedTable ¶
func OuterSourceIsDerivedTable(op LogicalOperator, alias string) bool
OuterSourceIsDerivedTable reports whether `alias` (a lateral unnest's segment-0 outer source name) is bound, in the outer sub-plan `op`, to a DERIVED-TABLE / CTE leg — i.e. a `LogicalCTE` whose Name equals `alias`. It reads the logical tree STRUCTURALLY (not a translator-internal cteScope map), so it fires independent of cteScope population order and regardless of whether the alias also names a real same-named base table.
A derived table `(SELECT …) AS D` lowers to a `LogicalCTE{Name:D, Main:Scan(D)}` inside the outer leg. When such a leg shadows a real same-named table, validating the unnest's array field against the base-table descriptor would explode the WRONG column (a derived-output silent-wrong). Detecting the derived/CTE leg here — by the in-scope quantifier alias, exactly as Java's generateCorrelatedFieldAccess resolves the in-scope source rather than the catalog table — lets the caller reject (or skip a base-table check for) the derived-output unnest in ALL cases.
Like FindOuterScanTable, it does NOT descend into a CTE's Body (only its Main): a derived table is its own FROM scope; a same-named CTE nested inside another derived body is out of the current scope.
Types ¶
type AggregateCall ¶
type AggregateCall struct {
Func string // upper-case aggregate function name (COUNT/SUM/MIN/MAX/AVG)
Operand string // canonical operand text; "*" for COUNT(*)
Star bool // COUNT(*) (or COUNT(<non-null const>) collapsed to it)
Distinct bool
// BareColumn: the operand is a single column reference PER THE PARSE
// TREE — a lazy FieldValue read of Operand is well-defined without
// catalog resolution. False for computed/expression operands, which
// require a resolved Value.
BareColumn bool
// Qualified: a BareColumn operand carried a table qualifier PER THE
// PARSE TREE (FullId segment count > 1) — never a dot scan of the
// rendered name, which a delimited identifier containing a literal dot
// would false-positive. Always false for computed operands; consumers
// gating on qualification fall back to a conservative canonical-text
// scan there (a dot inside a computed rendering only ever comes from a
// real qualified reference or a quoted literal, and the gate is
// optimization-only).
Qualified bool
// Bare/Qualifier are the SEGMENTS behind Operand for a BareColumn — the
// last segment and the leading one(s). Qualified already said whether a
// qualifier exists; these say what it IS, which is what a consumer needs to
// resolve the operand WITHOUT slicing the rendered text apart at its first
// dot. Empty for computed operands and for producers that have not been
// taught to carry them; read them only through Ref().
Bare string
Qualifier string
}
AggregateCall is the STRUCTURED form of one aggregate in the SELECT list, captured from the parse tree at build time — function, operand text (for result-map keying), and the star/distinct/bare-column classification. The translator consumes this instead of re-parsing the display text in Aggregates: a missing entry is a typed decline, never a string split (RFC-180 F-1 — the text reparse mangled nested arithmetic and silently dropped HAVING groups).
func (AggregateCall) CanonicalName ¶
func (c AggregateCall) CanonicalName() string
CanonicalName renders the call as `FUNC(OPERAND)`, `FUNC(DISTINCT OPERAND)` or `COUNT(*)` — the alias-free output-column name for an aggregate. The FUNC half is upper because it is written that way from an enum, never folded from the SQL; the OPERAND half is whatever the producer minted and is not touched here.
CONSUMERS NO LONGER ALL FOLD, and this sentence used to say they did ("case-insensitively, upper-cased or via normalizeAggOutputName"). Say exactly which changed, because the first attempt at this correction over-claimed in the other direction:
- normalizeAggOutputName and normalizeAggregateBindingName stopped upper-casing under RFC-237. They now strip whitespace only.
- Some consumers still apply their OWN strings.ToUpper to this result — logical_predicate.go's aggTypes map is one, and it folds symmetrically on write and read, which is a consistent key rather than a naming decision.
So the upper-case Func is safe because it is a LITERAL, not because a fold on the far side would have rescued it. That distinction is load-bearing the moment a consumer compares exactly, and several now do.
func (AggregateCall) Ref ¶
func (c AggregateCall) Ref() ColumnRef
Ref is the operand's segment triple, reconciled against its canonical text. A COUNT(*) or computed operand captures nothing: its Operand is a rendering, and BareColumn is what says so.
type AggregateOutputSlot ¶
AggregateOutputSlot preserves one visible aggregate SELECT item after the parser's internal key/call harvesting and reordering. SelectOrdinal is one-based and unique. NativeOrdinal is zero-based into [group keys..., aggregate calls...], or -1 for a computed expression.
type Assignment ¶
Assignment is one SET clause entry. Expr is the canonical text (used for explain and as a fallback); Value is the resolved RHS expression Value (populated by the catalog-aware builder) that the executor evaluates against each target row. A nil Value means the text builder ran without catalog resolution.
type ColumnRef ¶
type ColumnRef struct {
Present bool
Bare string // last segment
Qualifier string // leading segment(s); "" when unqualified
Qualified bool // parse-tree segment count > 1
}
ColumnRef is the parse-tree segment triple of a plain column reference. Present mirrors SortKey/GroupKey's convention of a populated Bare, made explicit because an unqualified reference and an uncaptured one are otherwise the same zero value and mean opposite things.
func ColumnRefFor ¶
ColumnRefFor states a segment triple against the name that was actually RENDERED for it, and is the only way a ColumnRef should be built from a producer's fields.
The reconciliation is the invariant, not a formality. The rendered name is not always the segments joined: the derived-table shell strips a `X.` qualifier prefix off the rendering, and a rebase can rewrite a key's text entirely. A triple describing a DIFFERENT string than the one downstream carries is worse than no triple, because it is trusted — told a bare `ID` is qualified by X, a consumer looks for leg X's ID instead of the flat column the shell just produced.
So Present is claimed only when the triple demonstrably spells the rendered name. Anything else reads as "not captured", which every consumer already handles by keeping the behaviour it had.
It lives here rather than beside any one producer because three of them now build these — projected columns, ORDER BY keys and aggregate operands — and three copies of an invariant is three chances for one to drift.
type CorrelatedScalarSubquery ¶
type CorrelatedScalarSubquery struct {
// rows, so SQL standard at-most-one-row semantics apply: a second inner row
// per outer row is a cardinality violation (21000), not silent truncation.
// It is false when exact pagination caps a multi-row-capable source to 0/1,
// or when the source is intrinsically <=1 (for example a non-grouped real
// aggregate, for which even LIMIT >1 remains safe). Data-dependent LIMIT >1
// is rejected before this IR until post-pagination scalar collapse exists.
StrictSingle bool
}
CorrelatedScalarSubquery pairs a correlation alias with a logical plan for a correlated scalar subquery like `(SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id)`. The inner plan has the correlation predicate baked in as a filter child of the aggregate — the executor re-evaluates it per outer row via FlatMap. Carried on LogicalProject or LogicalFilter, depending on whether the scalar is consumed by SELECT or WHERE.
type ExistsSubquery ¶
type ExistsSubquery struct {
Alias values.CorrelationIdentifier
// FlowedType is the exact whole-row type of Plan. It is captured when the
// subquery is built so every ExistsValue and existential Quantifier use one
// stable type authority rather than re-deriving a placeholder type.
FlowedType values.Type
Plan LogicalOperator
JoinPredicate predicates.QueryPredicate
// KnownTruth is non-nil when the front-end can prove the EXISTS result from
// relational cardinality alone. A non-grouped aggregate produces exactly one
// row before pagination; applying a literal LIMIT/OFFSET therefore makes the
// result statically TRUE (the row survives) or FALSE (the row is skipped).
// LIMIT 0 is likewise empty for every supported inner shape. The translator
// substitutes the constant in positive and negated WHERE consumers instead
// of building a correlated semi-join. Nil means the result is data-dependent.
KnownTruth predicates.TriBool
// OuterOnlyJoinConjuncts marks a JoinPredicate carrying conjuncts with
// NO inner-source reference that can FILTER (a nested-EXISTS middle
// routes them here; the inside placement does not plan for that
// composition; statically-TRUE tautologies are excluded - routing TRUE
// is a no-op). The outer-routing validity matrix, enforced by
// declineNegatedOuterOnlyEsq (predicate side) and
// declineNegatedOuterOnlyEsqValue (value side) in the translator:
//
// WHERE/ON + positive -> VALID (P AND EXISTS(Q) == EXISTS(P AND Q))
// WHERE/ON + negative -> DECLINE (would compute P AND NOT-EXISTS(Q))
// projected + either -> DECLINE (outer-routing filters the row
// stream; a projected boolean must not)
//
// HAVING is kept out of the surface by translateAggregate's blanket
// rejection of HavingExistsSubqueries.
OuterOnlyJoinConjuncts bool
}
ExistsSubquery pairs an existential alias with the logical plan for an EXISTS subquery. Carried on LogicalFilter so the Cascades translator can build ExistentialQuantifiers over the subquery plans.
type GroupKey ¶
type GroupKey struct {
Display string
Bare string // last segment of a bare column ref; "" for expression keys
Qualifier string // leading segment(s) when qualified; "" otherwise
Qualified bool // parse-tree segment count > 1
// Segs is the FULL ordered segment list of a bare column reference
// (`r.v.z` -> [R V Z]); nil for expression keys. Qualifier joins the
// leading segments into one string and so cannot say where one segment
// ends and the next begins — `R.V` reads as a source alias, which is why
// a grouping key that descends INTO a struct column could only be
// resolved once the segments travelled with it. Resolution consumes Segs;
// Qualifier/Display remain renderings.
Segs []string
Value values.Value
}
LogicalAggregate runs GROUP BY + aggregate functions on its child. GroupKeys are the grouping-column expressions; Calls holds the structured aggregate calls (see AggregateCall) with parallel Aliases. GroupKey is one GROUP BY key in structured form (RFC-180 F-3): the display text is a rendering for output naming and diagnostics, never re-parsed; qualification and segments are parse-tree truth; Value is the resolved key (expressions always resolve; a bare column's Value is filled by the upgrade passes, nil = lazy bare read).
type LogicalAggregate ¶
type LogicalAggregate struct {
Input LogicalOperator
GroupKeys []GroupKey
// Calls is the SOLE aggregate-call representation. Builders populate it
// from the parse tree; the translator requires it (RFC-180 F-3 retired
// the parallel display-text slice).
Calls []AggregateCall
Aliases []string // parallel to Calls
AggregateOperands []values.Value // resolved operand Values (parallel to Calls); nil slot = use text
HasDistinctAggregate bool // true when any aggregate uses DISTINCT (e.g. COUNT(DISTINCT x))
// HasHaving: the query carried a HAVING clause. Presence SENTINEL only
// (RFC-180 F-3 — the former canonical-text field was never re-parsed):
// the translator declines when HasHaving is set but HavingPredicate is
// nil, so an unstructured HAVING can never be silently dropped.
HasHaving bool
HavingPredicate predicates.QueryPredicate
HavingExistsSubqueries []ExistsSubquery // EXISTS subquery plans inside HAVING
HavingScalarSubqueries []ScalarSubquery // scalar subquery plans inside HAVING
// OutputSlots is the visible SQL SELECT contract in SELECT-list order.
// NativeOrdinal addresses the aggregate's physical output row
// [GroupKeys..., Calls...]; -1 marks a computed post-aggregate item.
// The aggregate keeps producing its native row until the legal projection
// boundary above ORDER BY, where this contract is materialized. Public
// labels live exclusively on that Project and are not producer identity.
OutputSlots []AggregateOutputSlot
}
func NewAggregate ¶
func NewAggregate(input LogicalOperator, groupKeys []GroupKey, calls []AggregateCall, aliases []string, hasHaving bool) *LogicalAggregate
func (*LogicalAggregate) Children ¶
func (a *LogicalAggregate) Children() []LogicalOperator
func (*LogicalAggregate) Explain ¶
func (a *LogicalAggregate) Explain(indent string) string
type LogicalCTE ¶
type LogicalCTE struct {
Name string
Body LogicalOperator
Main LogicalOperator
Recursive bool
ColumnAliases []string // WITH c(a, b) AS (...) → renames body's output columns
TraversalOrder TraversalOrder
// PreserveMainSource marks a scope-only envelope: Name registers Body for
// scans in Main, but the envelope does not replace Main's outward source
// identity. Correlated EXISTS/Scalar plans use this when they copy enclosing
// CTE definitions into a self-contained subplan. Derived-table alias carriers
// leave it false because their CTE name deliberately IS the outward alias.
PreserveMainSource bool
// Binding is the derived/CTE leg's binding correlation name when its
// FROM alias duplicates an earlier leg's ("" = Name binds). See
// LogicalScan.Binding.
Binding string
}
LogicalCTE wraps a named Common Table Expression around a Main query. The Body is the CTE's own plan; Main references Body via a LogicalScan on Name. Recursive CTEs set Recursive=true — Body may self-reference (the recursive evaluator lives at the executor layer for now).
func NewCTE ¶
func NewCTE(name string, body, main LogicalOperator, recursive bool) *LogicalCTE
NewCTE constructs a LogicalCTE.
func (*LogicalCTE) Children ¶
func (c *LogicalCTE) Children() []LogicalOperator
func (*LogicalCTE) Explain ¶
func (c *LogicalCTE) Explain(indent string) string
type LogicalDDL ¶
LogicalDDL wraps a DDL statement that has no meaningful tree shape (CREATE TABLE, DROP INDEX, …). Kind carries the DDL command ("CREATE TABLE" etc.) and Text the canonical source.
func NewDDL ¶
func NewDDL(kind, text string) *LogicalDDL
func (*LogicalDDL) Children ¶
func (*LogicalDDL) Children() []LogicalOperator
func (*LogicalDDL) Explain ¶
func (d *LogicalDDL) Explain(indent string) string
type LogicalDelete ¶
type LogicalDelete struct {
Target string
Input LogicalOperator
}
LogicalDelete removes rows matching Input from Target.
func NewDelete ¶
func NewDelete(target string, input LogicalOperator) *LogicalDelete
func (*LogicalDelete) Children ¶
func (d *LogicalDelete) Children() []LogicalOperator
func (*LogicalDelete) Explain ¶
func (d *LogicalDelete) Explain(indent string) string
type LogicalDistinct ¶
type LogicalDistinct struct {
Input LogicalOperator
}
LogicalDistinct removes duplicate rows from its input.
func NewDistinct ¶
func NewDistinct(input LogicalOperator) *LogicalDistinct
func (*LogicalDistinct) Children ¶
func (d *LogicalDistinct) Children() []LogicalOperator
func (*LogicalDistinct) Explain ¶
func (d *LogicalDistinct) Explain(indent string) string
type LogicalFilter ¶
type LogicalFilter struct {
Input LogicalOperator
Predicate predicates.QueryPredicate // preferred when non-nil
PredicateText string // source-text fallback
ExistsSubqueries []ExistsSubquery // subquery plans for EXISTS predicates
ScalarSubqueries []ScalarSubquery // uncorrelated scalar plans (pre-evaluated)
}
LogicalFilter applies a WHERE/HAVING predicate to its child.
Predicate is the preferred representation — a cascades QueryPredicate tree produced by the expr walker. When non-nil, Explain renders it via Predicate.Explain(), which yields the normalised form after simplification (tautology-folded, NOTs pushed to leaves, operands tree-walked).
PredicateText is the fallback: the canonical source text of the WHERE expression. Used when the expression shape is out of the walker's scope (UnsupportedExpressionShapeError) or when the builder is constructed without a metadata-backed catalog (the catalog-less Explain path, which has no transaction in scope).
func NewFilter ¶
func NewFilter(input LogicalOperator, pred string) *LogicalFilter
NewFilter constructs a text-only LogicalFilter — used by the non-catalog-aware logical-builder path where only canonical source text is available. Pair with NewFilterWithPredicate when a predicates.QueryPredicate tree is in scope (catalog-aware builder); the predicate-tree form takes precedence in Explain output when both are set.
func NewFilterWithPredicate ¶
func NewFilterWithPredicate(input LogicalOperator, pred predicates.QueryPredicate, text string) *LogicalFilter
NewFilterWithPredicate constructs a LogicalFilter whose predicate is a cascades QueryPredicate tree. The text form is retained for diagnostics so Explain output stays stable even when the Predicate render differs from the source text (e.g. after tautology-folding).
func (*LogicalFilter) Children ¶
func (f *LogicalFilter) Children() []LogicalOperator
func (*LogicalFilter) Explain ¶
func (f *LogicalFilter) Explain(indent string) string
type LogicalInlineValues ¶
type LogicalInlineValues struct {
Alias string
Binding string
// contains filtered or unexported fields
}
LogicalInlineValues is a multi-row VALUES table source in a FROM clause. Its collection is the exact literal array Java lowers directly to an ExplodeExpression: every array element is one named record row.
This is deliberately distinct from both LogicalValues (the legacy single-row, text-only SELECT-without-FROM seed) and LogicalUnnest (a lateral correlated array access). Conflating either shape with an inline table would give it the wrong cardinality or make it participate in lateral-unnest gather/collision rules.
func FindOwnerInlineValues ¶
func FindOwnerInlineValues(op LogicalOperator, alias string) *LogicalInlineValues
FindOwnerInlineValues resolves one visible inline VALUES source in the current logical FROM scope. CTE bodies are separate scopes, so a CTE exposes only its Main here. Duplicate aliases are ambiguous and deliberately return nil instead of selecting whichever leaf the traversal happens to visit first.
func NewInlineValues ¶
func NewInlineValues(alias string, collection values.Value) (*LogicalInlineValues, error)
NewInlineValues constructs an exact literal-table source. alias is the source's query-block correlation (an authored inline-table alias, or a parser-minted private alias when SQL omitted one).
func (*LogicalInlineValues) Children ¶
func (*LogicalInlineValues) Children() []LogicalOperator
func (*LogicalInlineValues) CollectionValue ¶
func (v *LogicalInlineValues) CollectionValue() values.Value
func (*LogicalInlineValues) Explain ¶
func (v *LogicalInlineValues) Explain(indent string) string
func (*LogicalInlineValues) ResultType ¶
func (v *LogicalInlineValues) ResultType() values.Type
type LogicalInsert ¶
type LogicalInsert struct {
Table string
Columns []string
Source LogicalOperator
ValuesArray values.Value
}
LogicalInsert describes an INSERT into Table. Source is the row- producing child (a SELECT); Columns is the projected-column list (may be empty to mean "all columns").
ValuesArray holds the literal VALUES rows as a Cascades array Value (an ArrayConstructorValue of one RecordConstructorValue per row), mutually exclusive with Source. The translator wraps it in an ExplodeExpression so INSERT … VALUES streams through the same Cascades path as INSERT … SELECT — matching Java's RecordConstructorValue → array → Explode → Insert shape. It is a typed Value rather than a child operator because the rows are built from evaluated literals (parameters are already substituted at plan time), which needs the connection's evaluation context the pure logical builder lacks.
func NewInsert ¶
func NewInsert(table string, cols []string, source LogicalOperator) *LogicalInsert
func (*LogicalInsert) Children ¶
func (i *LogicalInsert) Children() []LogicalOperator
func (*LogicalInsert) Explain ¶
func (i *LogicalInsert) Explain(indent string) string
type LogicalJoin ¶
type LogicalJoin struct {
Left LogicalOperator
Right LogicalOperator
Kind JoinKind
OnText string
OnPredicate any // predicates.QueryPredicate when set
// OnExistsSubqueries carries EXISTS subqueries lifted from the ON clause
// (RFC-154 §5). The cascades translator turns each into an existential
// quantifier on the join's SelectExpression, so the NLJ rule's
// the existential peel path builds the semi-join. Only populated for
// INNER joins (OUTER EXISTS-in-ON is deferred — RFC-154 §5.2b).
OnExistsSubqueries []ExistsSubquery
}
LogicalJoin combines two children. Empty OnText means "no ON condition" (comma cross-join form — the outer WHERE provides the predicate). OnPredicate is the optional structured form (used by the catalog-aware walker); when non-nil, it takes precedence over OnText for Cascades lowering.
func NewJoin ¶
func NewJoin(left, right LogicalOperator, kind JoinKind, on string) *LogicalJoin
func NewJoinWithPredicate ¶
func NewJoinWithPredicate(left, right LogicalOperator, kind JoinKind, pred any) *LogicalJoin
NewJoinWithPredicate builds a LogicalJoin with a structured ON predicate.
func (*LogicalJoin) Children ¶
func (j *LogicalJoin) Children() []LogicalOperator
func (*LogicalJoin) Explain ¶
func (j *LogicalJoin) Explain(indent string) string
type LogicalLimit ¶
type LogicalLimit struct {
Input LogicalOperator
Limit int64
Offset int64
LimitValue values.Value
}
LogicalLimit caps the row count, optionally after skipping Offset. Negative Limit means "no limit" (pure offset).
LimitValue is an OPTIONAL runtime row cap (RFC-156 parameterized vector rank limit `... <= ?`): when non-nil the cap is evaluated at execution against the bound parameters and Limit is the no-cap sentinel (-1).
func NewLimit ¶
func NewLimit(input LogicalOperator, limit, offset int64) *LogicalLimit
func NewRuntimeLimit ¶
func NewRuntimeLimit(input LogicalOperator, limitValue values.Value, offset int64) *LogicalLimit
NewRuntimeLimit builds a LIMIT whose row cap is a runtime Value (evaluated at execution). The static Limit is the no-cap sentinel (-1).
func (*LogicalLimit) Children ¶
func (l *LogicalLimit) Children() []LogicalOperator
func (*LogicalLimit) Explain ¶
func (l *LogicalLimit) Explain(indent string) string
type LogicalOperator ¶
type LogicalOperator interface {
// Children returns the immediate child operators. Returning an
// empty slice (not nil) for leaf nodes keeps caller code free
// of nil checks.
Children() []LogicalOperator
// Explain returns an indented textual rendering of this node
// and its subtree. The indent argument is prefixed to this
// node's first line; children receive indent + " ".
//
// This is the stable surface Plan.Explain() exposes to
// frontend callers. Cascades physical plans will Explain()
// through their own impl; logical plans use this.
Explain(indent string) string
}
LogicalOperator is the root interface every logical operator satisfies. A LogicalOperator exposes its children (tree structure) and can render an indented text explanation of the subtree.
Operators are value-types (small, immutable once constructed). They are NOT identity-comparable — two structurally-identical Filter nodes should compare equal under a structural walker (implemented on top of this interface, not part of it).
func AttachedPlans ¶
func AttachedPlans(op LogicalOperator) []LogicalOperator
AttachedPlans returns the SUBQUERY plans attached to op beyond Children(): EXISTS/scalar plans on filters, projections, aggregates (HAVING) and joins (ON). Tree walkers that must see EVERY built operator (e.g. the CTE alias-arity validator) traverse Children() + AttachedPlans(). Keep this switch in lockstep with the attachment fields on the structs above — a new attachment field added without an arm here silently escapes whole-tree validation.
func FoldTransparentUnaryInput ¶
func FoldTransparentUnaryInput(op LogicalOperator) (LogicalOperator, bool)
FoldTransparentUnaryInput returns (input, true) when op is a fold-transparent unary operator — one the RFC-141 projected-EXISTS fold descends THROUGH to reach the existential filter without changing the row shape. Only Sort and Limit qualify (a Project/Join/Aggregate/Distinct/Union reshapes the rows and is NOT fold-transparent). This is the SINGLE source of truth for the transparency set: both the translator's `findExistsFilterUnderUnaryChain` (which folds the projection through the chain) and the generator's `existsFilterReachableForFold` (which rejects a projected EXISTS the fold cannot reach) consult it, so the two can never silently diverge. Returns (nil, false) for any non-transparent op.
type LogicalProject ¶
type LogicalProject struct {
Input LogicalOperator
Projections []string
Aliases []string // parallel to Projections; "" means no alias
ProjectedValues []values.Value // parallel to Projections; nil slot = walker declined
IsComputed []bool // parallel to Projections; true = expression, not plain column ref
// AliasMinted is parallel to Aliases: true = the alias in that slot was
// written by the MACHINERY, not by the user's `AS`. It is the provenance of
// the name, carried, because the name's SHAPE cannot carry it: the
// duplicated-bare-leaf dedup pins a projected reference's QUALIFIED
// spelling ("A.K") as the alias so two same-named datum keys stay
// distinguishable in the executor's row map, and a user is equally free to
// write `AS "A.K"`. A consumer that decides between them by inspecting the
// string gets the user's alias wrong, which is how `SELECT u.name AS
// "U.NAME"` came to report the label NAME.
//
// Java needs no such marker because its internal column for a duplicate is
// ANONYMOUS (Expressions.java:268-287) while the label is untouched; where
// it genuinely needs two names for one column it carries a SEPARATE
// construction-time field (Type.Record.Field.fieldStorageNameOptional,
// Type.java:2826-2827,2897-2899) rather than recovering provenance from a
// spelling later. Go's executor keys its row map by name, so the qualified
// key must stay — deleting it returns ONE column for `SELECT a.k, b.k` —
// and the provenance rides beside it instead.
//
// nil (or a short slice) reads as "user alias" for every slot it does not
// cover: a machinery mint is the exceptional case and states itself.
AliasMinted []bool
// AliasSources is parallel to AliasMinted. A present entry is the
// STRUCTURED authored source identity captured at the instant the machinery
// minted that slot's alias. It is not the later physical Value owner: a
// projection Value can be reanchored onto `_current` while the alias remains
// the datum key minted from source A. A short/nil vector means the source was
// not captured; consumers must never reconstruct it from alias text.
AliasSources []values.ProjectionAliasSource
// AggregateOutputOrdinals is the exact native [group keys..., aggregate
// calls...] input slot for each post-aggregate projection item. A negative
// entry marks a computed item whose Value tree is bound separately. nil
// means this is not the SQL aggregate-output boundary. Keeping this
// identity separate from Projections/Aliases prevents a group key and an
// aggregate alias with the same spelling from being rebound by name.
AggregateOutputOrdinals []int
// InputOrdinals is a complete positional projection contract for machinery
// boundaries that are created before their input quantifier exists (for
// example a CTE column-list rename). The logical node deliberately leaves
// the corresponding ProjectedValues nil; cascades translation resolves each
// slot against the real input quantifier's exact flowed object value. nil
// means no such positional boundary.
InputOrdinals []int
// AggregateSlots is parallel to Projections; true = the slot's value tree
// CONTAINS an aggregate. Captured pre-rewrite, where the *AggregateValue
// node is still present (rewriteAggregateValuesInTree destructively replaces
// it with a typed FieldValue). Read once by the INSERT…SELECT promotion guard
// to identify reliably-typed aggregate-result columns — plain columns are
// concrete-typed too (ResolveIdentifier), so type-presence cannot
// discriminate. A bridge until the Java end-state (PromoteValue projection
// nodes), which dissolves this marker.
AggregateSlots []bool
ScalarSubqueries []ScalarSubquery // uncorrelated scalar subquery plans (pre-evaluated)
// ProjectionRefs is parallel to Projections and carries the parse-tree
// SEGMENTS of a plain column item — the same triple SortKey and GroupKey
// already carry, for the same reason (RFC-197: qualification is FullId
// SEGMENT COUNT, never a scan of the rendered name).
//
// Projections is a RENDERING. `A.B` written as a qualified reference and
// `"A.B"` written as one quoted identifier are the same bytes, so a consumer
// that recovers the qualifier by slicing at the first dot cannot tell a
// reference to source A from a column literally so named — it manufactures a
// qualifier out of a name and resolves against the wrong row. The segments
// are what the parser actually saw, so they decide it.
//
// A zero entry means "no segments": a computed item, a sentinel slot, or a
// producer that has not been taught to capture them. Consumers must treat
// that as "unknown", never as "not qualified" — the ONLY safe reading of an
// absent segment triple is to fall back to whatever the rendered name
// supported before.
ProjectionRefs []ColumnRef
}
LogicalProject selects / renames columns and computes expressions. Each element of Projections is the canonical text of the projected expression or column name. Aliases (parallel slice) hold the output name; empty string means "use the underlying name."
ProjectedValues (parallel to Projections) carries resolved Value trees when the catalog-aware builder successfully walks the ANTLR expression. nil slots mean the walker declined (unsupported shape) — the Cascades translator treats nil as "cannot translate" and returns nil for the whole query. Non-nil slots are used directly as projection Values in the Cascades plan.
func NewProject ¶
func NewProject(input LogicalOperator, projs, aliases []string) *LogicalProject
func (*LogicalProject) Children ¶
func (p *LogicalProject) Children() []LogicalOperator
func (*LogicalProject) Explain ¶
func (p *LogicalProject) Explain(indent string) string
type LogicalScan ¶
type LogicalScan struct {
Table string
Alias string
// Binding is the scan's binding correlation name when its FROM alias
// DUPLICATES an earlier leg's at the same level ("" = the alias binds —
// every non-duplicate leg). Carried from the
// parser's single mint authority (assignFromLegBindingIDs); consumers
// read it via sourceBinding and never re-derive binding identity from
// the alias. The SQL alias stays the DISPLAY qualifier (sourceAlias).
Binding string
}
LogicalScan reads a single table. Empty Alias means "use the table name as the source alias."
func (*LogicalScan) Children ¶
func (*LogicalScan) Children() []LogicalOperator
func (*LogicalScan) Explain ¶
func (s *LogicalScan) Explain(indent string) string
type LogicalSort ¶
type LogicalSort struct {
Input LogicalOperator
Keys []SortKey
}
LogicalSort sorts its child rows by the given keys.
func NewSort ¶
func NewSort(input LogicalOperator, keys []SortKey) *LogicalSort
func (*LogicalSort) Children ¶
func (s *LogicalSort) Children() []LogicalOperator
func (*LogicalSort) Explain ¶
func (s *LogicalSort) Explain(indent string) string
type LogicalUnion ¶
type LogicalUnion struct {
Inputs []LogicalOperator
Distinct bool
}
LogicalUnion ties together two (or more) children with UNION [ALL] semantics. Distinct = true applies a DISTINCT dedup across the union.
func NewUnion ¶
func NewUnion(inputs []LogicalOperator, distinct bool) *LogicalUnion
func (*LogicalUnion) Children ¶
func (u *LogicalUnion) Children() []LogicalOperator
func (*LogicalUnion) Explain ¶
func (u *LogicalUnion) Explain(indent string) string
type LogicalUnnest ¶
type LogicalUnnest struct {
// Segments is the un-flattened dotted name of the array source
// (`["T1","ARR1"]` for `T1.arr1`). Segment 0 names the in-scope outer
// source; the remaining segments name the array field on it. Kept
// un-flattened (no re-split of a joined string) so the translator
// resolves segment-by-segment against the scope.
Segments []string
// Binding carries the comma source's duplicate-alias binding id
// (see LogicalScan.Binding) so the
// TABLE-FIRST demotion (demoteSchemaQualifiedUnnest) can restore it on
// the demoted LogicalScan — a mis-classified schema-qualified TABLE leg
// is a table leg for binding purposes. A GENUINE unnest never consumes
// it: a duplicate unnest AS/AT alias is rejected outright (RFC-142).
Binding string
// Alias is the AS alias (`x` in `... AS x`) bound to each unnested
// element. Empty when the AS alias is omitted (AT-only form).
Alias string
// AtAlias is the AT ordinal alias (`ord` in `... AT ord`), empty when
// absent. Its presence makes the Explode WITH ORDINALITY.
AtAlias string
// of a correlated subquery (`EXISTS (SELECT ... FROM R.TAGS AS E)`). It is
// the already-resolved array FieldValue over the outer correlation. A
// regular lateral FROM leg gets that value from the LogicalJoin on its left
// and leaves this nil. Carrying the resolved Value preserves minted outer
// correlations and avoids resolving the owner a second time.
CorrelatedCollection values.Value
}
LogicalUnnest is a lateral array UNNEST source in the FROM list (`FROM t, t.arr AS x [AT ord]`). It is the RIGHT child of a lateral LogicalJoin whose LEFT child is the source `t` it correlates to. The translator lowers it to an Explode of the correlated array field under a FlatMap of the outer source. Mirrors Java's `LogicalOperator.generateCorrelatedFieldAccess`. RFC-142 R5.
func FindOwnerUnnest ¶
func FindOwnerUnnest(op LogicalOperator, alias string) *LogicalUnnest
FindOwnerUnnest returns the LogicalUnnest in the outer sub-plan `op` whose element (AS) alias matches `alias` — the OWNER of a CHAINED lateral unnest (`FROM t, t.arr AS x, x.sub AS y`: the second unnest's segment-0 `x` names the first unnest's element). Mirrors FindOuterScanTable's walk (does not descend into a CTE/derived Body — a derived table is its own FROM scope, so a chain rooted inside a derived body is out of scope), but matches unnest element aliases instead of scans. nil when no such prior unnest.
The AT ordinal alias (`t.arr AS x AT o`) is deliberately NOT matched: `o` binds a scalar INTEGER ordinal, not a struct, so `o.sub` can never be a valid chain root. Matching it would misclassify `o.sub` as owned by this unnest and resolve `sub` against the ELEMENT's descriptor — emitting a plan that silently returns zero rows where Java rejects `o.sub` (a field access on a scalar). Only the AS element alias carries a chainable struct.
func (*LogicalUnnest) Children ¶
func (*LogicalUnnest) Children() []LogicalOperator
func (*LogicalUnnest) Explain ¶
func (u *LogicalUnnest) Explain(indent string) string
type LogicalUpdate ¶
type LogicalUpdate struct {
Target string
Sets []Assignment
Input LogicalOperator // the scan + filter producing target rows
}
LogicalUpdate updates Target rows matching Input with the per-col expression assignments in Sets.
func NewUpdate ¶
func NewUpdate(target string, sets []Assignment, input LogicalOperator) *LogicalUpdate
func (*LogicalUpdate) Children ¶
func (u *LogicalUpdate) Children() []LogicalOperator
func (*LogicalUpdate) Explain ¶
func (u *LogicalUpdate) Explain(indent string) string
type LogicalValues ¶
LogicalValues is a leaf operator that yields a single row of constant/expression projections — the canonical target for a SELECT without a FROM clause (`SELECT 1 + 2, 'hello'`). Rows is a list of expression-texts per output column; Aliases is parallel (empty string = no AS clause). The number of rows is always 1 in this seed; a future VALUES (…), (…) literal table would extend to multi-row. Java equivalent: a ConstantExpression flowing through LogicalProjectionExpression.
func NewValues ¶
func NewValues(rows, aliases []string) *LogicalValues
NewValues constructs a LogicalValues with per-column expression text + parallel aliases.
func (*LogicalValues) Children ¶
func (*LogicalValues) Children() []LogicalOperator
func (*LogicalValues) Explain ¶
func (v *LogicalValues) Explain(indent string) string
type ScalarSubquery ¶
type ScalarSubquery struct {
Alias values.CorrelationIdentifier
Plan LogicalOperator
}
ScalarSubquery pairs a correlation alias with the logical plan for a scalar subquery `(SELECT MAX(v) FROM t2)`. Carried on LogicalFilter and LogicalProject so the Cascades translator can build inner plans. The executor pre-evaluates these and binds the scalar result under Alias before evaluating the outer plan's predicates/projections.
type SortKey ¶
type SortKey struct {
Expr string // canonical text
Dir SortDir
NullsFirst bool
Value values.Value // resolved Value expression (nil = use text as FieldValue)
// Pos is the 1-based SELECT-list position for a positional key
// (`ORDER BY <n>`); 0 = not positional. A positional key IS an output
// ordinal by SQL definition, so the translator bakes it directly to the
// projection's output slot — no text-rendering round-trip, which
// diverges for computed items whose canonical source text differs from the
// baked output spelling.
Pos int
// AggregateOutputOrdinal is an exact address into the grouped input's
// native [keys..., calls...] row. The bool distinguishes native slot zero
// from an unset field. It is used when the visible aggregate Project is
// deliberately above ORDER BY.
AggregateOutputOrdinal int
HasAggregateOutputOrdinal bool
// AggregateOutputValueExact marks Value as already structurally bound to
// the native aggregate row (including a computed tree whose leaves are
// native ordinals). Generic sort rebasing must not overwrite it by name.
AggregateOutputValueExact bool
// Bare/Qualifier/Qualified: parse-tree segments of a plain column
// reference key; zero values for positional and expression keys (their
// Expr is a rendering only). Qualification is FullId SEGMENT COUNT.
Bare string
Qualifier string
Qualified bool
// Segs is the FULL ordered segment list of the reference (`a.n.sk` ->
// [A N SK]). Resolution consumes Segs; Qualifier is a rendering that
// cannot say where one segment ends and the next begins, so a deeper
// reference resolved through it looks for a source named "A.N".
Segs []string
// BareRef marks a key whose source text is a plain ONE-segment column
// reference — the only shape SQL binds to an output alias. False for
// qualified references (`ORDER BY d.x`), aggregates and computed
// expressions: their canonical renderings ("X" after qualifier
// stripping, "SUM(S.SCORE)") are indistinguishable from a delimited
// alias's spelling, so alias-binding passes REQUIRE BareRef instead of
// inspecting Expr text.
BareRef bool
}
SortKey is one ORDER BY entry.
type TraversalOrder ¶
type TraversalOrder int
const ( TraversalAnyOrder TraversalOrder = iota TraversalLevelOrder TraversalPreOrder TraversalPostOrder )
The zero value is ANY — a recursion with NO traversal clause leaves the order to the planner (Java TraversalStrategy.ANY: both the level union and the DFS join implement, the cost model picks). An EXPLICIT `TRAVERSAL ORDER level_order` is TraversalLevelOrder and PINS the level union (Java's LEVEL gates the DFS rule off) — the two were once conflated, which made the explicit clause unable to force the plan.