001package gudusoft.gsqlparser.ir.semantic;
002
003import gudusoft.gsqlparser.TCustomSqlStatement;
004import gudusoft.gsqlparser.TSourceToken;
005
006import java.util.Objects;
007import java.util.Optional;
008
009/**
010 * Keeps a statement slice and global {@link SourceSpan} coordinates tied to
011 * the same original SQL text.
012 *
013 * <p>GSP spans use global, 1-based line/column coordinates, while
014 * {@link AnalysisResult#getStatementText()} is trimmed and has one trailing
015 * semicolon removed. Consumers should therefore use {@link #slice(SourceSpan)}
016 * instead of applying a global span directly to the per-statement string.
017 */
018public final class StatementSourceView {
019
020    private final String fullSql;
021    private final String statementText;
022    private final long statementStartOffset;
023    private final long statementStartLine;
024    private final long statementStartColumn;
025
026    private StatementSourceView(String fullSql, String statementText,
027                                long statementStartOffset,
028                                long statementStartLine,
029                                long statementStartColumn) {
030        this.fullSql = fullSql;
031        this.statementText = statementText;
032        this.statementStartOffset = statementStartOffset;
033        this.statementStartLine = statementStartLine;
034        this.statementStartColumn = statementStartColumn;
035    }
036
037    /** Build a view from the original input and one parsed statement. */
038    public static StatementSourceView of(String fullSql, TCustomSqlStatement statement) {
039        Objects.requireNonNull(fullSql, "fullSql");
040        Objects.requireNonNull(statement, "statement");
041
042        TSourceToken startToken = statement.getStartToken();
043        TSourceToken endToken = statement.getEndToken();
044        long startOffset = startToken == null ? -1L : startToken.offset;
045        long startLine = startToken == null ? -1L : startToken.lineNo;
046        long startColumn = startToken == null ? -1L : startToken.columnNo;
047
048        String text = sourceStatementText(fullSql, statement, startToken, endToken);
049        return new StatementSourceView(fullSql, text, startOffset,
050                startLine, startColumn);
051    }
052
053    public String getFullSql() {
054        return fullSql;
055    }
056
057    /**
058     * Return the analyzer-compatible statement text: surrounding whitespace
059     * and one trailing statement separator are removed.
060     */
061    public String getStatementText() {
062        return statementText;
063    }
064
065    /** @return global character offset, or {@code -1} when tokens lack one. */
066    public long getStatementStartOffset() {
067        return statementStartOffset;
068    }
069
070    /** @return global 1-based line, or {@code -1} when unavailable. */
071    public long getStatementStartLine() {
072        return statementStartLine;
073    }
074
075    /** @return global 1-based column, or {@code -1} when unavailable. */
076    public long getStatementStartColumn() {
077        return statementStartColumn;
078    }
079
080    /**
081     * Slice a global half-open span from the original SQL text.
082     *
083     * @return empty when either coordinate is outside the source or the end
084     *         precedes the start
085     */
086    public Optional<String> slice(SourceSpan span) {
087        if (span == null) return Optional.empty();
088        int start = offsetOf(span.getStartLine(), span.getStartColumn());
089        int end = offsetOf(span.getEndLine(), span.getEndColumn());
090        if (start < 0 || end < start) return Optional.empty();
091        return Optional.of(fullSql.substring(start, end));
092    }
093
094    private int offsetOf(long targetLine, long targetColumn) {
095        if (targetLine < 1 || targetColumn < 1) return -1;
096        long line = 1;
097        long column = 1;
098        for (int i = 0; i < fullSql.length(); i++) {
099            if (line == targetLine && column == targetColumn) return i;
100            char ch = fullSql.charAt(i);
101            if (ch == '\n') {
102                line++;
103                column = 1;
104            } else {
105                column++;
106            }
107        }
108        return line == targetLine && column == targetColumn
109                ? fullSql.length() : -1;
110    }
111
112    private static String sourceStatementText(String sql,
113                                              TCustomSqlStatement statement,
114                                              TSourceToken start,
115                                              TSourceToken end) {
116        if (start != null && end != null
117                && start.offset >= 0 && start.offset <= Integer.MAX_VALUE
118                && end.offset >= 0 && end.offset <= Integer.MAX_VALUE) {
119            int startOffset = (int) start.offset;
120            String endText = end.getAstext();
121            long endOffsetLong = end.offset + (endText == null ? 0 : endText.length());
122            if (endOffsetLong <= Integer.MAX_VALUE) {
123                int endOffset = (int) endOffsetLong;
124                if (endOffset >= startOffset && endOffset <= sql.length()) {
125                    String slice = sql.substring(startOffset, endOffset).trim();
126                    if (slice.endsWith(";")) {
127                        slice = slice.substring(0, slice.length() - 1).trim();
128                    }
129                    return slice;
130                }
131            }
132        }
133        try {
134            return statement.toString();
135        } catch (RuntimeException ex) {
136            return null;
137        }
138    }
139}