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 * Register a resolution that CORRECTS one already registered for the same 123 * reference, retracting the superseded bookkeeping first. 124 * 125 * <p>{@link #registerResolution} is purely additive: it overwrites the forward 126 * map but appends to {@code columnNameIndex}, the status lists, 127 * {@code tableReferences} and the counters. Calling it twice for one reference 128 * therefore leaves the OLD binding visible — after a WHERE reference is 129 * corrected from a table column to a SELECT-list alias, 130 * {@code getReferencesTo(oldTable)} would still list it, it would appear twice 131 * in {@code getExactMatches()}, and {@code totalColumnReferences} would 132 * over-count. That makes the context disagree with {@code TObjectName}, and 133 * resolver2's own data structures are supposed to be the single source of truth 134 * (MantisBT 4659).</p> 135 * 136 * <p>Safe to call when nothing was registered before — it degrades to a plain 137 * {@link #registerResolution}.</p> 138 */ 139 public void replaceResolution(TObjectName objName, ResolutionResult result) { 140 ResolutionResult previous = resolutions.get(objName); 141 if (previous != null) { 142 totalColumnReferences--; 143 144 String columnName = objName.getColumnNameOnly(); 145 if (columnName != null) { 146 List<TObjectName> byName = columnNameIndex.get(columnName); 147 if (byName != null) { 148 byName.remove(objName); 149 if (byName.isEmpty()) columnNameIndex.remove(columnName); 150 } 151 } 152 153 switch (previous.getStatus()) { 154 case EXACT_MATCH: 155 exactMatches.remove(objName); 156 if (previous.getColumnSource() != null) { 157 removeTableReference(previous.getColumnSource().getFinalTable(), objName); 158 } 159 break; 160 case AMBIGUOUS: 161 ambiguousColumns.remove(objName); 162 AmbiguousColumnSource ambiguous = previous.getAmbiguousSource(); 163 if (ambiguous != null && ambiguous.getCandidates() != null) { 164 for (ColumnSource candidate : ambiguous.getCandidates()) { 165 removeTableReference(candidate.getFinalTable(), objName); 166 } 167 } 168 break; 169 case NOT_FOUND: 170 unresolvedColumns.remove(objName); 171 break; 172 } 173 } 174 175 registerResolution(objName, result); 176 } 177 178 /** 179 * Undo one {@link #updateTableReference} contribution. 180 */ 181 private void removeTableReference(TTable table, TObjectName objName) { 182 if (table == null) return; 183 184 List<TObjectName> refs = tableReferences.get(table); 185 if (refs != null) { 186 refs.remove(objName); 187 if (refs.isEmpty()) tableReferences.remove(table); 188 } 189 190 Integer count = tableReferenceCounts.get(table); 191 if (count != null) { 192 if (count <= 1) { 193 tableReferenceCounts.remove(table); 194 } else { 195 tableReferenceCounts.put(table, count - 1); 196 } 197 } 198 } 199 200 /** 201 * Update table reference index 202 */ 203 private void updateTableReference(TTable table, TObjectName objName) { 204 if (table == null) return; 205 206 // Reverse index 207 tableReferences.computeIfAbsent(table, k -> new ArrayList<>()) 208 .add(objName); 209 210 // Reference count 211 tableReferenceCounts.merge(table, 1, Integer::sum); 212 } 213 214 215 // ===== Public query API (Level 2 API) ===== 216 217 /** 218 * Get all column references to a specific table. 219 * Complexity: O(1) 220 * 221 * @param table Target table 222 * @return List of TObjectName referencing that table 223 */ 224 public List<TObjectName> getReferencesTo(TTable table) { 225 return tableReferences.getOrDefault(table, Collections.emptyList()); 226 } 227 228 /** 229 * Get all column references to a specific table.column. 230 * Complexity: O(m) where m = number of references to the table 231 * 232 * @param table Target table 233 * @param columnName Target column name 234 * @return List of matching TObjectName 235 */ 236 public List<TObjectName> getReferencesTo(TTable table, String columnName) { 237 INameMatcher matcher = new DefaultNameMatcher(); 238 239 return getReferencesTo(table).stream() 240 .filter(obj -> { 241 String colName = obj.getColumnNameOnly(); 242 return colName != null && matcher.matches(colName, columnName); 243 }) 244 .collect(java.util.stream.Collectors.toList()); 245 } 246 247 /** 248 * Find all column references with a given name (for conflict detection). 249 * Complexity: O(1) 250 */ 251 public List<TObjectName> getColumnsByName(String columnName) { 252 return columnNameIndex.getOrDefault(columnName, Collections.emptyList()); 253 } 254 255 /** 256 * Get all exactly matched columns 257 */ 258 public List<TObjectName> getExactMatches() { 259 return Collections.unmodifiableList(exactMatches); 260 } 261 262 /** 263 * Get all ambiguous columns 264 */ 265 public List<TObjectName> getAmbiguousColumns() { 266 return Collections.unmodifiableList(ambiguousColumns); 267 } 268 269 /** 270 * Get all unresolved columns 271 */ 272 public List<TObjectName> getUnresolvedColumns() { 273 return Collections.unmodifiableList(unresolvedColumns); 274 } 275 276 /** 277 * Get reference count for a specific table 278 */ 279 public int getTableReferenceCount(TTable table) { 280 return tableReferenceCounts.getOrDefault(table, 0); 281 } 282 283 /** 284 * Get all tables that have been referenced 285 */ 286 public Set<TTable> getAllReferencedTables() { 287 return tableReferences.keySet(); 288 } 289 290 /** 291 * Get resolution statistics 292 */ 293 public ResolutionStatistics getStatistics() { 294 return new ResolutionStatistics( 295 totalColumnReferences, 296 exactMatches.size(), 297 ambiguousColumns.size(), 298 unresolvedColumns.size(), 299 tableReferences.size() 300 ); 301 } 302 303 /** 304 * Get resolution result for a specific TObjectName 305 */ 306 public ResolutionResult getResolution(TObjectName objName) { 307 return resolutions.get(objName); 308 } 309 310 /** 311 * Clear all data (for reuse or testing) 312 */ 313 public void clear() { 314 resolutions.clear(); 315 tableReferences.clear(); 316 columnNameIndex.clear(); 317 exactMatches.clear(); 318 ambiguousColumns.clear(); 319 unresolvedColumns.clear(); 320 tableReferenceCounts.clear(); 321 totalColumnReferences = 0; 322 if (columnResolutionResults != null) { 323 columnResolutionResults.clear(); 324 } 325 if (columnSkipReasons != null) { 326 columnSkipReasons.clear(); 327 } 328 } 329 330 // ===== Binding-trace API (S3, plan §7.3) ===== 331 332 /** 333 * Enable per-{@link TObjectName} binding-trace recording. 334 * 335 * <p>Allocates the identity-keyed trace and skip-reason maps on first call; 336 * subsequent calls are no-ops. {@code TSQLResolver2.resolve()} flips this 337 * on at the start of resolution when either {@code emitBindingDiagnostics} 338 * or {@code bindingIncludeSuccessfulReferences} is set.</p> 339 */ 340 public void enableBindingTrace() { 341 if (!bindingTraceEnabled) { 342 bindingTraceEnabled = true; 343 columnResolutionResults = new IdentityHashMap<>(); 344 columnSkipReasons = new IdentityHashMap<>(); 345 } 346 } 347 348 /** 349 * @return {@code true} once {@link #enableBindingTrace()} has been called 350 * for this context. 351 */ 352 public boolean isBindingTraceEnabled() { 353 return bindingTraceEnabled; 354 } 355 356 /** 357 * Get the recorded resolution for a specific {@link TObjectName} reference. 358 * 359 * <p>Identity-keyed (two references with equal {@code toString} are kept 360 * separate). Returns {@code null} when no resolution has been recorded for 361 * this reference, including when the trace is disabled.</p> 362 */ 363 public ResolutionResult getColumnResolutionResult(TObjectName objName) { 364 if (columnResolutionResults == null || objName == null) { 365 return null; 366 } 367 return columnResolutionResults.get(objName); 368 } 369 370 /** 371 * Get the skip-reason recorded for a specific {@link TObjectName} reference. 372 */ 373 public BindingSkipReason getColumnSkipReason(TObjectName objName) { 374 if (columnSkipReasons == null || objName == null) { 375 return null; 376 } 377 return columnSkipReasons.get(objName); 378 } 379 380 /** 381 * Record an explicit binding-skip reason for a reference. Silently ignored 382 * when the trace is disabled. 383 */ 384 public void recordColumnSkipReason(TObjectName objName, BindingSkipReason reason) { 385 if (!bindingTraceEnabled || objName == null || reason == null) { 386 return; 387 } 388 columnSkipReasons.put(objName, reason); 389 } 390}