001package gudusoft.gsqlparser.dlineage.dynamicsql; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.List; 006 007/** 008 * Result for a single dynamic-SQL EXEC site inside a procedure. 009 * 010 * <p>Either it resolved (status {@link Status#RESOLVED}/{@link Status#PARTIAL}/ 011 * {@link Status#CONDITIONAL}) and carries a materialized {@link #getResolvedSql()} 012 * plus the lineage {@link #getEdges()} extracted from analyzing it, or it could not 013 * be reduced (status {@link Status#UNRESOLVED}/{@link Status#PARSE_ERROR}) and carries 014 * an honest {@link #getUnresolvedReason()} so the caller keeps it as a diagnostic 015 * rather than guessing. 016 */ 017public final class DynamicSiteResult { 018 019 public enum Kind { EXEC_STRING, SP_EXECUTESQL } 020 021 public enum Status { 022 /** Materialized to a fully concrete string; lineage edges produced. */ 023 RESOLVED, 024 /** 025 * Materialized, but the string still contains unresolved fragment(s). 026 * LEGACY/SHADOW retain historical candidate edges; SHADOW also labels 027 * their incomplete provenance. 028 */ 029 PARTIAL, 030 /** 031 * The EXEC site materialized to a usable string but is gated by an undecidable conditional 032 * (e.g. {@code IF @Debug = 0 EXEC(@sql)} with {@code @Debug} unbound), so any edge it 033 * produces is reached only <em>if the guarded statement executes</em>. The materialized 034 * string may be fully concrete (all edges emitted) or still PARTIAL (placeholder edges 035 * suppressed, only the resolvable subset emitted). The subset can legitimately be empty when 036 * every relationship touched a placeholder; like {@link #RESOLVED}, this status reflects that 037 * the SQL resolved, not that edges are guaranteed. {@link #getUnresolvedReason()} records the 038 * gate, any placeholder suppression, and whether resolvable edges were produced. Edges, if 039 * any, are correct if the branch runs. 040 */ 041 CONDITIONAL, 042 /** Could not be reduced to a usable string (NULL, opaque value, bound exceeded, undecidable control flow). */ 043 UNRESOLVED, 044 /** Materialized string failed to re-parse. */ 045 PARSE_ERROR 046 } 047 048 private final Kind kind; 049 private final Status status; 050 private final String resolvedSql; 051 private final String concreteSqlHash; 052 private final String sourceProc; 053 private final String dynamicSite; 054 private final String bindingHash; 055 private final String unresolvedReason; 056 private final String diagnostic; 057 private final long startLine; 058 private final long startColumn; 059 private final List<DynamicLineageEdge> edges; 060 private final DynamicSqlTrustMode trustMode; 061 private final int unprovenRelationshipCount; 062 private final int unresolvedHoleCount; 063 private final boolean holeCountExact; 064 065 DynamicSiteResult(Kind kind, Status status, String resolvedSql, String concreteSqlHash, String sourceProc, 066 String dynamicSite, String bindingHash, String unresolvedReason, long startLine, long startColumn, 067 List<DynamicLineageEdge> edges) { 068 this(kind, status, resolvedSql, concreteSqlHash, sourceProc, dynamicSite, bindingHash, 069 unresolvedReason, startLine, startColumn, edges, DynamicSqlTrustMode.LEGACY, 070 0, -1, false, null); 071 } 072 073 DynamicSiteResult(Kind kind, Status status, String resolvedSql, String concreteSqlHash, String sourceProc, 074 String dynamicSite, String bindingHash, String unresolvedReason, long startLine, long startColumn, 075 List<DynamicLineageEdge> edges, DynamicSqlTrustMode trustMode, 076 int unprovenRelationshipCount, 077 int unresolvedHoleCount, boolean holeCountExact) { 078 this(kind, status, resolvedSql, concreteSqlHash, sourceProc, dynamicSite, bindingHash, 079 unresolvedReason, startLine, startColumn, edges, trustMode, 080 unprovenRelationshipCount, unresolvedHoleCount, holeCountExact, null); 081 } 082 083 DynamicSiteResult(Kind kind, Status status, String resolvedSql, String concreteSqlHash, String sourceProc, 084 String dynamicSite, String bindingHash, String unresolvedReason, long startLine, long startColumn, 085 List<DynamicLineageEdge> edges, DynamicSqlTrustMode trustMode, 086 int unprovenRelationshipCount, 087 int unresolvedHoleCount, boolean holeCountExact, String diagnostic) { 088 this.kind = kind; 089 this.status = status; 090 this.resolvedSql = resolvedSql; 091 this.concreteSqlHash = concreteSqlHash; 092 this.sourceProc = sourceProc; 093 this.dynamicSite = dynamicSite; 094 this.bindingHash = bindingHash; 095 this.unresolvedReason = unresolvedReason; 096 this.diagnostic = diagnostic; 097 this.startLine = startLine; 098 this.startColumn = startColumn; 099 this.edges = edges == null ? new ArrayList<DynamicLineageEdge>() 100 : new ArrayList<DynamicLineageEdge>(edges); 101 this.trustMode = trustMode == null ? DynamicSqlTrustMode.LEGACY : trustMode; 102 this.unprovenRelationshipCount = unprovenRelationshipCount; 103 this.unresolvedHoleCount = unresolvedHoleCount; 104 this.holeCountExact = holeCountExact; 105 } 106 107 public Kind getKind() { 108 return kind; 109 } 110 111 public Status getStatus() { 112 return status; 113 } 114 115 public boolean isResolved() { 116 return status == Status.RESOLVED || status == Status.PARTIAL || status == Status.CONDITIONAL; 117 } 118 119 /** True when unresolved string fragments remain or their count is unavailable. */ 120 public boolean hasUnresolvedFragments() { 121 return !holeCountExact || unresolvedHoleCount != 0; 122 } 123 124 /** The materialized SQL string (null when unresolved with no usable text). */ 125 public String getResolvedSql() { 126 return resolvedSql; 127 } 128 129 public String getConcreteSqlHash() { 130 return concreteSqlHash; 131 } 132 133 public String getSourceProc() { 134 return sourceProc; 135 } 136 137 /** Stable id of this EXEC site (proc + ordinal path + source span), robust to reformatting. */ 138 public String getDynamicSite() { 139 return dynamicSite; 140 } 141 142 public String getBindingHash() { 143 return bindingHash; 144 } 145 146 public String getUnresolvedReason() { 147 return unresolvedReason; 148 } 149 150 /** Additive analysis note, kept separate from an unresolved reason. */ 151 public String getDiagnostic() { 152 return diagnostic; 153 } 154 155 public long getStartLine() { 156 return startLine; 157 } 158 159 public long getStartColumn() { 160 return startColumn; 161 } 162 163 public List<DynamicLineageEdge> getEdges() { 164 return Collections.unmodifiableList(edges); 165 } 166 167 public int getPublishedRelationshipCount() { 168 return edges.size(); 169 } 170 171 /** Historical candidate relationships SHADOW observed without a per-edge proof. */ 172 public int getUnprovenRelationshipCount() { 173 return unprovenRelationshipCount; 174 } 175 176 /** Exact unresolved fragment count, or {@code -1} if the producing channel cannot enumerate holes. */ 177 public int getUnresolvedHoleCount() { 178 return unresolvedHoleCount; 179 } 180 181 public boolean isHoleCountExact() { 182 return holeCountExact; 183 } 184 185 public DynamicSqlTrustMode getTrustMode() { 186 return trustMode; 187 } 188 189 @Override 190 public String toString() { 191 return "DynamicSiteResult{" + kind + ", " + status + ", site=" + dynamicSite 192 + ", edges=" + edges.size() 193 + (unresolvedReason != null ? ", reason=" + unresolvedReason : "") + "}"; 194 } 195}