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 */
030public final class ColumnRef {
031
032    private final String relationAlias;
033    private final String columnName;
034    private final StructuredColumnPath structuredPath;
035    private final SourceSpan sourceSpan;
036    private final ColumnResolution resolution;
037
038    public ColumnRef(String relationAlias, String columnName) {
039        this(relationAlias, columnName, null, null, null);
040    }
041
042    public ColumnRef(String relationAlias, String columnName, StructuredColumnPath structuredPath) {
043        this(relationAlias, columnName, structuredPath, null, null);
044    }
045
046    /** Slice 164 — adds the optional source span. */
047    public ColumnRef(String relationAlias, String columnName,
048                     StructuredColumnPath structuredPath, SourceSpan sourceSpan) {
049        this(relationAlias, columnName, structuredPath, sourceSpan, null);
050    }
051
052    /**
053     * Full constructor (join-analysis slice 165). {@code sourceSpan} and
054     * {@code resolution} are optional (null when the parser cannot anchor
055     * the reference / when no resolution was attempted) and both are
056     * excluded from identity.
057     */
058    public ColumnRef(String relationAlias, String columnName,
059                     StructuredColumnPath structuredPath, SourceSpan sourceSpan,
060                     ColumnResolution resolution) {
061        if (relationAlias == null || relationAlias.isEmpty()) {
062            throw new IllegalArgumentException("relationAlias must be non-empty");
063        }
064        if (columnName == null || columnName.isEmpty()) {
065            throw new IllegalArgumentException("columnName must be non-empty");
066        }
067        this.relationAlias = relationAlias;
068        this.columnName = columnName;
069        this.structuredPath = structuredPath;
070        this.sourceSpan = sourceSpan;
071        this.resolution = resolution;
072    }
073
074    public String getRelationAlias() {
075        return relationAlias;
076    }
077
078    public String getColumnName() {
079        return columnName;
080    }
081
082    public StructuredColumnPath getStructuredPath() {
083        return structuredPath;
084    }
085
086    /**
087     * Optional source-text span of this column reference (join-analysis
088     * slice 164). Null when the parser cannot anchor it. Excluded from
089     * {@link #equals}/{@link #hashCode}.
090     */
091    public SourceSpan getSourceSpan() {
092        return sourceSpan;
093    }
094
095    /**
096     * Optional resolution of this column to its base relation / final
097     * table (join-analysis slice 165, GAP 5). Null when no resolution was
098     * attempted (e.g. synthetic merged-key refs); otherwise carries an
099     * explicit {@link ColumnResolution} (RESOLVED with a final-table name,
100     * or UNRESOLVED). Excluded from {@link #equals}/{@link #hashCode}.
101     */
102    public ColumnResolution getResolution() {
103        return resolution;
104    }
105
106    @Override
107    public boolean equals(Object o) {
108        if (this == o) return true;
109        if (!(o instanceof ColumnRef)) return false;
110        ColumnRef other = (ColumnRef) o;
111        // sourceSpan and resolution intentionally excluded — see class javadoc.
112        return relationAlias.equals(other.relationAlias)
113                && columnName.equals(other.columnName)
114                && Objects.equals(structuredPath, other.structuredPath);
115    }
116
117    @Override
118    public int hashCode() {
119        // sourceSpan and resolution intentionally excluded — see class javadoc.
120        return Objects.hash(relationAlias, columnName, structuredPath);
121    }
122
123    @Override
124    public String toString() {
125        if (structuredPath != null) {
126            return relationAlias + "." + structuredPath.toDisplayString();
127        }
128        return relationAlias + "." + columnName;
129    }
130}