001package gudusoft.gsqlparser.resolver2.binding;
002
003import gudusoft.gsqlparser.nodes.TObjectName;
004import gudusoft.gsqlparser.nodes.TTable;
005import gudusoft.gsqlparser.resolver2.ResolutionStatus;
006import gudusoft.gsqlparser.resolver2.namespace.INamespace;
007
008/**
009 * Immutable record of HOW one name reference was bound during the final
010 * resolver pass (design: {@code dynamic-sql-fragment-provenance-design.md},
011 * R3 / R4 step 3).
012 *
013 * <p>The dynamic-SQL per-edge publication proof (P3) needs the full binding
014 * chain of every evidence reference, and its rule is fail-closed on
015 * completeness: {@code trace == null || !trace.complete} ⇒ the reference is
016 * unprovable. A missing trace is therefore always SAFE — it can only shrink
017 * the published-edge set, never let an unproven edge through. This is why the
018 * many direct {@code setSourceTable} sites in ScopeBuilder do not all need
019 * instrumenting at once: uninstrumented paths simply yield no trace.
020 *
021 * <p>Captured today:
022 * <ul>
023 *   <li>the scope-machinery choke point ({@code NameResolver.resolve}) —
024 *       records the winning match's namespace, scope, resolve path and the
025 *       climb depth from the starting scope (correlated references);</li>
026 *   <li>the typed direct-binding clusters review round 2 named: OUTPUT
027 *       {@code INSERTED}/{@code DELETED}, trigger pseudo-tables, MERGE
028 *       target/SET/INSERT columns and MERGE USING values.</li>
029 * </ul>
030 *
031 * <p>Capture is off by default ({@code TSQLResolverConfig.captureBindingTrace})
032 * and observation-only: nothing in resolution reads a trace.
033 */
034public final class BindingTrace {
035
036    /** What kind of binder resolved the reference. */
037    public enum BinderKind {
038        /** Resolved through the scope machinery (namespace recorded). */
039        SCOPE_RESOLUTION,
040        /** Directly bound to a DML target pseudo-table (OUTPUT INSERTED/DELETED, trigger). */
041        DML_TARGET,
042        /** Directly bound to the MERGE target (SET / INSERT column list). */
043        MERGE_TARGET,
044        /** Directly bound to the MERGE USING source (VALUES expressions). */
045        MERGE_USING
046    }
047
048    /** The reference this trace describes (identity key in the registry). */
049    public final TObjectName reference;
050    public final BinderKind binderKind;
051    /** Scope-machinery outcome; {@code null} for direct bindings. */
052    public final ResolutionStatus status;
053    /** Winning namespace; {@code null} for direct bindings or failed resolution. */
054    public final INamespace namespace;
055    /** Direct-binding target table; {@code null} for scope resolution. */
056    public final TTable directTarget;
057    /**
058     * The AST node that DEFINES the binder: the {@code TTable} of a table
059     * namespace, the {@code TCTE} of a CTE namespace (header column lists
060     * included), the {@code TSelectSqlStatement} of a derived table, or the
061     * direct-binding target table. This is what the publication proof checks
062     * spans against; a trace without it cannot be complete.
063     */
064    public final Object definitionNode;
065    /**
066     * The reference's {@code sourceTable} AS OF capture time. Two roles:
067     * (a) staleness signal — post-resolution correction handlers (QUALIFY /
068     * USING / subquery-alias / MERGE restores) may change or clear it, and the
069     * registry invalidates the trace when the current value differs by
070     * identity; (b) the FROM-reference wrapper node of the binding chain — for
071     * CTE and derived-table binds this is the {@code TTable} carrying the
072     * {@code AS alias}, whose span the definition node alone does not cover.
073     * A complete scope trace requires it.
074     */
075    public final TTable observedSourceTable;
076    /** Scope-type name where resolution started; {@code null} for direct bindings. */
077    public final String startingScope;
078    /** Scope-type name where the winning match lives; {@code null} when unknown. */
079    public final String matchedScope;
080    /** The winning match's resolve path (display form); {@code null} when unknown. */
081    public final String resolvePath;
082    /** Number of matches the scope machinery reported (1 for a clean bind). */
083    public final int matchCount;
084    /**
085     * Parent-scope hops from the starting scope to the matched scope; 0 for a
086     * local bind, &gt; 0 for correlated references, -1 when the matched scope
087     * was not on the starting scope's parent chain (unknown — incomplete).
088     */
089    public final int scopeClimbDepth;
090    /**
091     * TRUE iff the ENTIRE binding chain is known: an exact single-match scope
092     * resolution with a recorded namespace, fully consumed name parts and a
093     * known climb path — or a typed direct binding with a known target. The
094     * P3 rule refuses anything else.
095     */
096    public final boolean complete;
097
098    private BindingTrace(TObjectName reference, BinderKind binderKind, ResolutionStatus status,
099            INamespace namespace, TTable directTarget, Object definitionNode,
100            TTable observedSourceTable, String startingScope,
101            String matchedScope, String resolvePath, int matchCount, int scopeClimbDepth,
102            boolean complete) {
103        this.reference = reference;
104        this.binderKind = binderKind;
105        this.status = status;
106        this.namespace = namespace;
107        this.directTarget = directTarget;
108        this.definitionNode = definitionNode;
109        this.observedSourceTable = observedSourceTable;
110        this.startingScope = startingScope;
111        this.matchedScope = matchedScope;
112        this.resolvePath = resolvePath;
113        this.matchCount = matchCount;
114        this.scopeClimbDepth = scopeClimbDepth;
115        this.complete = complete;
116    }
117
118    /**
119     * Scope-machinery capture (NameResolver choke point). Complete only for an
120     * exact single match with a recorded namespace AND its definition node, at
121     * most the terminal column segment unconsumed, a known climb path, and NO
122     * fallback involved (a struct-field fallback replaces the result after the
123     * recorded match, so the match data no longer describes the binding).
124     */
125    public static BindingTrace scopeResolution(TObjectName reference, ResolutionStatus status,
126            INamespace namespace, Object definitionNode, TTable observedSourceTable,
127            String startingScope, String matchedScope,
128            String resolvePath, int matchCount, int scopeClimbDepth, boolean fullyConsumed,
129            boolean exactMatch, boolean fallbackUsed) {
130        // observedSourceTable is ALSO the FROM-reference wrapper node (the
131        // TTable carrying "AS alias") — the alias is part of the binding
132        // chain, and the definition node alone (TCTE / derived-table SELECT)
133        // does not cover its span. A complete chain requires BOTH.
134        boolean complete = exactMatch && matchCount == 1 && namespace != null
135                && definitionNode != null && observedSourceTable != null
136                && fullyConsumed && scopeClimbDepth >= 0
137                && !fallbackUsed;
138        return new BindingTrace(reference, BinderKind.SCOPE_RESOLUTION, status, namespace, null,
139                definitionNode, observedSourceTable, startingScope, matchedScope, resolvePath,
140                matchCount, scopeClimbDepth, complete);
141    }
142
143    /** Typed direct-binding capture (ScopeBuilder clusters). */
144    public static BindingTrace direct(TObjectName reference, BinderKind kind, TTable target) {
145        return new BindingTrace(reference, kind, null, null, target, target, target,
146                null, null, null, 1, 0, target != null);
147    }
148
149    @Override
150    public String toString() {
151        return "BindingTrace{" + binderKind + ", complete=" + complete
152                + (status != null ? ", status=" + status : "")
153                + (resolvePath != null ? ", path=" + resolvePath : "")
154                + (directTarget != null ? ", target=" + directTarget.getName() : "")
155                + ", climb=" + scopeClimbDepth + ", matches=" + matchCount + '}';
156    }
157}