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 Pair3<Long, Long, String> startPosition; 066 private final Pair3<Long, Long, String> endPosition; 067 private final int resolvedRelationshipCount; 068 private final int partialRelationshipCount; 069 private final int unprovenRelationshipCount; 070 private final int unresolvedHoleCount; 071 private final boolean holeCountExact; 072 private final DynamicSqlTrustMode trustMode; 073 private final List<Long> observedRelationshipIds; 074 private final List<Long> provenRelationshipIds; 075 private final List<UnresolvedFragment> unresolvedFragments; 076 private final String materializedText; 077 078 public DynamicSqlSite(Kind kind, Status status, String reason, 079 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 080 int resolvedRelationshipCount, int partialRelationshipCount) { 081 this(kind, status, reason, startPosition, endPosition, resolvedRelationshipCount, 082 partialRelationshipCount, 0, -1, false, DynamicSqlTrustMode.LEGACY, 083 Collections.<Long>emptyList(), Collections.<Long>emptyList(), null); 084 } 085 086 public DynamicSqlSite(Kind kind, Status status, String reason, 087 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 088 int resolvedRelationshipCount, int partialRelationshipCount, 089 int unprovenRelationshipCount, 090 int unresolvedHoleCount, boolean holeCountExact, DynamicSqlTrustMode trustMode) { 091 this(kind, status, reason, startPosition, endPosition, resolvedRelationshipCount, 092 partialRelationshipCount, unprovenRelationshipCount, 093 unresolvedHoleCount, holeCountExact, trustMode, 094 Collections.<Long>emptyList(), Collections.<Long>emptyList(), null); 095 } 096 097 public DynamicSqlSite(Kind kind, Status status, String reason, 098 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 099 int resolvedRelationshipCount, int partialRelationshipCount, 100 int unprovenRelationshipCount, 101 int unresolvedHoleCount, boolean holeCountExact, DynamicSqlTrustMode trustMode, 102 List<Long> observedRelationshipIds, List<Long> provenRelationshipIds) { 103 this(kind, status, reason, startPosition, endPosition, 104 resolvedRelationshipCount, partialRelationshipCount, 105 unprovenRelationshipCount, unresolvedHoleCount, holeCountExact, 106 trustMode, observedRelationshipIds, provenRelationshipIds, null); 107 } 108 109 public DynamicSqlSite(Kind kind, Status status, String reason, 110 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 111 int resolvedRelationshipCount, int partialRelationshipCount, 112 int unprovenRelationshipCount, 113 int unresolvedHoleCount, boolean holeCountExact, DynamicSqlTrustMode trustMode, 114 List<Long> observedRelationshipIds, List<Long> provenRelationshipIds, 115 String diagnostic) { 116 this(kind, status, reason, startPosition, endPosition, 117 resolvedRelationshipCount, partialRelationshipCount, 118 unprovenRelationshipCount, unresolvedHoleCount, holeCountExact, 119 trustMode, observedRelationshipIds, provenRelationshipIds, diagnostic, 120 Collections.<UnresolvedFragment>emptyList(), null); 121 } 122 123 public DynamicSqlSite(Kind kind, Status status, String reason, 124 Pair3<Long, Long, String> startPosition, Pair3<Long, Long, String> endPosition, 125 int resolvedRelationshipCount, int partialRelationshipCount, 126 int unprovenRelationshipCount, 127 int unresolvedHoleCount, boolean holeCountExact, DynamicSqlTrustMode trustMode, 128 List<Long> observedRelationshipIds, List<Long> provenRelationshipIds, 129 String diagnostic, List<UnresolvedFragment> unresolvedFragments, 130 String materializedText) { 131 this.kind = kind; 132 this.status = status; 133 this.reason = reason; 134 this.diagnostic = diagnostic; 135 this.startPosition = startPosition; 136 this.endPosition = endPosition; 137 this.resolvedRelationshipCount = resolvedRelationshipCount; 138 this.partialRelationshipCount = partialRelationshipCount; 139 this.unprovenRelationshipCount = unprovenRelationshipCount; 140 this.unresolvedFragments = unresolvedFragments == null || unresolvedFragments.isEmpty() 141 ? Collections.<UnresolvedFragment>emptyList() 142 : Collections.unmodifiableList(new ArrayList<UnresolvedFragment>(unresolvedFragments)); 143 // A listed fragment set IS the count: the two can never disagree. 144 this.unresolvedHoleCount = this.unresolvedFragments.isEmpty() ? unresolvedHoleCount 145 : this.unresolvedFragments.size(); 146 this.holeCountExact = this.unresolvedFragments.isEmpty() ? holeCountExact : true; 147 this.trustMode = trustMode == null ? DynamicSqlTrustMode.LEGACY : trustMode; 148 this.observedRelationshipIds = immutableIds(observedRelationshipIds); 149 this.provenRelationshipIds = immutableIds(provenRelationshipIds); 150 this.materializedText = materializedText; 151 } 152 153 private static List<Long> immutableIds(List<Long> values) { 154 if (values == null || values.isEmpty()) { 155 return Collections.emptyList(); 156 } 157 return Collections.unmodifiableList(new ArrayList<Long>(values)); 158 } 159 160 public Kind getKind() { 161 return kind; 162 } 163 164 public Status getStatus() { 165 return status; 166 } 167 168 /** Human-readable explanation; {@code null} for {@link Status#RESOLVED}. */ 169 public String getReason() { 170 return reason; 171 } 172 173 /** Additive analysis note, kept separate from an unresolved reason. */ 174 public String getDiagnostic() { 175 return diagnostic; 176 } 177 178 public Pair3<Long, Long, String> getStartPosition() { 179 return startPosition; 180 } 181 182 public Pair3<Long, Long, String> getEndPosition() { 183 return endPosition; 184 } 185 186 /** 187 * Number of lineage edges this site produced into a <b>real</b> target column (a resolved base-table 188 * column, not a T-SQL variable or placeholder). Intermediate result-set hops are not counted. 189 * {@code > 0} iff the site's dynamic SQL yielded usable lineage. 190 */ 191 public int getResolvedRelationshipCount() { 192 return resolvedRelationshipCount; 193 } 194 195 /** 196 * Number of lineage edges this site produced into an <b>unresolved</b> target — a T-SQL variable or 197 * placeholder (e.g. {@code EXEC sp_executesql @sql} folded to {@code INSERT INTO @t SELECT ...}). These 198 * are partial: a source is known but the target object is not. 199 */ 200 public int getPartialRelationshipCount() { 201 return partialRelationshipCount; 202 } 203 204 /** Historical candidate relationships SHADOW observed on incomplete text without a per-edge proof. */ 205 public int getUnprovenRelationshipCount() { 206 return unprovenRelationshipCount; 207 } 208 209 /** 210 * Number of unresolved splice fragments, or {@code -1} when the producer exposes only a 211 * partial flag. Whenever {@link #getUnresolvedFragments()} is non-empty this IS its size 212 * and {@link #isHoleCountExact()} is true; an exact positive count with an empty list 213 * can only come from a producer that counted without listing (none in GSP itself). 214 */ 215 public int getUnresolvedHoleCount() { 216 return unresolvedHoleCount; 217 } 218 219 public boolean isHoleCountExact() { 220 return holeCountExact; 221 } 222 223 /** 224 * The unresolved fragments ("holes") of this site's argument, one per non-literal 225 * fragment of its materialization, in string order: which variable / parameter / 226 * expression the evaluator could not fold, why, where its value would come from, and 227 * the origin span to highlight. Empty (never null) for a fully resolved site and for 228 * a site the evaluator did not see (a dynamic site outside a procedure body, or a 229 * vendor without the abstract evaluator) — check {@link #isHoleCountExact()} to tell 230 * "no holes" from "not known". 231 */ 232 public List<UnresolvedFragment> getUnresolvedFragments() { 233 return unresolvedFragments; 234 } 235 236 /** 237 * The argument as the evaluator materialized it, with every hole rendered as its 238 * {@link UnresolvedFragment#getText()} (a compile-time literal argument is its own 239 * text); {@code null} when the evaluator did not see the site or the argument did not 240 * reduce to a string at all - the holes are still listed, but there is no honest 241 * rendering of an unreduced value. 242 */ 243 public String getMaterializedText() { 244 return materializedText; 245 } 246 247 public DynamicSqlTrustMode getTrustMode() { 248 return trustMode; 249 } 250 251 /** Relationship IDs attributed to this site, including unproven SHADOW candidates. */ 252 public List<Long> getObservedRelationshipIds() { 253 return observedRelationshipIds; 254 } 255 256 /** 257 * Relationship IDs whose analyzed text was complete, parsed, and contained 258 * no unresolved provenance holes or placeholder targets. 259 */ 260 public List<Long> getProvenRelationshipIds() { 261 return provenRelationshipIds; 262 } 263 264 /** 265 * True iff this site produced at least one resolved lineage edge (into a real target column). 266 * In SHADOW this may be true for a PARTIAL site because candidate edges remain published for 267 * comparison; check {@link #getStatus()} and {@link #getUnprovenRelationshipCount()} before 268 * treating those edges as proven. A site with only unresolved-target edges or no edge returns false. 269 */ 270 public boolean producedLineage() { 271 return resolvedRelationshipCount > 0; 272 } 273}