001package gudusoft.gsqlparser.resolver2.model;
002
003import gudusoft.gsqlparser.TSourceToken;
004import gudusoft.gsqlparser.nodes.TObjectName;
005
006/**
007 * Column reference record - preserves original AST node information for traceability.
008 *
009 * <p>When multiple syntactically different identifiers refer to the same semantic column
010 * (e.g., "Column1" and column1 in Oracle), this class allows tracing back from
011 * the normalized column name to all original occurrences in the SQL text.</p>
012 *
013 * <p>Example: For SQL {@code SELECT "Column1", column1 FROM dual}:</p>
014 * <ul>
015 *   <li>Both references resolve to the same column COLUMN1</li>
016 *   <li>Each creates a separate ColumnReference with its original text and position</li>
017 *   <li>The ColumnSourceWithReferences holds both references</li>
018 * </ul>
019 *
020 * @since 3.1.0.9
021 */
022public class ColumnReference {
023
024    /** Original AST node */
025    private final TObjectName objectName;
026
027    /** Original text as it appears in SQL (e.g., "Column1" or column1) */
028    private final String originalText;
029
030    /** Start offset in SQL text */
031    private final int startOffset;
032
033    /** End offset in SQL text */
034    private final int endOffset;
035
036    /** Line number (1-based) */
037    private final int lineNumber;
038
039    /** Column number (1-based) */
040    private final int columnNumber;
041
042    /**
043     * Create a column reference from a TObjectName AST node.
044     *
045     * @param objectName the original AST node
046     */
047    public ColumnReference(TObjectName objectName) {
048        this.objectName = objectName;
049        this.originalText = objectName != null ? objectName.toString() : null;
050
051        // Extract position information from token
052        if (objectName != null && objectName.getStartToken() != null) {
053            TSourceToken startToken = objectName.getStartToken();
054            this.startOffset = (int) startToken.offset;
055            this.lineNumber = (int) startToken.lineNo;
056            this.columnNumber = (int) startToken.columnNo;
057
058            TSourceToken endToken = objectName.getEndToken();
059            this.endOffset = endToken != null ? (int) (endToken.offset + endToken.astext.length()) : startOffset;
060        } else {
061            this.startOffset = -1;
062            this.endOffset = -1;
063            this.lineNumber = -1;
064            this.columnNumber = -1;
065        }
066    }
067
068    /**
069     * Create a column reference with explicit values (for testing or special cases).
070     *
071     * @param objectName the original AST node (may be null)
072     * @param originalText the original text
073     * @param startOffset start offset
074     * @param endOffset end offset
075     * @param lineNumber line number
076     * @param columnNumber column number
077     */
078    public ColumnReference(TObjectName objectName, String originalText,
079                          int startOffset, int endOffset,
080                          int lineNumber, int columnNumber) {
081        this.objectName = objectName;
082        this.originalText = originalText;
083        this.startOffset = startOffset;
084        this.endOffset = endOffset;
085        this.lineNumber = lineNumber;
086        this.columnNumber = columnNumber;
087    }
088
089    /**
090     * Get the original AST node.
091     *
092     * @return the TObjectName node, or null if not available
093     */
094    public TObjectName getObjectName() {
095        return objectName;
096    }
097
098    /**
099     * Get the original text as it appears in SQL.
100     *
101     * @return original text (e.g., "Column1" with quotes, or column1 without)
102     */
103    public String getOriginalText() {
104        return originalText;
105    }
106
107    /**
108     * Get the start offset in the SQL text.
109     *
110     * @return start offset (0-based), or -1 if not available
111     */
112    public int getStartOffset() {
113        return startOffset;
114    }
115
116    /**
117     * Get the end offset in the SQL text.
118     *
119     * @return end offset (0-based, exclusive), or -1 if not available
120     */
121    public int getEndOffset() {
122        return endOffset;
123    }
124
125    /**
126     * Get the line number.
127     *
128     * @return line number (1-based), or -1 if not available
129     */
130    public int getLineNumber() {
131        return lineNumber;
132    }
133
134    /**
135     * Get the column number.
136     *
137     * @return column number (1-based), or -1 if not available
138     */
139    public int getColumnNumber() {
140        return columnNumber;
141    }
142
143    /**
144     * Check if position information is available.
145     *
146     * @return true if position information is valid
147     */
148    public boolean hasPositionInfo() {
149        return startOffset >= 0 && lineNumber >= 0;
150    }
151
152    @Override
153    public String toString() {
154        if (hasPositionInfo()) {
155            return String.format("%s @ line %d, col %d (offset %d-%d)",
156                originalText, lineNumber, columnNumber, startOffset, endOffset);
157        }
158        return originalText != null ? originalText : "<null>";
159    }
160
161    @Override
162    public boolean equals(Object o) {
163        if (this == o) return true;
164        if (o == null || getClass() != o.getClass()) return false;
165        ColumnReference that = (ColumnReference) o;
166        // Two references are equal if they point to the same location
167        return startOffset == that.startOffset &&
168               lineNumber == that.lineNumber &&
169               columnNumber == that.columnNumber;
170    }
171
172    @Override
173    public int hashCode() {
174        int result = startOffset;
175        result = 31 * result + lineNumber;
176        result = 31 * result + columnNumber;
177        return result;
178    }
179}