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