001package gudusoft.gsqlparser.ir.semantic;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.List;
006
007/**
008 * One projected column in the SELECT list. {@link #sources} is the list of
009 * input columns this output depends on; {@link #derived} distinguishes a
010 * direct column reference (e.g. {@code SELECT id}) from a computed
011 * expression (e.g. {@code SELECT salary*2 AS doubled} or
012 * {@code SELECT a.x + a.y}); {@link #aggregate} marks the output as the
013 * result of an aggregate function call (e.g. {@code COUNT(*)},
014 * {@code SUM(salary)}); {@link #windowSpec} (slice 13) carries
015 * PARTITION BY / OVER ORDER BY refs when the projection is a window
016 * function.
017 *
018 * <p>Slice 4 introduced {@code derived}; slice 6 added {@code aggregate}
019 * and lifted the empty-sources restriction for aggregates only (e.g.
020 * {@code COUNT(*)} has no column refs); slice 13 added {@code windowSpec}
021 * and lifts the empty-sources restriction for window functions
022 * (e.g. {@code ROW_NUMBER() OVER (...)} has no column refs).
023 *
024 * <p><b>Note on flag propagation:</b> {@code derived}, {@code aggregate},
025 * and {@code windowSpec} are local to a single statement's output. They
026 * are <i>not</i> propagated transitively. If a CTE defines
027 * {@code total = SUM(salary)}, that CTE's output has
028 * {@code aggregate=true}; an outer query that re-projects the CTE's
029 * {@code total} column ({@code SELECT total FROM cte}) is a direct
030 * reference and so the outer's output has {@code aggregate=false} (and
031 * {@code derived=false}). Consumers that need a transitive view should
032 * walk {@link gudusoft.gsqlparser.ir.semantic.SemanticProgram#getLineage()}
033 * and inspect each upstream {@code OutputColumn}.
034 *
035 * <p><b>Window-function invariant (slice 13):</b> when {@code windowSpec
036 * != null}, {@code aggregate} MUST be {@code false}. Window functions
037 * are row-preserving (analytic), not row-collapsing, so the per-output
038 * aggregate flag stays false even when the window function is a name
039 * that overlaps with an aggregate (e.g. {@code AVG(salary) OVER (...)}).
040 * The constructor enforces this invariant.
041 */
042public final class OutputColumn {
043
044    private final String name;
045    private final boolean derived;
046    private final boolean aggregate;
047    private final List<ColumnRef> sources;
048    private final WindowSpec windowSpec;
049
050    /**
051     * Backwards-compatible constructor delegating to the 5-arg form with
052     * {@code windowSpec=null}. Slice 13 added the 5-arg constructor; this
053     * overload preserves source-compatibility for downstream callers that
054     * were already using {@link OutputColumn} from a published version of
055     * the library.
056     */
057    public OutputColumn(String name, boolean derived, boolean aggregate,
058                        List<ColumnRef> sources) {
059        this(name, derived, aggregate, sources, null);
060    }
061
062    public OutputColumn(String name, boolean derived, boolean aggregate,
063                        List<ColumnRef> sources, WindowSpec windowSpec) {
064        if (name == null || name.isEmpty()) {
065            throw new IllegalArgumentException("name must be non-empty");
066        }
067        if (sources == null) {
068            throw new IllegalArgumentException("sources must not be null");
069        }
070        if (windowSpec != null && aggregate) {
071            throw new IllegalArgumentException(
072                    "windowSpec is non-null but aggregate=true; window functions are "
073                            + "row-preserving and must carry aggregate=false");
074        }
075        this.name = name;
076        this.derived = derived;
077        this.aggregate = aggregate;
078        this.sources = Collections.unmodifiableList(new ArrayList<>(sources));
079        this.windowSpec = windowSpec;
080    }
081
082    public String getName() {
083        return name;
084    }
085
086    /** {@code true} when the projection is an expression rather than a direct column reference. */
087    public boolean isDerived() {
088        return derived;
089    }
090
091    /**
092     * {@code true} when the projection is an aggregate function call
093     * (COUNT, SUM, AVG, MIN, MAX, etc.). Always implies {@link #isDerived()}.
094     * Always {@code false} when {@link #getWindowSpec()} is non-null
095     * (window functions are row-preserving).
096     */
097    public boolean isAggregate() {
098        return aggregate;
099    }
100
101    public List<ColumnRef> getSources() {
102        return sources;
103    }
104
105    /**
106     * Per-output analytic dependencies (slice 13). Non-null iff this
107     * projection is a window function (e.g. {@code FUNC(arg) OVER (...)});
108     * null otherwise.
109     */
110    public WindowSpec getWindowSpec() {
111        return windowSpec;
112    }
113}