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}