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}