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, > 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}