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    private final String expressionText;
050    private final SourceSpan expressionSpan;
051
052    /**
053     * Backwards-compatible constructor delegating to the 5-arg form with
054     * {@code windowSpec=null}. Slice 13 added the 5-arg constructor; this
055     * overload preserves source-compatibility for downstream callers that
056     * were already using {@link OutputColumn} from a published version of
057     * the library.
058     */
059    public OutputColumn(String name, boolean derived, boolean aggregate,
060                        List<ColumnRef> sources) {
061        this(name, derived, aggregate, sources, null);
062    }
063
064    public OutputColumn(String name, boolean derived, boolean aggregate,
065                        List<ColumnRef> sources, WindowSpec windowSpec) {
066        this(name, derived, aggregate, sources, windowSpec, null, null);
067    }
068
069    /**
070     * Full constructor (US-021a). {@code expressionText} and
071     * {@code expressionSpan} are optional projection-expression provenance
072     * (see {@link #getExpressionText()} / {@link #getExpressionSpan()});
073     * both default to {@code null} via the shorter overloads, preserving
074     * full behavioural compatibility for every existing caller.
075     */
076    public OutputColumn(String name, boolean derived, boolean aggregate,
077                        List<ColumnRef> sources, WindowSpec windowSpec,
078                        String expressionText, SourceSpan expressionSpan) {
079        if (name == null || name.isEmpty()) {
080            throw new IllegalArgumentException("name must be non-empty");
081        }
082        if (sources == null) {
083            throw new IllegalArgumentException("sources must not be null");
084        }
085        if (windowSpec != null && aggregate) {
086            throw new IllegalArgumentException(
087                    "windowSpec is non-null but aggregate=true; window functions are "
088                            + "row-preserving and must carry aggregate=false");
089        }
090        this.name = name;
091        this.derived = derived;
092        this.aggregate = aggregate;
093        this.sources = Collections.unmodifiableList(new ArrayList<>(sources));
094        this.windowSpec = windowSpec;
095        this.expressionText = expressionText;
096        this.expressionSpan = expressionSpan;
097    }
098
099    /**
100     * Copy of this output column with the projection-expression provenance
101     * attached (US-021a). Every other field is preserved verbatim. Returns
102     * {@code this} when both arguments are {@code null} (nothing to attach).
103     */
104    public OutputColumn withExpression(String expressionText, SourceSpan expressionSpan) {
105        if (expressionText == null && expressionSpan == null) {
106            return this;
107        }
108        return new OutputColumn(name, derived, aggregate, sources, windowSpec,
109                expressionText, expressionSpan);
110    }
111
112    public String getName() {
113        return name;
114    }
115
116    /** {@code true} when the projection is an expression rather than a direct column reference. */
117    public boolean isDerived() {
118        return derived;
119    }
120
121    /**
122     * {@code true} when the projection is an aggregate function call
123     * (COUNT, SUM, AVG, MIN, MAX, etc.). Always implies {@link #isDerived()}.
124     * Always {@code false} when {@link #getWindowSpec()} is non-null
125     * (window functions are row-preserving).
126     */
127    public boolean isAggregate() {
128        return aggregate;
129    }
130
131    public List<ColumnRef> getSources() {
132        return sources;
133    }
134
135    /**
136     * Per-output analytic dependencies (slice 13). Non-null iff this
137     * projection is a window function (e.g. {@code FUNC(arg) OVER (...)});
138     * null otherwise.
139     */
140    public WindowSpec getWindowSpec() {
141        return windowSpec;
142    }
143
144    /**
145     * Original source text of the projection expression this output was
146     * built from (US-021a), reconstructed from the parser's token stream so
147     * multi-line expressions keep their newlines. {@code null} when no
148     * single projection expression exists for this output — e.g. star
149     * expansion (one {@code *} produces many outputs), synthetic outputs
150     * (predicate-body scaffolding, UPDATE SET shells), or columns built by
151     * paths that have not been wired yet. Null-safe: never throws.
152     */
153    public String getExpressionText() {
154        return expressionText;
155    }
156
157    /**
158     * Source span of the projection expression (US-021a), half-open per
159     * {@link SourceSpan}. {@code null} under the same conditions as
160     * {@link #getExpressionText()}, and additionally when the expression's
161     * boundary tokens cannot anchor a span. Null-safe: never throws.
162     */
163    public SourceSpan getExpressionSpan() {
164        return expressionSpan;
165    }
166}