001package gudusoft.gsqlparser.dlineage.dynamicsql; 002 003/** 004 * One provenance-tagged fragment of a materialized dynamic-SQL string. 005 * 006 * <p>A materialized string is an ordered fragment sequence whose concatenated 007 * {@link #text} equals the rendered SQL exactly. Each fragment is either: 008 * <ul> 009 * <li>{@link Kind#LITERAL} — text proven to be the exact runtime characters: 010 * a string literal, or the output of a transform proven byte-exact for 011 * the vendor;</li> 012 * <li>{@link Kind#HOLE} — text standing in for a value the evaluator could 013 * not prove exactly: an unbound variable's own token, an unfoldable 014 * expression's placeholder, or the output of a transform that is folded 015 * for display but NOT proven byte-exact ({@link HoleOrigin#INEXACT_TRANSFORM}).</li> 016 * </ul> 017 * 018 * <p>Consumers (the per-edge publication proof, design doc 019 * {@code dynamic-sql-fragment-provenance-design.md}) must treat every HOLE as 020 * arbitrary runtime text; no lineage evidence may rest on HOLE characters. The 021 * legacy {@code SqlStringValue.state}/{@code text} contract is unchanged — 022 * fragments are additional provenance carried alongside it. 023 */ 024public final class SqlFragment { 025 026 /** Fragment class: exact literal text vs. unproven stand-in text. */ 027 public enum Kind { LITERAL, HOLE } 028 029 /** Why a HOLE exists — which producer limitation created it. */ 030 public enum HoleOrigin { 031 /** A variable/parameter never bound and never assigned a foldable value. */ 032 UNBOUND_VARIABLE, 033 /** A bare (non-variable) identifier used as a value — data-dependent. */ 034 NON_VARIABLE_IDENT, 035 /** A function call the evaluator does not fold. */ 036 OPAQUE_CALL, 037 /** An expression form the evaluator does not model. */ 038 UNSUPPORTED_EXPR, 039 /** Evaluation hit a configured resource bound. */ 040 RESOURCE_LIMIT, 041 /** 042 * A transform was folded for display but its output is not proven 043 * byte-exact against vendor runtime semantics (CAST/CONVERT padding and 044 * styles, collation-aware REPLACE/UPPER/LOWER, whitespace-class TRIM, 045 * non-default QUOTENAME delimiters, numeric rendering). 046 */ 047 INEXACT_TRANSFORM, 048 /** 049 * A variable whose last visible assignment read from a query 050 * ({@code SELECT @v = col FROM t ...}): its value is runtime data. The 051 * hole keeps the assignment's own rendering (the source column name as a 052 * placeholder) but is named after the VARIABLE, and {@link #valueOrigin} 053 * describes the column / expression the value would come from. 054 */ 055 QUERY_ASSIGNED_VARIABLE, 056 /** 057 * A variable whose value depends on control flow the evaluator cannot 058 * decide: assigned inside an undecidable IF / WHILE branch, declared 059 * there, or assigned by a driver loop whose iteration is unknown. 060 */ 061 FLOW_DEPENDENT_VARIABLE 062 } 063 064 public final Kind kind; 065 /** Exact rendered text this fragment contributes to the materialized SQL. Never null. */ 066 public final String text; 067 /** HOLE only: why the hole exists; null for LITERAL. */ 068 public final HoleOrigin origin; 069 /** HOLE only: the variable/function/expression name that caused it; may be null. */ 070 public final String originName; 071 /** HOLE only: human-readable cause; null for LITERAL. */ 072 public final String reason; 073 /** 074 * HOLE only: true when the hole's runtime VALUE is unknown (an unbound variable, or a 075 * transform applied to one). False for an {@code INEXACT_TRANSFORM} computed entirely 076 * from literals, where the value is known and only its byte-exactness against vendor 077 * runtime semantics is unproven. The distinction matters to consumers deciding whether 078 * a folded identifier is a runtime TEMPLATE ({@code UPPER(@X)} — unknown) or merely an 079 * unproven rendering of a known name ({@code UPPER('src')} — known). Always false for 080 * LITERAL fragments. 081 */ 082 public final boolean valueUnknown; 083 /** 084 * Where the value came from. LITERAL: the seed of an exact value — null for 085 * ordinary SQL string fragments; constant-relation values use a stable 086 * description such as {@code INSERT VALUES row 2 of @Source}. HOLE: the 087 * data source the unknown value would be read from, when the evaluator saw 088 * it ({@code dbo.SSF_Entity.Entity} for a {@link HoleOrigin#QUERY_ASSIGNED_VARIABLE}); 089 * null when unknown. 090 */ 091 public final String valueOrigin; 092 /** 093 * Source offsets, end-exclusive, of the fragment's origin in the analyzed 094 * text: for a LITERAL the seeding expression; for a HOLE the place the 095 * value was last defined — the assignment target of a query-assigned or 096 * flow-dependent variable, the declaration of an unbound parameter or of a 097 * declared-but-unassigned variable, otherwise the reference that produced 098 * the hole. {@code -1} when the origin is not in the analyzed text. 099 */ 100 public final long sourceStartOffset; 101 public final long sourceEndOffset; 102 /** 103 * The same origin span as line / column: 1-based, end-exclusive end column, 104 * in the coordinate system of {@code TSourceToken.lineNo / columnNo}. 105 * {@code -1} when unknown. Populated for HOLE fragments whose origin is a 106 * source node; LITERAL fragments carry offsets only. 107 */ 108 public final int sourceStartLine; 109 public final int sourceStartColumn; 110 public final int sourceEndLine; 111 public final int sourceEndColumn; 112 /** 113 * LITERAL only: where the characters of {@link #text} sit in the analyzed source, one piece per string literal 114 * token they were read from (Mantis 4851); empty when unknown, as for the output of a transform. Positional 115 * metadata only, no part of the fragment's provenance: merging and {@link #hasSameLiteralProvenance} ignore it. 116 */ 117 public final java.util.List<SourcePiece> pieces; 118 119 private SqlFragment(Kind kind, String text, HoleOrigin origin, String originName, String reason, 120 boolean valueUnknown, String valueOrigin, long sourceStartOffset, 121 long sourceEndOffset) { 122 this(kind, text, origin, originName, reason, valueUnknown, valueOrigin, 123 sourceStartOffset, sourceEndOffset, -1, -1, -1, -1); 124 } 125 126 private SqlFragment(Kind kind, String text, HoleOrigin origin, String originName, String reason, 127 boolean valueUnknown, String valueOrigin, long sourceStartOffset, 128 long sourceEndOffset, int sourceStartLine, int sourceStartColumn, 129 int sourceEndLine, int sourceEndColumn) { 130 this(kind, text, origin, originName, reason, valueUnknown, valueOrigin, sourceStartOffset, sourceEndOffset, 131 sourceStartLine, sourceStartColumn, sourceEndLine, sourceEndColumn, 132 java.util.Collections.<SourcePiece>emptyList()); 133 } 134 135 private SqlFragment(Kind kind, String text, HoleOrigin origin, String originName, String reason, 136 boolean valueUnknown, String valueOrigin, long sourceStartOffset, 137 long sourceEndOffset, int sourceStartLine, int sourceStartColumn, 138 int sourceEndLine, int sourceEndColumn, java.util.List<SourcePiece> pieces) { 139 this.pieces = pieces; 140 this.kind = kind; 141 this.text = text == null ? "" : text; 142 this.origin = origin; 143 this.originName = originName; 144 this.reason = reason; 145 this.valueUnknown = valueUnknown; 146 this.valueOrigin = valueOrigin; 147 this.sourceStartOffset = sourceStartOffset; 148 this.sourceEndOffset = sourceEndOffset; 149 this.sourceStartLine = sourceStartLine; 150 this.sourceStartColumn = sourceStartColumn; 151 this.sourceEndLine = sourceEndLine; 152 this.sourceEndColumn = sourceEndColumn; 153 } 154 155 public static SqlFragment literal(String text) { 156 return new SqlFragment(Kind.LITERAL, text, null, null, null, false, 157 null, -1L, -1L); 158 } 159 160 /** Exact literal value with its procedure-body seed provenance. */ 161 public static SqlFragment literal(String text, String valueOrigin, 162 long sourceStartOffset, long sourceEndOffset) { 163 return new SqlFragment(Kind.LITERAL, text, null, null, null, false, 164 valueOrigin, sourceStartOffset, sourceEndOffset); 165 } 166 167 /** Exact literal value read from one string literal token, with where its characters sit (Mantis 4851). */ 168 public static SqlFragment literal(String text, SourcePiece piece) { 169 return piece == null ? literal(text) 170 : new SqlFragment(Kind.LITERAL, text, null, null, null, false, null, -1L, -1L, -1, -1, -1, -1, 171 java.util.Collections.singletonList(piece)); 172 } 173 174 /** 175 * The LITERAL that {@code a} followed by {@code b} make when merged: {@code a}'s provenance, as before, and the 176 * pieces of both, those of {@code b} moved past {@code a}'s text. 177 */ 178 static SqlFragment mergedLiteral(SqlFragment a, SqlFragment b) { 179 java.util.List<SourcePiece> pieces = java.util.Collections.emptyList(); 180 if (!a.pieces.isEmpty() || !b.pieces.isEmpty()) { 181 pieces = new java.util.ArrayList<SourcePiece>(a.pieces.size() + b.pieces.size()); 182 pieces.addAll(a.pieces); 183 for (SourcePiece p : b.pieces) pieces.add(p.shifted(a.text.length())); 184 } 185 return new SqlFragment(Kind.LITERAL, a.text + b.text, null, null, null, false, a.valueOrigin, 186 a.valueOrigin == null ? -1L : a.sourceStartOffset, a.valueOrigin == null ? -1L : a.sourceEndOffset, 187 -1, -1, -1, -1, pieces); 188 } 189 190 /** 191 * {@code length} characters of a LITERAL's text, from {@code textStart}, read from one string literal token of the 192 * analyzed source (Mantis 4851): where each sits there, so that what GSP analyzes inside the string reports 193 * positions in the source. {@code ''} is one character of the text and two of the source; the token keeps its 194 * quotes, a one-letter prefix such as N, and its line breaks. 195 */ 196 public static final class SourcePiece { 197 public final int textStart; 198 public final int length; 199 private final long tokenLine; 200 private final long tokenColumn; 201 private final String tokenText; 202 private final int contentStart; 203 204 private SourcePiece(int textStart, int length, long tokenLine, long tokenColumn, String tokenText, 205 int contentStart) { 206 this.textStart = textStart; 207 this.length = length; 208 this.tokenLine = tokenLine; 209 this.tokenColumn = tokenColumn; 210 this.tokenText = tokenText; 211 this.contentStart = contentStart; 212 } 213 214 /** 215 * The piece of string literal token {@code token} whose content decodes to {@code decoded}: null unless the 216 * token is a single-quoted literal (with at most a one-letter prefix) whose content, {@code ''} read as one 217 * quote, is exactly {@code decoded}. 218 */ 219 public static SourcePiece of(gudusoft.gsqlparser.TSourceToken token, String decoded) { 220 if (token == null || decoded == null || token.lineNo <= 0 || token.columnNo <= 0) return null; 221 String raw = token.toString(); 222 if (raw == null) return null; 223 int q = raw.indexOf('\''); 224 if (q < 0 || q > 1) return null; 225 StringBuilder content = new StringBuilder(); 226 int j = q + 1; 227 boolean closed = false; 228 while (j < raw.length()) { 229 char ch = raw.charAt(j); 230 if (ch == '\'') { 231 if (j + 1 < raw.length() && raw.charAt(j + 1) == '\'') { 232 content.append('\''); 233 j += 2; 234 continue; 235 } 236 closed = true; 237 j++; 238 break; 239 } 240 content.append(ch); 241 j++; 242 } 243 if (!closed || j != raw.length() || !content.toString().equals(decoded)) return null; 244 return new SourcePiece(0, decoded.length(), token.lineNo, token.columnNo, raw, q + 1); 245 } 246 247 SourcePiece shifted(int by) { 248 return new SourcePiece(textStart + by, length, tokenLine, tokenColumn, tokenText, contentStart); 249 } 250 251 /** 252 * Writes the source line and column of each character of this piece into {@code lines} and {@code columns}, 253 * at index {@code base + textStart + k} for its k-th character; returns the position just past its last 254 * character, the literal's closing quote, where a value concatenated after it is spliced in. 255 */ 256 public long[] fill(long[] lines, long[] columns, int base) { 257 long line = tokenLine, column = tokenColumn; 258 int j = 0; 259 for (; j < contentStart; j++) { 260 if (breaksLine(tokenText, j)) { line++; column = 1; } else { column++; } 261 } 262 for (int k = 0; k < length && j < tokenText.length(); k++) { 263 int at = base + textStart + k; 264 if (at >= 0 && at < lines.length) { 265 lines[at] = line; 266 columns[at] = column; 267 } 268 char ch = tokenText.charAt(j); 269 if (ch == '\'' && j + 1 < tokenText.length() && tokenText.charAt(j + 1) == '\'') { 270 j += 2; 271 column += 2; 272 } else { 273 if (breaksLine(tokenText, j)) { line++; column = 1; } else { column++; } 274 j++; 275 } 276 } 277 return new long[] {line, column}; 278 } 279 280 /** Whether character {@code j} ends a line as the lexer counts lines: LF, or a CR not followed by LF. */ 281 private static boolean breaksLine(String text, int j) { 282 char ch = text.charAt(j); 283 return ch == '\n' || (ch == '\r' && (j + 1 >= text.length() || text.charAt(j + 1) != '\n')); 284 } 285 } 286 287 /** 288 * Hole with the default value-knowledge for its origin: every origin except 289 * {@code INEXACT_TRANSFORM} stands in for a value nobody knows; an inexact transform 290 * defaults to value-known (computed from literals). A transform over a PARTIAL input 291 * must use the explicit overload with {@code valueUnknown=true}. 292 */ 293 public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason) { 294 HoleOrigin resolved = origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin; 295 return new SqlFragment(Kind.HOLE, renderedText, resolved, originName, reason, 296 resolved != HoleOrigin.INEXACT_TRANSFORM, null, -1L, -1L); 297 } 298 299 public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason, 300 boolean valueUnknown) { 301 return new SqlFragment(Kind.HOLE, renderedText, 302 origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin, originName, reason, valueUnknown, 303 null, -1L, -1L); 304 } 305 306 /** 307 * Hole with full provenance: the data source its value would come from 308 * ({@code valueOrigin}, may be null) and the origin span in the analyzed 309 * text (see {@link #sourceStartOffset} / {@link #sourceStartLine}; pass 310 * {@code -1} for unknown). 311 */ 312 public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason, 313 boolean valueUnknown, String valueOrigin, long sourceStartOffset, long sourceEndOffset, 314 int sourceStartLine, int sourceStartColumn, int sourceEndLine, int sourceEndColumn) { 315 return new SqlFragment(Kind.HOLE, renderedText, 316 origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin, originName, reason, valueUnknown, 317 valueOrigin, sourceStartOffset, sourceEndOffset, 318 sourceStartLine, sourceStartColumn, sourceEndLine, sourceEndColumn); 319 } 320 321 /** True when the origin span is known in line / column form. */ 322 public boolean hasSourceSpan() { 323 return sourceStartLine > 0 && sourceStartColumn > 0; 324 } 325 326 public boolean isLiteral() { 327 return kind == Kind.LITERAL; 328 } 329 330 boolean hasSameLiteralProvenance(SqlFragment other) { 331 if (other == null || !isLiteral() || !other.isLiteral()) { 332 return false; 333 } 334 if (valueOrigin == null ? other.valueOrigin != null : !valueOrigin.equals(other.valueOrigin)) { 335 return false; 336 } 337 return sourceStartOffset == other.sourceStartOffset 338 && sourceEndOffset == other.sourceEndOffset; 339 } 340 341 @Override 342 public String toString() { 343 return kind == Kind.LITERAL 344 ? "L[" + text + "]" 345 : "H[" + text + " <" + origin + (originName != null ? ":" + originName : "") + ">]"; 346 } 347}