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    private SqlFragment(Kind kind, String text, HoleOrigin origin, String originName, String reason,
114            boolean valueUnknown, String valueOrigin, long sourceStartOffset,
115            long sourceEndOffset) {
116        this(kind, text, origin, originName, reason, valueUnknown, valueOrigin,
117                sourceStartOffset, sourceEndOffset, -1, -1, -1, -1);
118    }
119
120    private SqlFragment(Kind kind, String text, HoleOrigin origin, String originName, String reason,
121            boolean valueUnknown, String valueOrigin, long sourceStartOffset,
122            long sourceEndOffset, int sourceStartLine, int sourceStartColumn,
123            int sourceEndLine, int sourceEndColumn) {
124        this.kind = kind;
125        this.text = text == null ? "" : text;
126        this.origin = origin;
127        this.originName = originName;
128        this.reason = reason;
129        this.valueUnknown = valueUnknown;
130        this.valueOrigin = valueOrigin;
131        this.sourceStartOffset = sourceStartOffset;
132        this.sourceEndOffset = sourceEndOffset;
133        this.sourceStartLine = sourceStartLine;
134        this.sourceStartColumn = sourceStartColumn;
135        this.sourceEndLine = sourceEndLine;
136        this.sourceEndColumn = sourceEndColumn;
137    }
138
139    public static SqlFragment literal(String text) {
140        return new SqlFragment(Kind.LITERAL, text, null, null, null, false,
141                null, -1L, -1L);
142    }
143
144    /** Exact literal value with its procedure-body seed provenance. */
145    public static SqlFragment literal(String text, String valueOrigin,
146            long sourceStartOffset, long sourceEndOffset) {
147        return new SqlFragment(Kind.LITERAL, text, null, null, null, false,
148                valueOrigin, sourceStartOffset, sourceEndOffset);
149    }
150
151    /**
152     * Hole with the default value-knowledge for its origin: every origin except
153     * {@code INEXACT_TRANSFORM} stands in for a value nobody knows; an inexact transform
154     * defaults to value-known (computed from literals). A transform over a PARTIAL input
155     * must use the explicit overload with {@code valueUnknown=true}.
156     */
157    public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason) {
158        HoleOrigin resolved = origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin;
159        return new SqlFragment(Kind.HOLE, renderedText, resolved, originName, reason,
160                resolved != HoleOrigin.INEXACT_TRANSFORM, null, -1L, -1L);
161    }
162
163    public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason,
164            boolean valueUnknown) {
165        return new SqlFragment(Kind.HOLE, renderedText,
166                origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin, originName, reason, valueUnknown,
167                null, -1L, -1L);
168    }
169
170    /**
171     * Hole with full provenance: the data source its value would come from
172     * ({@code valueOrigin}, may be null) and the origin span in the analyzed
173     * text (see {@link #sourceStartOffset} / {@link #sourceStartLine}; pass
174     * {@code -1} for unknown).
175     */
176    public static SqlFragment hole(String renderedText, HoleOrigin origin, String originName, String reason,
177            boolean valueUnknown, String valueOrigin, long sourceStartOffset, long sourceEndOffset,
178            int sourceStartLine, int sourceStartColumn, int sourceEndLine, int sourceEndColumn) {
179        return new SqlFragment(Kind.HOLE, renderedText,
180                origin == null ? HoleOrigin.UNSUPPORTED_EXPR : origin, originName, reason, valueUnknown,
181                valueOrigin, sourceStartOffset, sourceEndOffset,
182                sourceStartLine, sourceStartColumn, sourceEndLine, sourceEndColumn);
183    }
184
185    /** True when the origin span is known in line / column form. */
186    public boolean hasSourceSpan() {
187        return sourceStartLine > 0 && sourceStartColumn > 0;
188    }
189
190    public boolean isLiteral() {
191        return kind == Kind.LITERAL;
192    }
193
194    boolean hasSameLiteralProvenance(SqlFragment other) {
195        if (other == null || !isLiteral() || !other.isLiteral()) {
196            return false;
197        }
198        if (valueOrigin == null ? other.valueOrigin != null : !valueOrigin.equals(other.valueOrigin)) {
199            return false;
200        }
201        return sourceStartOffset == other.sourceStartOffset
202                && sourceEndOffset == other.sourceEndOffset;
203    }
204
205    @Override
206    public String toString() {
207        return kind == Kind.LITERAL
208                ? "L[" + text + "]"
209                : "H[" + text + " <" + origin + (originName != null ? ":" + originName : "") + ">]";
210    }
211}