001package gudusoft.gsqlparser.resolver2.binding; 002 003import gudusoft.gsqlparser.nodes.TObjectName; 004 005import java.util.IdentityHashMap; 006import java.util.Map; 007 008/** 009 * Identity-keyed store of {@link BindingTrace}s for one resolver run. 010 * 011 * <p>Owned by {@code TSQLResolver2} when 012 * {@code TSQLResolverConfig.captureBindingTrace} is enabled; {@code null} 013 * otherwise (zero overhead on the default path). Not thread-safe — a resolver 014 * instance is single-threaded and single-use, and so is its registry. 015 * 016 * <p>The resolver is iterative (up to {@code maxIterations} passes); a 017 * re-resolution of the same reference overwrites its earlier trace, so the 018 * registry ends up describing the FINAL pass — exactly what the P3 proof 019 * consumes. 020 * 021 * <p>Consumers must treat a missing trace as unprovable 022 * ({@code trace == null || !trace.complete} ⇒ refuse); never assume presence. 023 */ 024public final class BindingTraceRegistry { 025 026 private final Map<TObjectName, BindingTrace> traces = 027 new IdentityHashMap<TObjectName, BindingTrace>(); 028 029 /** Record (or overwrite with) the latest trace for a reference. */ 030 public void register(BindingTrace trace) { 031 if (trace == null || trace.reference == null) { 032 return; 033 } 034 // A FAILED scope lookup does not rebind anything (NameResolver leaves 035 // sourceTable untouched on NOT_FOUND, UNNEST special case aside), so it 036 // must not overwrite a complete DIRECT trace — e.g. OUTPUT INSERTED.a 037 // is direct-bound by ScopeBuilder and then re-looked-up through scopes, 038 // where it is legitimately NOT_FOUND. The protection covers ONLY direct 039 // kinds: a newer scope trace always replaces an older scope trace, so 040 // an exact→ambiguous re-resolution in a later pass correctly degrades 041 // the trace to incomplete instead of preserving a stale complete one. 042 BindingTrace existing = traces.get(trace.reference); 043 if (existing != null && existing.complete 044 && existing.binderKind != BindingTrace.BinderKind.SCOPE_RESOLUTION 045 && trace.binderKind == BindingTrace.BinderKind.SCOPE_RESOLUTION 046 && trace.status != gudusoft.gsqlparser.resolver2.ResolutionStatus.EXACT_MATCH) { 047 return; 048 } 049 traces.put(trace.reference, trace); 050 } 051 052 /** Drop a reference's trace (⇒ unprovable). Used by post-resolution corrections. */ 053 public void invalidate(TObjectName reference) { 054 if (reference != null) { 055 traces.remove(reference); 056 } 057 } 058 059 /** 060 * Consistency invalidation for post-resolution correction passes (QUALIFY / 061 * USING / subquery-alias / MERGE restores): those handlers may change or 062 * clear {@code sourceTable} AFTER the trace was captured. If a complete 063 * scope trace's table definition no longer matches the reference's current 064 * {@code sourceTable}, the trace is stale — drop it (fail-closed). Non-table 065 * definitions (CTE, derived table) cannot be cross-checked this way and are 066 * left to the overwrite policy. 067 */ 068 public void invalidateIfInconsistent(TObjectName reference) { 069 if (reference == null) { 070 return; 071 } 072 BindingTrace t = traces.get(reference); 073 if (t != null && t.complete 074 && t.binderKind == BindingTrace.BinderKind.SCOPE_RESOLUTION 075 && reference.getSourceTable() != t.observedSourceTable) { 076 traces.remove(reference); 077 } 078 } 079 080 /** Reset for a fresh resolver run (traces describe exactly one run). */ 081 public void clear() { 082 traces.clear(); 083 } 084 085 /** The final-pass trace for a reference, or {@code null} (⇒ unprovable). */ 086 public BindingTrace traceFor(TObjectName reference) { 087 return reference == null ? null : traces.get(reference); 088 } 089 090 public int size() { 091 return traces.size(); 092 } 093}