001package gudusoft.gsqlparser.resolver2.namespace;
002
003import gudusoft.gsqlparser.nodes.TObjectName;
004import gudusoft.gsqlparser.nodes.TTable;
005import gudusoft.gsqlparser.resolver2.ColumnLevel;
006import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
007import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher;
008import gudusoft.gsqlparser.resolver2.matcher.VendorNameMatcher;
009import gudusoft.gsqlparser.resolver2.model.ColumnReference;
010import gudusoft.gsqlparser.resolver2.model.ColumnSource;
011import gudusoft.gsqlparser.resolver2.model.ColumnSourceWithReferences;
012import gudusoft.gsqlparser.sqlenv.CanonKey;
013import gudusoft.gsqlparser.sqlenv.IdentifierService;
014
015import java.util.*;
016
017/**
018 * Abstract base class for all namespaces.
019 * Provides common functionality for column resolution and caching.
020 *
021 * <p>Enhanced with reference traceability support: multiple syntactically different
022 * identifiers that refer to the same semantic column can be tracked and traced back.</p>
023 */
024public abstract class AbstractNamespace implements INamespace {
025
026    /** Associated AST node */
027    protected final Object node;
028
029    /** Whether this namespace has been validated */
030    protected boolean validated = false;
031
032    /** Cached column sources (populated during validation) - keyed by normalized name */
033    protected Map<String, ColumnSource> columnSources = null;
034
035    /**
036     * Enhanced column sources with reference traceability (keyed by normalized name).
037     * When enabled, this map stores ColumnSourceWithReferences that track all original
038     * references to each semantic column.
039     */
040    protected Map<String, ColumnSourceWithReferences> columnSourcesWithRefs = null;
041
042    /** Whether reference traceability is enabled */
043    protected boolean referenceTraceabilityEnabled = false;
044
045    /** Name matcher for column name comparisons */
046    protected final INameMatcher nameMatcher;
047
048    /**
049     * Canonical-key index over {@link #columnSources} (Mantis 4684).
050     *
051     * <p>The matcher fallback loops in {@link #hasColumn}/{@link #resolveColumn}
052     * are O(map size) with a full identifier comparison per entry — on SQL Server
053     * each comparison is a {@link java.text.RuleBasedCollator} run. Inferred
054     * columns ({@code TableNamespace.resolveColumn}) grow the map by one per
055     * unresolved reference, so a wide select list makes resolution O(N²) in
056     * collator comparisons (a 2000-column select took ~24s).</p>
057     *
058     * <p>This index maps {@link CanonKey} → first map key carrying that key.
059     * Since P0d.1, {@code IdentifierService.areEqual} IS canonical-key equality
060     * (collation-based dialects use {@link java.text.CollationKey} bytes from the
061     * SAME cached collator), so an index probe is exactly equivalent to the
062     * matcher loop; {@code putIfAbsent} in insertion order preserves the loop's
063     * first-match-wins choice. The index is synced lazily: {@code columnSources}
064     * is append-only in every namespace (no removals exist), so a sync only
065     * indexes the entries added since the last one. Any name outside the
066     * canonical-key domain (multi-segment, malformed quoting) permanently
067     * disables the index for this namespace and the loop takes over.</p>
068     */
069    private Map<CanonKey, String> canonKeyIndex = null;
070    /** Identity of the map instance the index was built over (subclasses reassign {@link #columnSources}). */
071    private Map<String, ColumnSource> canonIndexedMap = null;
072    /** How many insertion-order entries of {@link #canonIndexedMap} are indexed. */
073    private int canonIndexedCount = 0;
074    /** Set when a name refuses canonical-key construction; falls back to the matcher loop. */
075    private boolean canonIndexUnusable = false;
076
077    /**
078     * Strategy for handling ambiguous columns.
079     * Defaults to -1, which means use global TBaseType.GUESS_COLUMN_STRATEGY.
080     * Can be overridden per-namespace instance.
081     */
082    protected int guessColumnStrategy = -1;
083
084    protected AbstractNamespace(Object node, INameMatcher nameMatcher) {
085        this.node = node;
086        this.nameMatcher = nameMatcher != null ? nameMatcher : new DefaultNameMatcher();
087    }
088
089    protected AbstractNamespace(Object node) {
090        this(node, new DefaultNameMatcher());
091    }
092
093    /**
094     * Set the strategy for handling ambiguous columns.
095     * @param strategy One of TBaseType.GUESS_COLUMN_STRATEGY_* constants, or -1 to use global default
096     */
097    public void setGuessColumnStrategy(int strategy) {
098        this.guessColumnStrategy = strategy;
099    }
100
101    /**
102     * Get the strategy for handling ambiguous columns.
103     * Returns the instance-level strategy if set, otherwise returns the global default.
104     * @return The strategy constant
105     */
106    public int getGuessColumnStrategy() {
107        if (guessColumnStrategy >= 0) {
108            return guessColumnStrategy;
109        }
110        return gudusoft.gsqlparser.TBaseType.GUESS_COLUMN_STRATEGY;
111    }
112
113    @Override
114    public Object getNode() {
115        return node;
116    }
117
118    @Override
119    public boolean isValidated() {
120        return validated;
121    }
122
123    @Override
124    public void validate() {
125        if (!validated) {
126            doValidate();
127            validated = true;
128        }
129    }
130
131    /**
132     * Subclasses override this to perform actual validation logic.
133     */
134    protected abstract void doValidate();
135
136    /**
137     * Enable reference traceability for this namespace.
138     * When enabled, all column additions will track original references.
139     */
140    public void enableReferenceTraceability() {
141        this.referenceTraceabilityEnabled = true;
142        if (columnSourcesWithRefs == null) {
143            columnSourcesWithRefs = new LinkedHashMap<>();
144        }
145    }
146
147    /**
148     * Check if reference traceability is enabled.
149     *
150     * @return true if traceability is enabled
151     */
152    public boolean isReferenceTraceabilityEnabled() {
153        return referenceTraceabilityEnabled;
154    }
155
156    /**
157     * Add a column source with reference traceability support.
158     *
159     * <p>This method normalizes the column name and either:</p>
160     * <ul>
161     *   <li>Creates a new entry if this is the first reference</li>
162     *   <li>Adds a reference to an existing entry if the column already exists</li>
163     * </ul>
164     *
165     * @param columnName original column name (may include quotes)
166     * @param source the column source
167     * @param objectName the original AST node for traceability (may be null)
168     */
169    protected void addColumnSource(String columnName, ColumnSource source, TObjectName objectName) {
170        if (columnSources == null) {
171            columnSources = new LinkedHashMap<>();
172        }
173
174        String normalizedKey = nameMatcher.normalize(columnName);
175
176        // Add to basic map if not exists
177        if (!columnSources.containsKey(normalizedKey)) {
178            columnSources.put(normalizedKey, source);
179            noteColumnSourceAdded(normalizedKey);
180        }
181
182        // Add to enhanced map with references if traceability is enabled
183        if (referenceTraceabilityEnabled) {
184            if (columnSourcesWithRefs == null) {
185                columnSourcesWithRefs = new LinkedHashMap<>();
186            }
187
188            ColumnSourceWithReferences enhanced = columnSourcesWithRefs.computeIfAbsent(
189                normalizedKey,
190                k -> new ColumnSourceWithReferences(normalizedKey, source)
191            );
192
193            if (objectName != null) {
194                enhanced.addReference(new ColumnReference(objectName));
195            }
196        }
197    }
198
199    /**
200     * Add a column source (backward compatible - no traceability).
201     *
202     * @param columnName original column name
203     * @param source the column source
204     */
205    protected void addColumnSource(String columnName, ColumnSource source) {
206        addColumnSource(columnName, source, null);
207    }
208
209    /**
210     * Slice S1: matcher-aware {@code containsKey} replacement for column maps
211     * that need vendor-specific identifier rules to govern dedupe / lookup.
212     *
213     * <p>Per-vendor identifier rules differ on whether {@code MyCol} and {@code mycol}
214     * are the same column (BigQuery / MySQL / SQL Server: yes for columns;
215     * Oracle / Postgres unquoted: yes via folding; quoted Oracle / Postgres: no).
216     * A raw {@code map.containsKey(name)} bypasses these rules and produces
217     * duplicate-key drift; a fixed {@code equalsIgnoreCase} loop is wrong for
218     * vendors where columns are case-sensitive (BigQuery tables, Oracle quoted).
219     *
220     * <p>This helper:
221     * <ol>
222     *   <li>tries an O(1) normalized-key probe (fast path for vendors that
223     *       fold unquoted identifiers to a canonical form, where the stored
224     *       raw key is itself the folded form — e.g. Postgres stored
225     *       {@code "mycol"} probed by query {@code "MYCOL"} that normalizes
226     *       to {@code "mycol"}),</li>
227     *   <li>tries an O(1) raw-key probe for the exact-match common case,</li>
228     *   <li>falls back to a matcher loop that compares against {@link
229     *       ColumnSource#getExposedName()} (when values are {@code ColumnSource})
230     *       so quote state is preserved on quoted-sensitive dialects.</li>
231     * </ol>
232     *
233     * <p><strong>Quote-state preservation (codex round 1 + round 2).</strong>
234     * The map values must hold the original-cased identifier (with quotes).
235     * Storage keys are also raw (= {@code exposedName}) — round 2 caught
236     * that storing under {@code nameMatcher.normalize(name)} can collide
237     * two matcher-distinct identifiers (e.g. Postgres quoted {@code "mycol"}
238     * vs unquoted {@code MYCOL} both normalize to {@code mycol}) into the
239     * same key and lose information. With raw-keyed storage, the normalize
240     * fast-probe only hits when the stored raw key already equals the
241     * normalized form, and in that case {@link
242     * gudusoft.gsqlparser.resolver2.matcher.INameMatcher#matches} agrees;
243     * the matcher loop catches the case-only-different case.
244     */
245    protected boolean containsColumnByMatcher(Map<String, ?> map, String columnName) {
246        if (map == null || map.isEmpty() || columnName == null) {
247            return false;
248        }
249        String normalizedKey = nameMatcher.normalize(columnName);
250        if (normalizedKey != null && map.containsKey(normalizedKey)) {
251            return true;
252        }
253        if (map.containsKey(columnName)) {
254            return true;
255        }
256        for (Map.Entry<String, ?> entry : map.entrySet()) {
257            String compareName = entry.getKey();
258            Object value = entry.getValue();
259            if (value instanceof ColumnSource) {
260                String exposed = ((ColumnSource) value).getExposedName();
261                if (exposed != null) {
262                    compareName = exposed;
263                }
264            }
265            if (compareName != null && nameMatcher.matches(compareName, columnName)) {
266                return true;
267            }
268        }
269        return false;
270    }
271
272    /**
273     * Slice S1: matcher-aware {@code Set#contains} replacement. See
274     * {@link #containsColumnByMatcher(Map, String)} for the rationale.
275     */
276    protected boolean containsColumnNameByMatcher(Set<String> set, String columnName) {
277        if (set == null || set.isEmpty() || columnName == null) {
278            return false;
279        }
280        if (set.contains(columnName)) {
281            return true;
282        }
283        String normalizedKey = nameMatcher.normalize(columnName);
284        if (normalizedKey != null && set.contains(normalizedKey)) {
285            return true;
286        }
287        for (String existing : set) {
288            if (nameMatcher.matches(existing, columnName)) {
289                return true;
290            }
291        }
292        return false;
293    }
294
295    @Override
296    public ColumnLevel hasColumn(String columnName) {
297        ensureValidated();
298
299        if (columnSources == null) {
300            return ColumnLevel.NOT_EXISTS;
301        }
302
303        // Use normalized key for O(1) lookup
304        String normalizedKey = nameMatcher.normalize(columnName);
305        if (columnSources.containsKey(normalizedKey)) {
306            return ColumnLevel.EXISTS;
307        }
308
309        // Matcher-equality fallback (canonical-key indexed — see canonKeyIndex)
310        if (findColumnKeyByMatcher(columnName) != null) {
311            return ColumnLevel.EXISTS;
312        }
313
314        return ColumnLevel.NOT_EXISTS;
315    }
316
317    @Override
318    public ColumnSource resolveColumn(String columnName) {
319        ensureValidated();
320
321        if (columnSources == null) {
322            return null;
323        }
324
325        // Use normalized key for O(1) lookup
326        String normalizedKey = nameMatcher.normalize(columnName);
327        ColumnSource source = columnSources.get(normalizedKey);
328        if (source != null) {
329            return source;
330        }
331
332        // Matcher-equality fallback (canonical-key indexed — see canonKeyIndex)
333        String matchedKey = findColumnKeyByMatcher(columnName);
334        return matchedKey != null ? columnSources.get(matchedKey) : null;
335    }
336
337    /**
338     * Find the first insertion-order key of {@link #columnSources} that the
339     * {@link #nameMatcher} considers equal to {@code columnName}, or null when
340     * no key matches.
341     *
342     * <p>Equivalent to the historical
343     * {@code for (key : columnSources.keySet()) if (nameMatcher.matches(key, columnName))}
344     * loop, but near-O(1) via the canonical-key index when the matcher is a
345     * {@link VendorNameMatcher} (Mantis 4684 — the loop made wide select lists
346     * quadratic with a collator comparison per entry on SQL Server).</p>
347     */
348    protected String findColumnKeyByMatcher(String columnName) {
349        if (columnSources == null || columnSources.isEmpty() || columnName == null) {
350            return null;
351        }
352        // Exact class only: a VendorNameMatcher SUBCLASS may override matches(),
353        // and the index would bypass the override (Codex round-1 P2).
354        if (!canonIndexUnusable && nameMatcher.getClass() == VendorNameMatcher.class) {
355            try {
356                syncCanonKeyIndex();
357                return canonKeyIndex.get(canonKeyOf(columnName));
358            } catch (RuntimeException e) {
359                // Name outside the canonical-key domain (multi-segment or
360                // malformed quoting): give up on the index for this namespace.
361                canonIndexUnusable = true;
362                canonKeyIndex = null;
363                canonIndexedMap = null;
364            }
365        }
366        for (String existingCol : columnSources.keySet()) {
367            if (nameMatcher.matches(existingCol, columnName)) {
368                return existingCol;
369            }
370        }
371        return null;
372    }
373
374    /** Canonical key of one name under the matcher's vendor + object type. */
375    private CanonKey canonKeyOf(String name) {
376        VendorNameMatcher vm = (VendorNameMatcher) nameMatcher;
377        return IdentifierService.canonKeyStatic(vm.getVendor(), vm.getDefaultObjectType(), name);
378    }
379
380    /**
381     * Incremental index maintenance for the append-one case (Codex round-1 P1):
382     * without this, every {@link #syncCanonKeyIndex} after a single-key insert
383     * re-walks the already-indexed prefix just to skip it — O(N²) iterator
384     * steps on the wide-select workload even with O(N) canonical keys. Call
385     * after putting {@code key} into {@link #columnSources}. A batch of puts
386     * without notes is still safe: the size check fails and the next sync
387     * catches up with the (now rare) prefix walk.
388     */
389    protected void noteColumnSourceAdded(String key) {
390        if (canonIndexUnusable || canonKeyIndex == null || canonIndexedMap != columnSources) {
391            return; // index not built yet or map replaced; next sync handles it
392        }
393        if (columnSources.size() != canonIndexedCount + 1) {
394            return; // overwrite (size unchanged) or un-noted batch; sync catches up
395        }
396        try {
397            canonKeyIndex.putIfAbsent(canonKeyOf(key), key);
398            canonIndexedCount++;
399        } catch (RuntimeException e) {
400            canonIndexUnusable = true;
401            canonKeyIndex = null;
402            canonIndexedMap = null;
403        }
404    }
405
406    /**
407     * Bring {@link #canonKeyIndex} up to date with {@link #columnSources}.
408     * The map is append-only between syncs, so only the entries beyond
409     * {@link #canonIndexedCount} need indexing; a reassigned map instance or a
410     * shrunken size (defensive — no remover exists today) rebuilds from scratch.
411     */
412    private void syncCanonKeyIndex() {
413        if (canonKeyIndex == null || canonIndexedMap != columnSources
414                || columnSources.size() < canonIndexedCount) {
415            canonKeyIndex = new HashMap<>();
416            canonIndexedMap = columnSources;
417            canonIndexedCount = 0;
418        }
419        int size = columnSources.size();
420        if (canonIndexedCount == size) {
421            return;
422        }
423        int i = 0;
424        for (String key : columnSources.keySet()) {
425            if (i++ < canonIndexedCount) {
426                continue;
427            }
428            // First entry with a given canonical key wins, matching the scan.
429            canonKeyIndex.putIfAbsent(canonKeyOf(key), key);
430        }
431        canonIndexedCount = size;
432    }
433
434    @Override
435    public Map<String, ColumnSource> getAllColumnSources() {
436        ensureValidated();
437        return columnSources != null
438            ? Collections.unmodifiableMap(columnSources)
439            : Collections.emptyMap();
440    }
441
442    /**
443     * Get all column references for a specific column.
444     *
445     * <p>Requires reference traceability to be enabled.</p>
446     *
447     * @param columnName the column name (normalized or original)
448     * @return list of all references, empty if not found or traceability not enabled
449     */
450    public List<ColumnReference> getColumnReferences(String columnName) {
451        if (columnSourcesWithRefs == null) {
452            return Collections.emptyList();
453        }
454
455        String normalizedKey = nameMatcher.normalize(columnName);
456        ColumnSourceWithReferences enhanced = columnSourcesWithRefs.get(normalizedKey);
457
458        return enhanced != null
459            ? enhanced.getAllReferences()
460            : Collections.emptyList();
461    }
462
463    /**
464     * Get all unique columns with their references.
465     *
466     * <p>Requires reference traceability to be enabled.</p>
467     *
468     * @return collection of enhanced column sources, empty if traceability not enabled
469     */
470    public Collection<ColumnSourceWithReferences> getAllUniqueColumns() {
471        if (columnSourcesWithRefs == null) {
472            return Collections.emptyList();
473        }
474        return Collections.unmodifiableCollection(columnSourcesWithRefs.values());
475    }
476
477    /**
478     * Get enhanced column source with references for a specific column.
479     *
480     * @param columnName the column name
481     * @return enhanced column source, or null if not found
482     */
483    public ColumnSourceWithReferences getColumnSourceWithReferences(String columnName) {
484        if (columnSourcesWithRefs == null) {
485            return null;
486        }
487        String normalizedKey = nameMatcher.normalize(columnName);
488        return columnSourcesWithRefs.get(normalizedKey);
489    }
490
491    @Override
492    public List<TTable> getAllFinalTables() {
493        TTable finalTable = getFinalTable();
494        if (finalTable != null) {
495            return Collections.singletonList(finalTable);
496        }
497        return Collections.emptyList();
498    }
499
500    /**
501     * Ensure this namespace is validated before accessing column info
502     */
503    protected void ensureValidated() {
504        if (!validated) {
505            validate();
506        }
507    }
508
509    /**
510     * Get the name matcher used by this namespace.
511     *
512     * @return the name matcher
513     */
514    public INameMatcher getNameMatcher() {
515        return nameMatcher;
516    }
517
518    @Override
519    public String toString() {
520        return getDisplayName();
521    }
522}