001package gudusoft.gsqlparser.ir.semantic;
002
003import gudusoft.gsqlparser.common.structured.StructuredColumnPath;
004import gudusoft.gsqlparser.ir.semantic.joinanalysis.ColumnResolution;
005
006import java.util.Objects;
007
008/**
009 * In-statement reference to a column. Holds the FROM-clause alias and the
010 * column name as they appear in the SQL — <i>not</i> the final physical
011 * table. Physical/provenance information lives on {@link RelationSource}'s
012 * {@link gudusoft.gsqlparser.ir.semantic.binding.RelationBinding} so that
013 * later CTE/subquery slices can describe provenance separately without
014 * mutating this shape.
015 *
016 * <p>An optional {@link StructuredColumnPath} carries a typed path into a
017 * structured column (e.g. {@code nodes[*].key}) produced by a
018 * structured-dataflow adapter such as Spark {@code from_json}+{@code explode}.
019 * Existing constructors leave it null for full backward compatibility.
020 *
021 * <p>Join-analysis slice 164 (S3) adds an optional {@link SourceSpan}
022 * pointing at the source-text reference this column was read from; slice
023 * 165 (S4) adds an optional {@link ColumnResolution} surfacing the
024 * resolved base relation (GAP 5). Both are <strong>excluded</strong> from
025 * {@link #equals}/{@link #hashCode} so that set/map dedupe and golden
026 * ordering are unaffected — two refs that differ only in span or
027 * resolution remain equal. Existing constructors default them to
028 * {@code null}.
029 *
030 * <p><b>API status:</b> read-only consumption of analyzer-produced column
031 * references is part of Join Analysis Consumption Profile v1. Constructors
032 * are producer-oriented and are outside that profile.
033 */
034public final class ColumnRef {
035
036    private final String relationAlias;
037    private final String columnName;
038    private final StructuredColumnPath structuredPath;
039    private final SourceSpan sourceSpan;
040    private final ColumnResolution resolution;
041
042    public ColumnRef(String relationAlias, String columnName) {
043        this(relationAlias, columnName, null, null, null);
044    }
045
046    public ColumnRef(String relationAlias, String columnName, StructuredColumnPath structuredPath) {
047        this(relationAlias, columnName, structuredPath, null, null);
048    }
049
050    /** Slice 164 — adds the optional source span. */
051    public ColumnRef(String relationAlias, String columnName,
052                     StructuredColumnPath structuredPath, SourceSpan sourceSpan) {
053        this(relationAlias, columnName, structuredPath, sourceSpan, null);
054    }
055
056    /**
057     * Full constructor (join-analysis slice 165). {@code sourceSpan} and
058     * {@code resolution} are optional (null when the parser cannot anchor
059     * the reference / when no resolution was attempted) and both are
060     * excluded from identity.
061     */
062    public ColumnRef(String relationAlias, String columnName,
063                     StructuredColumnPath structuredPath, SourceSpan sourceSpan,
064                     ColumnResolution resolution) {
065        if (relationAlias == null || relationAlias.isEmpty()) {
066            throw new IllegalArgumentException("relationAlias must be non-empty");
067        }
068        if (columnName == null || columnName.isEmpty()) {
069            throw new IllegalArgumentException("columnName must be non-empty");
070        }
071        this.relationAlias = relationAlias;
072        this.columnName = columnName;
073        this.structuredPath = structuredPath;
074        this.sourceSpan = sourceSpan;
075        this.resolution = resolution;
076    }
077
078    public String getRelationAlias() {
079        return relationAlias;
080    }
081
082    public String getColumnName() {
083        return columnName;
084    }
085
086    public StructuredColumnPath getStructuredPath() {
087        return structuredPath;
088    }
089
090    /**
091     * Optional source-text span of this column reference (join-analysis
092     * slice 164). Null when the parser cannot anchor it. Excluded from
093     * {@link #equals}/{@link #hashCode}.
094     */
095    public SourceSpan getSourceSpan() {
096        return sourceSpan;
097    }
098
099    /**
100     * Optional resolution of this column to its base relation / final
101     * table (join-analysis slice 165, GAP 5). Null when no resolution was
102     * attempted (e.g. synthetic merged-key refs); otherwise carries an
103     * explicit {@link ColumnResolution} (RESOLVED with a final-table name,
104     * or UNRESOLVED). Excluded from {@link #equals}/{@link #hashCode}.
105     */
106    public ColumnResolution getResolution() {
107        return resolution;
108    }
109
110    @Override
111    public boolean equals(Object o) {
112        if (this == o) return true;
113        if (!(o instanceof ColumnRef)) return false;
114        ColumnRef other = (ColumnRef) o;
115        // sourceSpan and resolution intentionally excluded — see class javadoc.
116        return relationAlias.equals(other.relationAlias)
117                && columnName.equals(other.columnName)
118                && Objects.equals(structuredPath, other.structuredPath);
119    }
120
121    @Override
122    public int hashCode() {
123        // sourceSpan and resolution intentionally excluded — see class javadoc.
124        return Objects.hash(relationAlias, columnName, structuredPath);
125    }
126
127    @Override
128    public String toString() {
129        if (structuredPath != null) {
130            return relationAlias + "." + structuredPath.toDisplayString();
131        }
132        return relationAlias + "." + columnName;
133    }
134}