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}