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.model.ColumnReference;
009import gudusoft.gsqlparser.resolver2.model.ColumnSource;
010import gudusoft.gsqlparser.resolver2.model.ColumnSourceWithReferences;
011
012import java.util.*;
013
014/**
015 * Abstract base class for all namespaces.
016 * Provides common functionality for column resolution and caching.
017 *
018 * <p>Enhanced with reference traceability support: multiple syntactically different
019 * identifiers that refer to the same semantic column can be tracked and traced back.</p>
020 */
021public abstract class AbstractNamespace implements INamespace {
022
023    /** Associated AST node */
024    protected final Object node;
025
026    /** Whether this namespace has been validated */
027    protected boolean validated = false;
028
029    /** Cached column sources (populated during validation) - keyed by normalized name */
030    protected Map<String, ColumnSource> columnSources = null;
031
032    /**
033     * Enhanced column sources with reference traceability (keyed by normalized name).
034     * When enabled, this map stores ColumnSourceWithReferences that track all original
035     * references to each semantic column.
036     */
037    protected Map<String, ColumnSourceWithReferences> columnSourcesWithRefs = null;
038
039    /** Whether reference traceability is enabled */
040    protected boolean referenceTraceabilityEnabled = false;
041
042    /** Name matcher for column name comparisons */
043    protected final INameMatcher nameMatcher;
044
045    /**
046     * Strategy for handling ambiguous columns.
047     * Defaults to -1, which means use global TBaseType.GUESS_COLUMN_STRATEGY.
048     * Can be overridden per-namespace instance.
049     */
050    protected int guessColumnStrategy = -1;
051
052    protected AbstractNamespace(Object node, INameMatcher nameMatcher) {
053        this.node = node;
054        this.nameMatcher = nameMatcher != null ? nameMatcher : new DefaultNameMatcher();
055    }
056
057    protected AbstractNamespace(Object node) {
058        this(node, new DefaultNameMatcher());
059    }
060
061    /**
062     * Set the strategy for handling ambiguous columns.
063     * @param strategy One of TBaseType.GUESS_COLUMN_STRATEGY_* constants, or -1 to use global default
064     */
065    public void setGuessColumnStrategy(int strategy) {
066        this.guessColumnStrategy = strategy;
067    }
068
069    /**
070     * Get the strategy for handling ambiguous columns.
071     * Returns the instance-level strategy if set, otherwise returns the global default.
072     * @return The strategy constant
073     */
074    public int getGuessColumnStrategy() {
075        if (guessColumnStrategy >= 0) {
076            return guessColumnStrategy;
077        }
078        return gudusoft.gsqlparser.TBaseType.GUESS_COLUMN_STRATEGY;
079    }
080
081    @Override
082    public Object getNode() {
083        return node;
084    }
085
086    @Override
087    public boolean isValidated() {
088        return validated;
089    }
090
091    @Override
092    public void validate() {
093        if (!validated) {
094            doValidate();
095            validated = true;
096        }
097    }
098
099    /**
100     * Subclasses override this to perform actual validation logic.
101     */
102    protected abstract void doValidate();
103
104    /**
105     * Enable reference traceability for this namespace.
106     * When enabled, all column additions will track original references.
107     */
108    public void enableReferenceTraceability() {
109        this.referenceTraceabilityEnabled = true;
110        if (columnSourcesWithRefs == null) {
111            columnSourcesWithRefs = new LinkedHashMap<>();
112        }
113    }
114
115    /**
116     * Check if reference traceability is enabled.
117     *
118     * @return true if traceability is enabled
119     */
120    public boolean isReferenceTraceabilityEnabled() {
121        return referenceTraceabilityEnabled;
122    }
123
124    /**
125     * Add a column source with reference traceability support.
126     *
127     * <p>This method normalizes the column name and either:</p>
128     * <ul>
129     *   <li>Creates a new entry if this is the first reference</li>
130     *   <li>Adds a reference to an existing entry if the column already exists</li>
131     * </ul>
132     *
133     * @param columnName original column name (may include quotes)
134     * @param source the column source
135     * @param objectName the original AST node for traceability (may be null)
136     */
137    protected void addColumnSource(String columnName, ColumnSource source, TObjectName objectName) {
138        if (columnSources == null) {
139            columnSources = new LinkedHashMap<>();
140        }
141
142        String normalizedKey = nameMatcher.normalize(columnName);
143
144        // Add to basic map if not exists
145        if (!columnSources.containsKey(normalizedKey)) {
146            columnSources.put(normalizedKey, source);
147        }
148
149        // Add to enhanced map with references if traceability is enabled
150        if (referenceTraceabilityEnabled) {
151            if (columnSourcesWithRefs == null) {
152                columnSourcesWithRefs = new LinkedHashMap<>();
153            }
154
155            ColumnSourceWithReferences enhanced = columnSourcesWithRefs.computeIfAbsent(
156                normalizedKey,
157                k -> new ColumnSourceWithReferences(normalizedKey, source)
158            );
159
160            if (objectName != null) {
161                enhanced.addReference(new ColumnReference(objectName));
162            }
163        }
164    }
165
166    /**
167     * Add a column source (backward compatible - no traceability).
168     *
169     * @param columnName original column name
170     * @param source the column source
171     */
172    protected void addColumnSource(String columnName, ColumnSource source) {
173        addColumnSource(columnName, source, null);
174    }
175
176    /**
177     * Slice S1: matcher-aware {@code containsKey} replacement for column maps
178     * that need vendor-specific identifier rules to govern dedupe / lookup.
179     *
180     * <p>Per-vendor identifier rules differ on whether {@code MyCol} and {@code mycol}
181     * are the same column (BigQuery / MySQL / SQL Server: yes for columns;
182     * Oracle / Postgres unquoted: yes via folding; quoted Oracle / Postgres: no).
183     * A raw {@code map.containsKey(name)} bypasses these rules and produces
184     * duplicate-key drift; a fixed {@code equalsIgnoreCase} loop is wrong for
185     * vendors where columns are case-sensitive (BigQuery tables, Oracle quoted).
186     *
187     * <p>This helper:
188     * <ol>
189     *   <li>tries an O(1) normalized-key probe (fast path for vendors that
190     *       fold unquoted identifiers to a canonical form, where the stored
191     *       raw key is itself the folded form — e.g. Postgres stored
192     *       {@code "mycol"} probed by query {@code "MYCOL"} that normalizes
193     *       to {@code "mycol"}),</li>
194     *   <li>tries an O(1) raw-key probe for the exact-match common case,</li>
195     *   <li>falls back to a matcher loop that compares against {@link
196     *       ColumnSource#getExposedName()} (when values are {@code ColumnSource})
197     *       so quote state is preserved on quoted-sensitive dialects.</li>
198     * </ol>
199     *
200     * <p><strong>Quote-state preservation (codex round 1 + round 2).</strong>
201     * The map values must hold the original-cased identifier (with quotes).
202     * Storage keys are also raw (= {@code exposedName}) — round 2 caught
203     * that storing under {@code nameMatcher.normalize(name)} can collide
204     * two matcher-distinct identifiers (e.g. Postgres quoted {@code "mycol"}
205     * vs unquoted {@code MYCOL} both normalize to {@code mycol}) into the
206     * same key and lose information. With raw-keyed storage, the normalize
207     * fast-probe only hits when the stored raw key already equals the
208     * normalized form, and in that case {@link
209     * gudusoft.gsqlparser.resolver2.matcher.INameMatcher#matches} agrees;
210     * the matcher loop catches the case-only-different case.
211     */
212    protected boolean containsColumnByMatcher(Map<String, ?> map, String columnName) {
213        if (map == null || map.isEmpty() || columnName == null) {
214            return false;
215        }
216        String normalizedKey = nameMatcher.normalize(columnName);
217        if (normalizedKey != null && map.containsKey(normalizedKey)) {
218            return true;
219        }
220        if (map.containsKey(columnName)) {
221            return true;
222        }
223        for (Map.Entry<String, ?> entry : map.entrySet()) {
224            String compareName = entry.getKey();
225            Object value = entry.getValue();
226            if (value instanceof ColumnSource) {
227                String exposed = ((ColumnSource) value).getExposedName();
228                if (exposed != null) {
229                    compareName = exposed;
230                }
231            }
232            if (compareName != null && nameMatcher.matches(compareName, columnName)) {
233                return true;
234            }
235        }
236        return false;
237    }
238
239    /**
240     * Slice S1: matcher-aware {@code Set#contains} replacement. See
241     * {@link #containsColumnByMatcher(Map, String)} for the rationale.
242     */
243    protected boolean containsColumnNameByMatcher(Set<String> set, String columnName) {
244        if (set == null || set.isEmpty() || columnName == null) {
245            return false;
246        }
247        if (set.contains(columnName)) {
248            return true;
249        }
250        String normalizedKey = nameMatcher.normalize(columnName);
251        if (normalizedKey != null && set.contains(normalizedKey)) {
252            return true;
253        }
254        for (String existing : set) {
255            if (nameMatcher.matches(existing, columnName)) {
256                return true;
257            }
258        }
259        return false;
260    }
261
262    @Override
263    public ColumnLevel hasColumn(String columnName) {
264        ensureValidated();
265
266        if (columnSources == null) {
267            return ColumnLevel.NOT_EXISTS;
268        }
269
270        // Use normalized key for O(1) lookup
271        String normalizedKey = nameMatcher.normalize(columnName);
272        if (columnSources.containsKey(normalizedKey)) {
273            return ColumnLevel.EXISTS;
274        }
275
276        // Fallback to linear search for backward compatibility
277        for (String existingCol : columnSources.keySet()) {
278            if (nameMatcher.matches(existingCol, columnName)) {
279                return ColumnLevel.EXISTS;
280            }
281        }
282
283        return ColumnLevel.NOT_EXISTS;
284    }
285
286    @Override
287    public ColumnSource resolveColumn(String columnName) {
288        ensureValidated();
289
290        if (columnSources == null) {
291            return null;
292        }
293
294        // Use normalized key for O(1) lookup
295        String normalizedKey = nameMatcher.normalize(columnName);
296        ColumnSource source = columnSources.get(normalizedKey);
297        if (source != null) {
298            return source;
299        }
300
301        // Fallback to linear search for backward compatibility
302        for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) {
303            if (nameMatcher.matches(entry.getKey(), columnName)) {
304                return entry.getValue();
305            }
306        }
307
308        return null;
309    }
310
311    @Override
312    public Map<String, ColumnSource> getAllColumnSources() {
313        ensureValidated();
314        return columnSources != null
315            ? Collections.unmodifiableMap(columnSources)
316            : Collections.emptyMap();
317    }
318
319    /**
320     * Get all column references for a specific column.
321     *
322     * <p>Requires reference traceability to be enabled.</p>
323     *
324     * @param columnName the column name (normalized or original)
325     * @return list of all references, empty if not found or traceability not enabled
326     */
327    public List<ColumnReference> getColumnReferences(String columnName) {
328        if (columnSourcesWithRefs == null) {
329            return Collections.emptyList();
330        }
331
332        String normalizedKey = nameMatcher.normalize(columnName);
333        ColumnSourceWithReferences enhanced = columnSourcesWithRefs.get(normalizedKey);
334
335        return enhanced != null
336            ? enhanced.getAllReferences()
337            : Collections.emptyList();
338    }
339
340    /**
341     * Get all unique columns with their references.
342     *
343     * <p>Requires reference traceability to be enabled.</p>
344     *
345     * @return collection of enhanced column sources, empty if traceability not enabled
346     */
347    public Collection<ColumnSourceWithReferences> getAllUniqueColumns() {
348        if (columnSourcesWithRefs == null) {
349            return Collections.emptyList();
350        }
351        return Collections.unmodifiableCollection(columnSourcesWithRefs.values());
352    }
353
354    /**
355     * Get enhanced column source with references for a specific column.
356     *
357     * @param columnName the column name
358     * @return enhanced column source, or null if not found
359     */
360    public ColumnSourceWithReferences getColumnSourceWithReferences(String columnName) {
361        if (columnSourcesWithRefs == null) {
362            return null;
363        }
364        String normalizedKey = nameMatcher.normalize(columnName);
365        return columnSourcesWithRefs.get(normalizedKey);
366    }
367
368    @Override
369    public List<TTable> getAllFinalTables() {
370        TTable finalTable = getFinalTable();
371        if (finalTable != null) {
372            return Collections.singletonList(finalTable);
373        }
374        return Collections.emptyList();
375    }
376
377    /**
378     * Ensure this namespace is validated before accessing column info
379     */
380    protected void ensureValidated() {
381        if (!validated) {
382            validate();
383        }
384    }
385
386    /**
387     * Get the name matcher used by this namespace.
388     *
389     * @return the name matcher
390     */
391    public INameMatcher getNameMatcher() {
392        return nameMatcher;
393    }
394
395    @Override
396    public String toString() {
397        return getDisplayName();
398    }
399}