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}