001package gudusoft.gsqlparser.dlineage.dataflow.model; 002 003import gudusoft.gsqlparser.dlineage.dynamicsql.DynamicSqlTrustMode; 004import gudusoft.gsqlparser.dlineage.util.Pair3; 005 006import java.util.ArrayList; 007import java.util.Collections; 008import java.util.List; 009 010/** 011 * A single dynamic-SQL execution site encountered by 012 * {@link gudusoft.gsqlparser.dlineage.DataFlowAnalyzer}, together with whether the analyzer 013 * could statically resolve it. Recording a site never mutates the core lineage model; consumers 014 * may use its proof IDs to project a trustworthy subset of that already-computed model. 015 * 016 * <p>{@link Status} is observation-mode-aware: 017 * <ul> 018 * <li>{@code RESOLVED} — a complete materialization parsed. In LEGACY mode this also preserves 019 * the historical classification of some incomplete sites for compatibility.</li> 020 * <li>{@code PARTIAL} — SHADOW analyzed incomplete text and retained the historical candidate 021 * relationships; {@link #getUnprovenRelationshipCount()} counts candidates whose 022 * per-edge proof is not available.</li> 023 * <li>{@code PARSE_ERROR} — a fully-known dynamic string whose {@code parse()} failed.</li> 024 * <li>{@code UNRESOLVED} — no usable candidate lineage was produced.</li> 025 * </ul> 026 * 027 * <p>LEGACY's three original states correspond to the internal v2 extraction 028 * states ({@code RESOLVED / AMBIGUOUS / PARSE_ERROR}); {@code PARTIAL} is an 029 * additive SHADOW-only diagnostic state. 030 * 031 * <p>Positions follow the GSP convention: 1-based, end-exclusive, tagged with the per-statement 032 * file hash (same shape as {@link ErrorInfo}). 033 * 034 * <p>For SHADOW consumers that must publish only independently proven 035 * relationships, {@link #getObservedRelationshipIds()} identifies the full 036 * relationship delta attributed to this site and 037 * {@link #getProvenRelationshipIds()} identifies the subset backed by a 038 * complete, hole-free materialization. IDs are scoped to this analyzer result 039 * and correspond to relationship IDs in non-simple {@code dataflow} output. 040 */ 041public final class DynamicSqlSite { 042 043 /** The syntactic form of the dynamic-exec site. */ 044 public enum Kind { 045 SP_EXECUTESQL, 046 EXEC_STRING, 047 EXECUTE_IMMEDIATE, 048 DBMS_SQL, 049 OTHER 050 } 051 052 /** Completeness/publication state of this dynamic SQL site. */ 053 public enum Status { 054 RESOLVED, 055 UNRESOLVED, 056 PARSE_ERROR, 057 /** Incomplete materialization retained for measurement in SHADOW mode. */ 058 PARTIAL 059 } 060 061 private final Kind kind; 062 private final Status status; 063 private final String reason; 064 private final String diagnostic; 065 private final boolean executionCertain; 066 private final Pair3<Long, Long, String> startPosition; 067 private final Pair3<Long, Long, String> endPosition; 068 private final int resolvedRelationshipCount; 069 private final int partialRelationshipCount; 070 private final int unprovenRelationshipCount; 071 private final int unresolvedHoleCount; 072 private final boolean holeCountExact; 073 private final DynamicSqlTrustMode trustMode; 074 private final List<Long> observedRelationshipIds; 075 private final List<Long> provenRelationshipIds; 076 private final List<UnresolvedFragment> unresolvedFragments; 077 private final String materializedText; 078 079 public DynamicSqlSite(Kind kind, Status status, String reason, 080 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 081 int resolvedRelationshipCount, int partialRelationshipCount) { 082 this(kind, status, reason, startPosition, endPosition, resolvedRelationshipCount, 083 partialRelationshipCount, 0, -1, false, DynamicSqlTrustMode.LEGACY, 084 Collections.<Long>emptyList(), Collections.<Long>emptyList(), null); 085 } 086 087 public DynamicSqlSite(Kind kind, Status status, String reason, 088 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 089 int resolvedRelationshipCount, int partialRelationshipCount, 090 int unprovenRelationshipCount, 091 int unresolvedHoleCount, boolean holeCountExact, DynamicSqlTrustMode trustMode) { 092 this(kind, status, reason, startPosition, endPosition, resolvedRelationshipCount, 093 partialRelationshipCount, unprovenRelationshipCount, 094 unresolvedHoleCount, holeCountExact, trustMode, 095 Collections.<Long>emptyList(), Collections.<Long>emptyList(), null); 096 } 097 098 public DynamicSqlSite(Kind kind, Status status, String reason, 099 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 100 int resolvedRelationshipCount, int partialRelationshipCount, 101 int unprovenRelationshipCount, 102 int unresolvedHoleCount, boolean holeCountExact, DynamicSqlTrustMode trustMode, 103 List<Long> observedRelationshipIds, List<Long> provenRelationshipIds) { 104 this(kind, status, reason, startPosition, endPosition, 105 resolvedRelationshipCount, partialRelationshipCount, 106 unprovenRelationshipCount, unresolvedHoleCount, holeCountExact, 107 trustMode, observedRelationshipIds, provenRelationshipIds, null); 108 } 109 110 public DynamicSqlSite(Kind kind, Status status, String reason, 111 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 112 int resolvedRelationshipCount, int partialRelationshipCount, 113 int unprovenRelationshipCount, 114 int unresolvedHoleCount, boolean holeCountExact, DynamicSqlTrustMode trustMode, 115 List<Long> observedRelationshipIds, List<Long> provenRelationshipIds, 116 String diagnostic) { 117 this(kind, status, reason, startPosition, endPosition, 118 resolvedRelationshipCount, partialRelationshipCount, 119 unprovenRelationshipCount, unresolvedHoleCount, holeCountExact, 120 trustMode, observedRelationshipIds, provenRelationshipIds, diagnostic, 121 Collections.<UnresolvedFragment>emptyList(), null); 122 } 123 124 public DynamicSqlSite(Kind kind, Status status, String reason, 125 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 126 int resolvedRelationshipCount, int partialRelationshipCount, 127 int unprovenRelationshipCount, 128 int unresolvedHoleCount, boolean holeCountExact, DynamicSqlTrustMode trustMode, 129 List<Long> observedRelationshipIds, List<Long> provenRelationshipIds, 130 String diagnostic, List<UnresolvedFragment> unresolvedFragments, 131 String materializedText) { 132 this(kind, status, reason, startPosition, endPosition, 133 resolvedRelationshipCount, partialRelationshipCount, 134 unprovenRelationshipCount, unresolvedHoleCount, holeCountExact, 135 trustMode, observedRelationshipIds, provenRelationshipIds, diagnostic, 136 unresolvedFragments, materializedText, true); 137 } 138 139 public DynamicSqlSite(Kind kind, Status status, String reason, 140 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 141 int resolvedRelationshipCount, int partialRelationshipCount, 142 int unprovenRelationshipCount, 143 int unresolvedHoleCount, boolean holeCountExact, DynamicSqlTrustMode trustMode, 144 List<Long> observedRelationshipIds, List<Long> provenRelationshipIds, 145 String diagnostic, List<UnresolvedFragment> unresolvedFragments, 146 String materializedText, boolean executionCertain) { 147 this.executionCertain = executionCertain; 148 this.kind = kind; 149 this.status = status; 150 this.reason = reason; 151 this.diagnostic = diagnostic; 152 this.startPosition = startPosition; 153 this.endPosition = endPosition; 154 this.resolvedRelationshipCount = resolvedRelationshipCount; 155 this.partialRelationshipCount = partialRelationshipCount; 156 this.unprovenRelationshipCount = unprovenRelationshipCount; 157 this.unresolvedFragments = unresolvedFragments == null || unresolvedFragments.isEmpty() 158 ? Collections.<UnresolvedFragment>emptyList() 159 : Collections.unmodifiableList(new ArrayList<UnresolvedFragment>(unresolvedFragments)); 160 // A listed fragment set IS the count: the two can never disagree. 161 this.unresolvedHoleCount = this.unresolvedFragments.isEmpty() ? unresolvedHoleCount 162 : this.unresolvedFragments.size(); 163 this.holeCountExact = this.unresolvedFragments.isEmpty() ? holeCountExact : true; 164 this.trustMode = trustMode == null ? DynamicSqlTrustMode.LEGACY : trustMode; 165 this.observedRelationshipIds = immutableIds(observedRelationshipIds); 166 this.provenRelationshipIds = immutableIds(provenRelationshipIds); 167 this.materializedText = materializedText; 168 } 169 170 private static List<Long> immutableIds(List<Long> values) { 171 if (values == null || values.isEmpty()) { 172 return Collections.emptyList(); 173 } 174 return Collections.unmodifiableList(new ArrayList<Long>(values)); 175 } 176 177 public Kind getKind() { 178 return kind; 179 } 180 181 public Status getStatus() { 182 return status; 183 } 184 185 /** Human-readable explanation; {@code null} for {@link Status#RESOLVED}. */ 186 public String getReason() { 187 return reason; 188 } 189 190 /** Additive analysis note, kept separate from an unresolved reason. */ 191 public String getDiagnostic() { 192 return diagnostic; 193 } 194 195 /** 196 * False when the statement may not run on the path that produced this text: it sits in a CATCH block, or 197 * behind a condition or loop whose outcome the analysis cannot decide (the diagnostic says which). The 198 * lineage the site published is what the statement does when it runs. 199 */ 200 public boolean isExecutionCertain() { 201 return executionCertain; 202 } 203 204 public Pair3<Long, Long, String> getStartPosition() { 205 return startPosition; 206 } 207 208 public Pair3<Long, Long, String> getEndPosition() { 209 return endPosition; 210 } 211 212 /** 213 * Number of lineage edges this site produced into a <b>real</b> target column (a resolved base-table 214 * column, not a T-SQL variable or placeholder). Intermediate result-set hops are not counted. 215 * {@code > 0} iff the site's dynamic SQL yielded usable lineage. 216 */ 217 public int getResolvedRelationshipCount() { 218 return resolvedRelationshipCount; 219 } 220 221 /** 222 * Number of lineage edges this site produced into an <b>unresolved</b> target — a T-SQL variable or 223 * placeholder (e.g. {@code EXEC sp_executesql @sql} folded to {@code INSERT INTO @t SELECT ...}). These 224 * are partial: a source is known but the target object is not. 225 */ 226 public int getPartialRelationshipCount() { 227 return partialRelationshipCount; 228 } 229 230 /** Historical candidate relationships SHADOW observed on incomplete text without a per-edge proof. */ 231 public int getUnprovenRelationshipCount() { 232 return unprovenRelationshipCount; 233 } 234 235 /** 236 * Number of unresolved splice fragments, or {@code -1} when the producer exposes only a 237 * partial flag. Whenever {@link #getUnresolvedFragments()} is non-empty this IS its size 238 * and {@link #isHoleCountExact()} is true; an exact positive count with an empty list 239 * can only come from a producer that counted without listing (none in GSP itself). 240 */ 241 public int getUnresolvedHoleCount() { 242 return unresolvedHoleCount; 243 } 244 245 public boolean isHoleCountExact() { 246 return holeCountExact; 247 } 248 249 /** 250 * The unresolved fragments ("holes") of this site's argument, one per non-literal 251 * fragment of its materialization, in string order: which variable / parameter / 252 * expression the evaluator could not fold, why, where its value would come from, and 253 * the origin span to highlight. Empty (never null) for a fully resolved site and for 254 * a site the evaluator did not see (a dynamic site outside a procedure body, or a 255 * vendor without the abstract evaluator) — check {@link #isHoleCountExact()} to tell 256 * "no holes" from "not known". 257 */ 258 public List<UnresolvedFragment> getUnresolvedFragments() { 259 return unresolvedFragments; 260 } 261 262 /** 263 * The argument as the evaluator materialized it, with every hole rendered as its 264 * {@link UnresolvedFragment#getText()} (a compile-time literal argument is its own 265 * text); {@code null} when the evaluator did not see the site or the argument did not 266 * reduce to a string at all - the holes are still listed, but there is no honest 267 * rendering of an unreduced value. 268 */ 269 public String getMaterializedText() { 270 return materializedText; 271 } 272 273 public DynamicSqlTrustMode getTrustMode() { 274 return trustMode; 275 } 276 277 /** Relationship IDs attributed to this site, including unproven SHADOW candidates. */ 278 public List<Long> getObservedRelationshipIds() { 279 return observedRelationshipIds; 280 } 281 282 /** 283 * Relationship IDs whose analyzed text was complete, parsed, and contained 284 * no unresolved provenance holes or placeholder targets. 285 */ 286 public List<Long> getProvenRelationshipIds() { 287 return provenRelationshipIds; 288 } 289 290 /** 291 * True iff this site produced at least one resolved lineage edge (into a real target column). 292 * In SHADOW this may be true for a PARTIAL site because candidate edges remain published for 293 * comparison; check {@link #getStatus()} and {@link #getUnprovenRelationshipCount()} before 294 * treating those edges as proven. A site with only unresolved-target edges or no edge returns false. 295 */ 296 public boolean producedLineage() { 297 return resolvedRelationshipCount > 0; 298 } 299}