001package gudusoft.gsqlparser.dlineage.dynamicsql;
002
003/**
004 * One provenance-tagged fragment of a materialized dynamic-SQL string.
005 *
006 * <p>A materialized string is an ordered fragment sequence whose concatenated
007 * {@link #text} equals the rendered SQL exactly. Each fragment is either:
008 * <ul>
009 *   <li>{@link Kind#LITERAL} — text proven to be the exact runtime characters:
010 *       a string literal, or the output of a transform proven byte-exact for
011 *       the vendor;</li>
012 *   <li>{@link Kind#HOLE} — text standing in for a value the evaluator could
013 *       not prove exactly: an unbound variable's own token, an unfoldable
014 *       expression's placeholder, or the output of a transform that is folded
015 *       for display but NOT proven byte-exact ({@link HoleOrigin#INEXACT_TRANSFORM}).</li>
016 * </ul>
017 *
018 * <p>Consumers (the per-edge publication proof, design doc
019 * {@code dynamic-sql-fragment-provenance-design.md}) must treat every HOLE as
020 * arbitrary runtime text; no lineage evidence may rest on HOLE characters. The
021 * legacy {@code SqlStringValue.state}/{@code text} contract is unchanged —
022 * fragments are additional provenance carried alongside it.
023 */
024public final class SqlFragment {
025
026    /** Fragment class: exact literal text vs. unproven stand-in text. */
027    public enum Kind { LITERAL, HOLE }
028
029    /** Why a HOLE exists — which producer limitation created it. */
030    public enum HoleOrigin {
031        /** A variable/parameter never bound and never assigned a foldable value. */
032        UNBOUND_VARIABLE,
033        /** A bare (non-variable) identifier used as a value — data-dependent. */
034        NON_VARIABLE_IDENT,
035        /** A function call the evaluator does not fold. */
036        OPAQUE_CALL,
037        /** An expression form the evaluator does not model. */
038        UNSUPPORTED_EXPR,
039        /** Evaluation hit a configured resource bound. */
040        RESOURCE_LIMIT,
041        /**
042         * A transform was folded for display but its output is not proven
043         * byte-exact against vendor runtime semantics (CAST/CONVERT padding and
044         * styles, collation-aware REPLACE/UPPER/LOWER, whitespace-class TRIM,
045         * non-default QUOTENAME delimiters, numeric rendering).
046         */
047        INEXACT_TRANSFORM,
048        /**
049         * A variable whose last visible assignment read from a query
050         * ({@code SELECT @v = col FROM t ...}): its value is runtime data. The
051         * hole keeps the assignment's own rendering (the source column name as a
052         * placeholder) but is named after the VARIABLE, and {@link #valueOrigin}
053         * describes the column / expression the value would come from.
054         */
055        QUERY_ASSIGNED_VARIABLE,
056        /**
057         * A variable whose value depends on control flow the evaluator cannot
058         * decide: assigned inside an undecidable IF / WHILE branch, declared
059         * there, or assigned by a driver loop whose iteration is unknown.
060         */
061        FLOW_DEPENDENT_VARIABLE
062    }
063
064    public final Kind kind;
065    /** Exact rendered text this fragment contributes to the materialized SQL. Never null. */
066    public final String text;
067    /** HOLE only: why the hole exists; null for LITERAL. */
068    public final HoleOrigin origin;
069    /** HOLE only: the variable/function/expression name that caused it; may be null. */
070    public final String originName;
071    /** HOLE only: human-readable cause; null for LITERAL. */
072    public final String reason;
073    /**
074     * HOLE only: true when the hole's runtime VALUE is unknown (an unbound variable, or a
075     * transform applied to one). False for an {@code INEXACT_TRANSFORM} computed entirely
076     * from literals, where the value is known and only its byte-exactness against vendor
077     * runtime semantics is unproven. The distinction matters to consumers deciding whether
078     * a folded identifier is a runtime TEMPLATE ({@code UPPER(@X)} — unknown) or merely an
079     * unproven rendering of a known name ({@code UPPER('src')} — known). Always false for
080     * LITERAL fragments.
081     */
082    public final boolean valueUnknown;
083    /**
084     * Where the value came from. LITERAL: the seed of an exact value — null for
085     * ordinary SQL string fragments; constant-relation values use a stable
086     * description such as {@code INSERT VALUES row 2 of @Source}. HOLE: the
087     * data source the unknown value would be read from, when the evaluator saw
088     * it ({@code dbo.SSF_Entity.Entity} for a {@link HoleOrigin#QUERY_ASSIGNED_VARIABLE});
089     * null when unknown.
090     */
091    public final String valueOrigin;
092    /**
093     * Source offsets, end-exclusive, of the fragment's origin in the analyzed
094     * text: for a LITERAL the seeding expression; for a HOLE the place the
095     * value was last defined — the assignment target of a query-assigned or
096     * flow-dependent variable, the declaration of an unbound parameter or of a
097     * declared-but-unassigned variable, otherwise the reference that produced
098     * the hole. {@code -1} when the origin is not in the analyzed text.
099     */
100    public final long sourceStartOffset;
101    public final long sourceEndOffset;
102    /**
103     * The same origin span as line / column: 1-based, end-exclusive end column,
104     * in the coordinate system of {@code TSourceToken.lineNo / columnNo}.
105     * {@code -1} when unknown. Populated for HOLE fragments whose origin is a
106     * source node; LITERAL fragments carry offsets only.
107     */
108    public final int sourceStartLine;
109    public final int sourceStartColumn;
110    public final int sourceEndLine;
111    public final int sourceEndColumn;
112    /**
113     * LITERAL only: where the characters of {@link #text} sit in the analyzed source, one piece per string literal
114     * token they were read from (Mantis 4851); empty when unknown, as for the output of a transform. Positional
115     * metadata only, no part of the fragment's provenance: merging and {@link #hasSameLiteralProvenance} ignore it.
116     */
117    public final java.util.List<SourcePiece> pieces;
118
119    private SqlFragment(Kind kind, String text, HoleOrigin origin, String originName, String reason,
120            boolean valueUnknown, String valueOrigin, long sourceStartOffset,
121            long sourceEndOffset) {
122        this(kind, text, origin, originName, reason, valueUnknown, valueOrigin,
123                sourceStartOffset, sourceEndOffset, -1, -1, -1, -1);
124    }
125
126    private SqlFragment(Kind kind, String text, HoleOrigin origin, String originName, String reason,
127            boolean valueUnknown, String valueOrigin, long sourceStartOffset,
128            long sourceEndOffset, int sourceStartLine, int sourceStartColumn,
129            int sourceEndLine, int sourceEndColumn) {
130        this(kind, text, origin, originName, reason, valueUnknown, valueOrigin, sourceStartOffset, sourceEndOffset,
131                sourceStartLine, sourceStartColumn, sourceEndLine, sourceEndColumn,
132                java.util.Collections.<SourcePiece>emptyList());
133    }
134
135    private SqlFragment(Kind kind, String text, HoleOrigin origin, String originName, String reason,
136            boolean valueUnknown, String valueOrigin, long sourceStartOffset,
137            long sourceEndOffset, int sourceStartLine, int sourceStartColumn,
138            int sourceEndLine, int sourceEndColumn, java.util.List<SourcePiece> pieces) {
139        this.pieces = pieces;
140        this.kind = kind;
141        this.text = text == null ? "" : text;
142        this.origin = origin;
143        this.originName = originName;
144        this.reason = reason;
145        this.valueUnknown = valueUnknown;
146        this.valueOrigin = valueOrigin;
147        this.sourceStartOffset = sourceStartOffset;
148        this.sourceEndOffset = sourceEndOffset;
149        this.sourceStartLine = sourceStartLine;
150        this.sourceStartColumn = sourceStartColumn;
151        this.sourceEndLine = sourceEndLine;
152        this.sourceEndColumn = sourceEndColumn;
153    }
154
155    public static SqlFragment literal(String text) {
156        return new SqlFragment(Kind.LITERAL, text, null, null, null, false,
157                null, -1L, -1L);
158    }
159
160    /** Exact literal value with its procedure-body seed provenance. */
161    public static SqlFragment literal(String text, String valueOrigin,
162            long sourceStartOffset, long sourceEndOffset) {
163        return new SqlFragment(Kind.LITERAL, text, null, null, null, false,
164                valueOrigin, sourceStartOffset, sourceEndOffset);
165    }
166
167    /** Exact literal value read from one string literal token, with where its characters sit (Mantis 4851). */
168    public static SqlFragment literal(String text, SourcePiece piece) {
169        return piece == null ? literal(text)
170                : new SqlFragment(Kind.LITERAL, text, null, null, null, false, null, -1L, -1L, -1, -1, -1, -1,
171                        java.util.Collections.singletonList(piece));
172    }
173
174    /**
175     * The LITERAL that {@code a} followed by {@code b} make when merged: {@code a}'s provenance, as before, and the
176     * pieces of both, those of {@code b} moved past {@code a}'s text.
177     */
178    static SqlFragment mergedLiteral(SqlFragment a, SqlFragment b) {
179        java.util.List<SourcePiece> pieces = java.util.Collections.emptyList();
180        if (!a.pieces.isEmpty() || !b.pieces.isEmpty()) {
181            pieces = new java.util.ArrayList<SourcePiece>(a.pieces.size() + b.pieces.size());
182            pieces.addAll(a.pieces);
183            for (SourcePiece p : b.pieces) pieces.add(p.shifted(a.text.length()));
184        }
185        return new SqlFragment(Kind.LITERAL, a.text + b.text, null, null, null, false, a.valueOrigin,
186                a.valueOrigin == null ? -1L : a.sourceStartOffset, a.valueOrigin == null ? -1L : a.sourceEndOffset,
187                -1, -1, -1, -1, pieces);
188    }
189
190    /**
191     * {@code length} characters of a LITERAL's text, from {@code textStart}, read from one string literal token of the
192     * analyzed source (Mantis 4851): where each sits there, so that what GSP analyzes inside the string reports
193     * positions in the source. {@code ''} is one character of the text and two of the source; the token keeps its
194     * quotes, a one-letter prefix such as N, and its line breaks.
195     */
196    public static final class SourcePiece {
197        public final int textStart;
198        public final int length;
199        private final long tokenLine;
200        private final long tokenColumn;
201        private final String tokenText;
202        private final int contentStart;
203
204        private SourcePiece(int textStart, int length, long tokenLine, long tokenColumn, String tokenText,
205                int contentStart) {
206            this.textStart = textStart;
207            this.length = length;
208            this.tokenLine = tokenLine;
209            this.tokenColumn = tokenColumn;
210            this.tokenText = tokenText;
211            this.contentStart = contentStart;
212        }
213
214        /**
215         * The piece of string literal token {@code token} whose content decodes to {@code decoded}: null unless the
216         * token is a single-quoted literal (with at most a one-letter prefix) whose content, {@code ''} read as one
217         * quote, is exactly {@code decoded}.
218         */
219        public static SourcePiece of(gudusoft.gsqlparser.TSourceToken token, String decoded) {
220            if (token == null || decoded == null || token.lineNo <= 0 || token.columnNo <= 0) return null;
221            String raw = token.toString();
222            if (raw == null) return null;
223            int q = raw.indexOf('\'');
224            if (q < 0 || q > 1) return null;
225            StringBuilder content = new StringBuilder();
226            int j = q + 1;
227            boolean closed = false;
228            while (j < raw.length()) {
229                char ch = raw.charAt(j);
230                if (ch == '\'') {
231                    if (j + 1 < raw.length() && raw.charAt(j + 1) == '\'') {
232                        content.append('\'');
233                        j += 2;
234                        continue;
235                    }
236                    closed = true;
237                    j++;
238                    break;
239                }
240                content.append(ch);
241                j++;
242            }
243            if (!closed || j != raw.length() || !content.toString().equals(decoded)) return null;
244            return new SourcePiece(0, decoded.length(), token.lineNo, token.columnNo, raw, q + 1);
245        }
246
247        SourcePiece shifted(int by) {
248            return new SourcePiece(textStart + by, length, tokenLine, tokenColumn, tokenText, contentStart);
249        }
250
251        /**
252         * Writes the source line and column of each character of this piece into {@code lines} and {@code columns},
253         * at index {@code base + textStart + k} for its k-th character; returns the position just past its last
254         * character, the literal's closing quote, where a value concatenated after it is spliced in.
255         */
256        public long[] fill(long[] lines, long[] columns, int base) {
257            long line = tokenLine, column = tokenColumn;
258            int j = 0;
259            for (; j < contentStart; j++) {
260                if (breaksLine(tokenText, j)) { line++; column = 1; } else { column++; }
261            }
262            for (int k = 0; k < length && j < tokenText.length(); k++) {
263                int at = base + textStart + k;
264                if (at >= 0 && at < lines.length) {
265                    lines[at] = line;
266                    columns[at] = column;
267                }
268                char ch = tokenText.charAt(j);
269                if (ch == '\'' && j + 1 < tokenText.length() && tokenText.charAt(j + 1) == '\'') {
270                    j += 2;
271                    column += 2;
272                } else {
273                    if (breaksLine(tokenText, j)) { line++; column = 1; } else { column++; }
274                    j++;
275                }
276            }
277            return new long[] {line, column};
278        }
279
280        /** Whether character {@code j} ends a line as the lexer counts lines: LF, or a CR not followed by LF. */
281        private static boolean breaksLine(String text, int j) {
282            char ch = text.charAt(j);
283            return ch == '\n' || (ch == '\r' && (j + 1 >= text.length() || text.charAt(j + 1) != '\n'));
284        }
285    }
286
287    /**
288     * Hole with the default value-knowledge for its origin: every origin except
289     * {@code INEXACT_TRANSFORM} stands in for a value nobody knows; an inexact transform
290     * defaults to value-known (computed from literals). A transform over a PARTIAL input
291     * must use the explicit overload with {@code valueUnknown=true}.
292     */
293    public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason) {
294        HoleOrigin resolved = origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin;
295        return new SqlFragment(Kind.HOLE, renderedText, resolved, originName, reason,
296                resolved != HoleOrigin.INEXACT_TRANSFORM, null, -1L, -1L);
297    }
298
299    public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason,
300            boolean valueUnknown) {
301        return new SqlFragment(Kind.HOLE, renderedText,
302                origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin, originName, reason, valueUnknown,
303                null, -1L, -1L);
304    }
305
306    /**
307     * Hole with full provenance: the data source its value would come from
308     * ({@code valueOrigin}, may be null) and the origin span in the analyzed
309     * text (see {@link #sourceStartOffset} / {@link #sourceStartLine}; pass
310     * {@code -1} for unknown).
311     */
312    public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason,
313            boolean valueUnknown, String valueOrigin, long sourceStartOffset, long sourceEndOffset,
314            int sourceStartLine, int sourceStartColumn, int sourceEndLine, int sourceEndColumn) {
315        return new SqlFragment(Kind.HOLE, renderedText,
316                origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin, originName, reason, valueUnknown,
317                valueOrigin, sourceStartOffset, sourceEndOffset,
318                sourceStartLine, sourceStartColumn, sourceEndLine, sourceEndColumn);
319    }
320
321    /** True when the origin span is known in line / column form. */
322    public boolean hasSourceSpan() {
323        return sourceStartLine > 0 && sourceStartColumn > 0;
324    }
325
326    public boolean isLiteral() {
327        return kind == Kind.LITERAL;
328    }
329
330    boolean hasSameLiteralProvenance(SqlFragment other) {
331        if (other == null || !isLiteral() || !other.isLiteral()) {
332            return false;
333        }
334        if (valueOrigin == null ? other.valueOrigin != null : !valueOrigin.equals(other.valueOrigin)) {
335            return false;
336        }
337        return sourceStartOffset == other.sourceStartOffset
338                && sourceEndOffset == other.sourceEndOffset;
339    }
340
341    @Override
342    public String toString() {
343        return kind == Kind.LITERAL
344                ? "L[" + text + "]"
345                : "H[" + text + " <" + origin + (originName != null ? ":" + originName : "") + ">]";
346    }
347}