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 private SqlFragment(Kind kind, String text, HoleOrigin origin, String originName, String reason, 114 boolean valueUnknown, String valueOrigin, long sourceStartOffset, 115 long sourceEndOffset) { 116 this(kind, text, origin, originName, reason, valueUnknown, valueOrigin, 117 sourceStartOffset, sourceEndOffset, -1, -1, -1, -1); 118 } 119 120 private SqlFragment(Kind kind, String text, HoleOrigin origin, String originName, String reason, 121 boolean valueUnknown, String valueOrigin, long sourceStartOffset, 122 long sourceEndOffset, int sourceStartLine, int sourceStartColumn, 123 int sourceEndLine, int sourceEndColumn) { 124 this.kind = kind; 125 this.text = text == null ? "" : text; 126 this.origin = origin; 127 this.originName = originName; 128 this.reason = reason; 129 this.valueUnknown = valueUnknown; 130 this.valueOrigin = valueOrigin; 131 this.sourceStartOffset = sourceStartOffset; 132 this.sourceEndOffset = sourceEndOffset; 133 this.sourceStartLine = sourceStartLine; 134 this.sourceStartColumn = sourceStartColumn; 135 this.sourceEndLine = sourceEndLine; 136 this.sourceEndColumn = sourceEndColumn; 137 } 138 139 public static SqlFragment literal(String text) { 140 return new SqlFragment(Kind.LITERAL, text, null, null, null, false, 141 null, -1L, -1L); 142 } 143 144 /** Exact literal value with its procedure-body seed provenance. */ 145 public static SqlFragment literal(String text, String valueOrigin, 146 long sourceStartOffset, long sourceEndOffset) { 147 return new SqlFragment(Kind.LITERAL, text, null, null, null, false, 148 valueOrigin, sourceStartOffset, sourceEndOffset); 149 } 150 151 /** 152 * Hole with the default value-knowledge for its origin: every origin except 153 * {@code INEXACT_TRANSFORM} stands in for a value nobody knows; an inexact transform 154 * defaults to value-known (computed from literals). A transform over a PARTIAL input 155 * must use the explicit overload with {@code valueUnknown=true}. 156 */ 157 public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason) { 158 HoleOrigin resolved = origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin; 159 return new SqlFragment(Kind.HOLE, renderedText, resolved, originName, reason, 160 resolved != HoleOrigin.INEXACT_TRANSFORM, null, -1L, -1L); 161 } 162 163 public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason, 164 boolean valueUnknown) { 165 return new SqlFragment(Kind.HOLE, renderedText, 166 origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin, originName, reason, valueUnknown, 167 null, -1L, -1L); 168 } 169 170 /** 171 * Hole with full provenance: the data source its value would come from 172 * ({@code valueOrigin}, may be null) and the origin span in the analyzed 173 * text (see {@link #sourceStartOffset} / {@link #sourceStartLine}; pass 174 * {@code -1} for unknown). 175 */ 176 public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason, 177 boolean valueUnknown, String valueOrigin, long sourceStartOffset, long sourceEndOffset, 178 int sourceStartLine, int sourceStartColumn, int sourceEndLine, int sourceEndColumn) { 179 return new SqlFragment(Kind.HOLE, renderedText, 180 origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin, originName, reason, valueUnknown, 181 valueOrigin, sourceStartOffset, sourceEndOffset, 182 sourceStartLine, sourceStartColumn, sourceEndLine, sourceEndColumn); 183 } 184 185 /** True when the origin span is known in line / column form. */ 186 public boolean hasSourceSpan() { 187 return sourceStartLine > 0 && sourceStartColumn > 0; 188 } 189 190 public boolean isLiteral() { 191 return kind == Kind.LITERAL; 192 } 193 194 boolean hasSameLiteralProvenance(SqlFragment other) { 195 if (other == null || !isLiteral() || !other.isLiteral()) { 196 return false; 197 } 198 if (valueOrigin == null ? other.valueOrigin != null : !valueOrigin.equals(other.valueOrigin)) { 199 return false; 200 } 201 return sourceStartOffset == other.sourceStartOffset 202 && sourceEndOffset == other.sourceEndOffset; 203 } 204 205 @Override 206 public String toString() { 207 return kind == Kind.LITERAL 208 ? "L[" + text + "]" 209 : "H[" + text + " <" + origin + (originName != null ? ":" + originName : "") + ">]"; 210 } 211}