001package gudusoft.gsqlparser.resolver2.model; 002 003import gudusoft.gsqlparser.nodes.TObjectName; 004import gudusoft.gsqlparser.nodes.TTable; 005import gudusoft.gsqlparser.resolver2.binding.BindingSkipReason; 006import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 007import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher; 008 009import java.util.*; 010 011/** 012 * Global context for resolution results. 013 * Provides efficient querying and statistics about resolved columns. 014 * 015 * Design principles: 016 * 1. Reverse indexing for O(1) queries 017 * 2. Categorized storage for quick access to different resolution states 018 * 3. Statistics for monitoring resolution quality 019 */ 020public class ResolutionContext { 021 022 // === Core storage: forward mapping === 023 024 /** All processed column references -> resolution results */ 025 private final Map<TObjectName, ResolutionResult> resolutions = new LinkedHashMap<>(); 026 027 // === Reverse indexes for efficient queries === 028 029 /** Table -> all columns referencing that table */ 030 private final Map<TTable, List<TObjectName>> tableReferences = new HashMap<>(); 031 032 /** Column name -> all column references with that name (for conflict detection) */ 033 private final Map<String, List<TObjectName>> columnNameIndex = new HashMap<>(); 034 035 // === Categorized lists for quick access === 036 037 /** Exactly matched columns (EXACT_MATCH) */ 038 private final List<TObjectName> exactMatches = new ArrayList<>(); 039 040 /** Ambiguous columns (AMBIGUOUS) */ 041 private final List<TObjectName> ambiguousColumns = new ArrayList<>(); 042 043 /** Unresolved columns (NOT_FOUND) */ 044 private final List<TObjectName> unresolvedColumns = new ArrayList<>(); 045 046 // === Statistics === 047 048 /** Table reference counts */ 049 private final Map<TTable, Integer> tableReferenceCounts = new HashMap<>(); 050 051 /** Total column references processed */ 052 private int totalColumnReferences = 0; 053 054 // === Binding-trace state (S3) === 055 // 056 // Identity-keyed per-TObjectName trace, used by BindingDiagnosticPostPass 057 // (S5+) as deterministic input. Both maps stay null until enableBindingTrace() 058 // is called by TSQLResolver2 — keeping the default-off path allocation-free 059 // (plan §10.1 perf gate). IdentityHashMap on purpose: two TObjectName 060 // instances with equal toString must NOT collide. 061 062 private boolean bindingTraceEnabled = false; 063 private IdentityHashMap<TObjectName, ResolutionResult> columnResolutionResults; 064 private IdentityHashMap<TObjectName, BindingSkipReason> columnSkipReasons; 065 066 067 // ===== Internal methods: called by NameResolver ===== 068 069 /** 070 * Register a resolution result. 071 * Called by NameResolver when a column is resolved. 072 */ 073 public void registerResolution(TObjectName objName, ResolutionResult result) { 074 // 1. Forward storage 075 resolutions.put(objName, result); 076 totalColumnReferences++; 077 078 // 2. Update reverse index by column name 079 String columnName = objName.getColumnNameOnly(); 080 if (columnName != null) { 081 columnNameIndex.computeIfAbsent(columnName, k -> new ArrayList<>()) 082 .add(objName); 083 } 084 085 // 3. Categorize by status 086 switch (result.getStatus()) { 087 case EXACT_MATCH: 088 exactMatches.add(objName); 089 ColumnSource source = result.getColumnSource(); 090 if (source != null) { 091 updateTableReference(source.getFinalTable(), objName); 092 } 093 break; 094 095 case AMBIGUOUS: 096 ambiguousColumns.add(objName); 097 // For ambiguous: all candidates count as references 098 AmbiguousColumnSource ambiguous = result.getAmbiguousSource(); 099 if (ambiguous != null) { 100 for (ColumnSource candidate : ambiguous.getCandidates()) { 101 updateTableReference(candidate.getFinalTable(), objName); 102 } 103 } 104 break; 105 106 case NOT_FOUND: 107 unresolvedColumns.add(objName); 108 break; 109 } 110 111 // Binding-trace tap (S3). All resolver call sites flow through 112 // registerResolution — both NameResolver.resolve() and the four 113 // bypass paths in TSQLResolver2 (USING-left, USING-right, Teradata 114 // NAMED alias, QUALIFY clause alias) — so a single put here covers 115 // every reference that produces a ResolutionResult. 116 if (bindingTraceEnabled && columnResolutionResults != null && objName != null) { 117 columnResolutionResults.put(objName, result); 118 } 119 } 120 121 /** 122 * Update table reference index 123 */ 124 private void updateTableReference(TTable table, TObjectName objName) { 125 if (table == null) return; 126 127 // Reverse index 128 tableReferences.computeIfAbsent(table, k -> new ArrayList<>()) 129 .add(objName); 130 131 // Reference count 132 tableReferenceCounts.merge(table, 1, Integer::sum); 133 } 134 135 136 // ===== Public query API (Level 2 API) ===== 137 138 /** 139 * Get all column references to a specific table. 140 * Complexity: O(1) 141 * 142 * @param table Target table 143 * @return List of TObjectName referencing that table 144 */ 145 public List<TObjectName> getReferencesTo(TTable table) { 146 return tableReferences.getOrDefault(table, Collections.emptyList()); 147 } 148 149 /** 150 * Get all column references to a specific table.column. 151 * Complexity: O(m) where m = number of references to the table 152 * 153 * @param table Target table 154 * @param columnName Target column name 155 * @return List of matching TObjectName 156 */ 157 public List<TObjectName> getReferencesTo(TTable table, String columnName) { 158 INameMatcher matcher = new DefaultNameMatcher(); 159 160 return getReferencesTo(table).stream() 161 .filter(obj -> { 162 String colName = obj.getColumnNameOnly(); 163 return colName != null && matcher.matches(colName, columnName); 164 }) 165 .collect(java.util.stream.Collectors.toList()); 166 } 167 168 /** 169 * Find all column references with a given name (for conflict detection). 170 * Complexity: O(1) 171 */ 172 public List<TObjectName> getColumnsByName(String columnName) { 173 return columnNameIndex.getOrDefault(columnName, Collections.emptyList()); 174 } 175 176 /** 177 * Get all exactly matched columns 178 */ 179 public List<TObjectName> getExactMatches() { 180 return Collections.unmodifiableList(exactMatches); 181 } 182 183 /** 184 * Get all ambiguous columns 185 */ 186 public List<TObjectName> getAmbiguousColumns() { 187 return Collections.unmodifiableList(ambiguousColumns); 188 } 189 190 /** 191 * Get all unresolved columns 192 */ 193 public List<TObjectName> getUnresolvedColumns() { 194 return Collections.unmodifiableList(unresolvedColumns); 195 } 196 197 /** 198 * Get reference count for a specific table 199 */ 200 public int getTableReferenceCount(TTable table) { 201 return tableReferenceCounts.getOrDefault(table, 0); 202 } 203 204 /** 205 * Get all tables that have been referenced 206 */ 207 public Set<TTable> getAllReferencedTables() { 208 return tableReferences.keySet(); 209 } 210 211 /** 212 * Get resolution statistics 213 */ 214 public ResolutionStatistics getStatistics() { 215 return new ResolutionStatistics( 216 totalColumnReferences, 217 exactMatches.size(), 218 ambiguousColumns.size(), 219 unresolvedColumns.size(), 220 tableReferences.size() 221 ); 222 } 223 224 /** 225 * Get resolution result for a specific TObjectName 226 */ 227 public ResolutionResult getResolution(TObjectName objName) { 228 return resolutions.get(objName); 229 } 230 231 /** 232 * Clear all data (for reuse or testing) 233 */ 234 public void clear() { 235 resolutions.clear(); 236 tableReferences.clear(); 237 columnNameIndex.clear(); 238 exactMatches.clear(); 239 ambiguousColumns.clear(); 240 unresolvedColumns.clear(); 241 tableReferenceCounts.clear(); 242 totalColumnReferences = 0; 243 if (columnResolutionResults != null) { 244 columnResolutionResults.clear(); 245 } 246 if (columnSkipReasons != null) { 247 columnSkipReasons.clear(); 248 } 249 } 250 251 // ===== Binding-trace API (S3, plan §7.3) ===== 252 253 /** 254 * Enable per-{@link TObjectName} binding-trace recording. 255 * 256 * <p>Allocates the identity-keyed trace and skip-reason maps on first call; 257 * subsequent calls are no-ops. {@code TSQLResolver2.resolve()} flips this 258 * on at the start of resolution when either {@code emitBindingDiagnostics} 259 * or {@code bindingIncludeSuccessfulReferences} is set.</p> 260 */ 261 public void enableBindingTrace() { 262 if (!bindingTraceEnabled) { 263 bindingTraceEnabled = true; 264 columnResolutionResults = new IdentityHashMap<>(); 265 columnSkipReasons = new IdentityHashMap<>(); 266 } 267 } 268 269 /** 270 * @return {@code true} once {@link #enableBindingTrace()} has been called 271 * for this context. 272 */ 273 public boolean isBindingTraceEnabled() { 274 return bindingTraceEnabled; 275 } 276 277 /** 278 * Get the recorded resolution for a specific {@link TObjectName} reference. 279 * 280 * <p>Identity-keyed (two references with equal {@code toString} are kept 281 * separate). Returns {@code null} when no resolution has been recorded for 282 * this reference, including when the trace is disabled.</p> 283 */ 284 public ResolutionResult getColumnResolutionResult(TObjectName objName) { 285 if (columnResolutionResults == null || objName == null) { 286 return null; 287 } 288 return columnResolutionResults.get(objName); 289 } 290 291 /** 292 * Get the skip-reason recorded for a specific {@link TObjectName} reference. 293 */ 294 public BindingSkipReason getColumnSkipReason(TObjectName objName) { 295 if (columnSkipReasons == null || objName == null) { 296 return null; 297 } 298 return columnSkipReasons.get(objName); 299 } 300 301 /** 302 * Record an explicit binding-skip reason for a reference. Silently ignored 303 * when the trace is disabled. 304 */ 305 public void recordColumnSkipReason(TObjectName objName, BindingSkipReason reason) { 306 if (!bindingTraceEnabled || objName == null || reason == null) { 307 return; 308 } 309 columnSkipReasons.put(objName, reason); 310 } 311}