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}