001package gudusoft.gsqlparser.resolver2.model;
002
003import gudusoft.gsqlparser.nodes.TTable;
004import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher;
005import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
006import gudusoft.gsqlparser.resolver2.matcher.VendorNameMatcher;
007import gudusoft.gsqlparser.resolver2.namespace.INamespace;
008import gudusoft.gsqlparser.resolver2.scope.GlobalScope;
009import gudusoft.gsqlparser.resolver2.scope.IScope;
010import gudusoft.gsqlparser.resolver2.scope.SelectScope;
011import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
012
013import java.util.LinkedHashMap;
014import java.util.List;
015import java.util.Map;
016
017/**
018 * Index structure for fast lookups in FromScope.
019 *
020 * <p>This class optimizes O(N) linear scans to O(1) hash lookups for common
021 * operations like finding tables by alias/name and finding candidate namespaces
022 * for star column inference.</p>
023 *
024 * <p>The index is built lazily on first access and cached for subsequent lookups.
025 * This avoids the overhead of building the index when it's not needed.</p>
026 *
027 * <p><b>Performance improvement:</b> For queries with N tables and M column references,
028 * reduces complexity from O(N*M) to O(N + M).</p>
029 *
030 * <p><b>Slice S2 — vendor-aware identifier compares.</b> Pre-S2 keys were
031 * built via raw {@link String#toLowerCase()} which broke quoted-sensitive
032 * dialects (Oracle / Postgres / Snowflake / Hive / Teradata quoted) and
033 * BigQuery's case-sensitive table rule. S2 stores aliases / display names
034 * <em>raw</em> (preserving quotes and original case) and routes lookups
035 * through {@link IdentifierService} via the supplied {@link INameMatcher}
036 * with {@link ESQLDataObjectType#dotTable}. The exact-text fast probe
037 * still handles same-as-written queries in O(1); cross-case / quoted-vs-
038 * unquoted lookups fall through to a vendor-aware linear scan. Storing
039 * the raw form is critical: a normalized key would lose quote provenance
040 * — codex round-1 review caught that comparing the stripped-and-folded
041 * stored key against the raw input would false-positive an unquoted
042 * probe against a quoted alias on strict-case dialects.</p>
043 *
044 * @since 3.1.0.9
045 */
046public class FromScopeIndex {
047
048    /**
049     * Map from <em>raw</em> alias text (with quotes preserved, original
050     * case) to ScopeChild. Lookup uses an exact-text fast probe first;
051     * mismatches fall through to a vendor-aware matcher scan. Storing
052     * raw text preserves quote provenance so {@link IdentifierService}
053     * can decide quoted-vs-unquoted compare authoritatively in the
054     * matcher scan (codex round-1 review).
055     */
056    private final Map<String, ScopeChild> aliasMap;
057
058    /**
059     * Map from <em>raw</em> namespace display name to ScopeChild.
060     * Used when lookup by alias fails but table name might match.
061     */
062    private final Map<String, ScopeChild> nameMap;
063
064    /**
065     * First namespace that supports star columns or dynamic inference.
066     * Used for unqualified column resolution when no table prefix is specified.
067     */
068    private final INamespace preferredStarNamespace;
069
070    /**
071     * Total number of children in the FromScope.
072     */
073    private final int childCount;
074
075    /**
076     * Name matcher used to normalize keys and to fall back on a vendor-aware
077     * linear scan when the stored key normalization disagrees with the
078     * lookup-time normalization (rare).
079     */
080    private final INameMatcher nameMatcher;
081
082    /**
083     * Creates an index from the given list of scope children using a
084     * vendor-agnostic {@link DefaultNameMatcher}. Prefer the matcher-aware
085     * constructor; this overload exists for synthetic / unit-test scopes
086     * that have no GlobalScope above them.
087     *
088     * @param children The list of ScopeChild from a FromScope
089     */
090    public FromScopeIndex(List<ScopeChild> children) {
091        this(children, new DefaultNameMatcher());
092    }
093
094    /**
095     * Creates an index from the given list of scope children, using the
096     * supplied matcher to normalize alias / table-name keys per-vendor.
097     *
098     * @param children The list of ScopeChild from a FromScope
099     * @param nameMatcher Vendor-aware matcher (typically the GlobalScope's)
100     */
101    public FromScopeIndex(List<ScopeChild> children, INameMatcher nameMatcher) {
102        this.nameMatcher = nameMatcher != null ? nameMatcher : new DefaultNameMatcher();
103        // Use LinkedHashMap so the matcher-driven fallback scan (when the
104        // lookup-time normalization disagrees with stored keys, e.g. a
105        // quoted identifier in scope alongside an unquoted one) walks
106        // entries in insertion order, matching the deterministic behaviour
107        // of FromScope.getChildren().
108        this.aliasMap = new LinkedHashMap<>();
109        this.nameMap = new LinkedHashMap<>();
110        this.childCount = children != null ? children.size() : 0;
111
112        INamespace firstStar = null;
113
114        if (children != null) {
115            for (ScopeChild child : children) {
116                // S2: store raw alias / display-name text (quotes preserved).
117                String alias = child.getAlias();
118                if (alias != null && !alias.isEmpty()) {
119                    aliasMap.put(alias, child);
120                }
121
122                // Also index by namespace display name
123                INamespace ns = child.getNamespace();
124                if (ns != null) {
125                    String displayName = ns.getDisplayName();
126                    if (displayName != null && !displayName.isEmpty()) {
127                        nameMap.put(displayName, child);
128                    }
129
130                    // Track first star/dynamic namespace
131                    if (firstStar == null &&
132                        (ns.hasStarColumn() || ns.supportsDynamicInference())) {
133                        firstStar = ns;
134                    }
135                }
136            }
137        }
138
139        this.preferredStarNamespace = firstStar;
140    }
141
142    /**
143     * Finds a ScopeChild by its alias (vendor-aware).
144     *
145     * @param alias The alias to search for
146     * @return The matching ScopeChild, or null if not found
147     */
148    public ScopeChild findByAlias(String alias) {
149        if (alias == null || alias.isEmpty()) {
150            return null;
151        }
152        // O(1) exact-text fast probe (same case + same quote state as the
153        // alias was originally written).
154        ScopeChild byExact = aliasMap.get(alias);
155        if (byExact != null) {
156            return byExact;
157        }
158        // Slow-path: vendor-aware matcher scan against raw stored aliases
159        // (with quotes preserved), so {@link IdentifierService} can decide
160        // quoted-vs-unquoted compare authoritatively.
161        return findByMatcher(aliasMap.values(), ScopeChild::getAlias, alias);
162    }
163
164    /**
165     * Finds a ScopeChild by alias first, then by namespace name (vendor-aware).
166     *
167     * @param qualifier The qualifier (alias or table name) to search for
168     * @return The matching ScopeChild, or null if not found
169     */
170    public ScopeChild findByQualifier(String qualifier) {
171        if (qualifier == null || qualifier.isEmpty()) {
172            return null;
173        }
174
175        ScopeChild result = findByAlias(qualifier);
176        if (result != null) {
177            return result;
178        }
179
180        ScopeChild byNameExact = nameMap.get(qualifier);
181        if (byNameExact != null) {
182            return byNameExact;
183        }
184        return findByMatcher(nameMap.values(),
185                child -> child.getNamespace() != null ? child.getNamespace().getDisplayName() : null,
186                qualifier);
187    }
188
189    /**
190     * Walk the children comparing the raw, quote-preserving identifier
191     * supplied by {@code rawAccessor} against the input via the matcher
192     * with {@link ESQLDataObjectType#dotTable}.
193     *
194     * <p><strong>Invariant:</strong> compare raw alias / display name
195     * (with quotes) — NOT a normalized stored key — so
196     * {@link IdentifierService} can resolve quoted-vs-unquoted semantics
197     * authoritatively.
198     */
199    private ScopeChild findByMatcher(java.util.Collection<ScopeChild> children,
200                                     java.util.function.Function<ScopeChild, String> rawAccessor,
201                                     String input) {
202        if (children.isEmpty()) {
203            return null;
204        }
205        for (ScopeChild child : children) {
206            String storedRaw = rawAccessor.apply(child);
207            if (storedRaw == null || storedRaw.isEmpty()) continue;
208            if (nameMatcher instanceof VendorNameMatcher) {
209                if (((VendorNameMatcher) nameMatcher).matches(storedRaw, input, ESQLDataObjectType.dotTable)) {
210                    return child;
211                }
212            } else if (nameMatcher.matches(storedRaw, input)) {
213                return child;
214            }
215        }
216        return null;
217    }
218
219    /**
220     * Gets the TTable from a ScopeChild by qualifier, if the namespace represents a table.
221     *
222     * @param qualifier The qualifier (alias or table name) to search for
223     * @return The TTable if found and the namespace is a table, null otherwise
224     */
225    public TTable findTableByQualifier(String qualifier) {
226        ScopeChild child = findByAlias(qualifier);
227        if (child != null) {
228            INamespace ns = child.getNamespace();
229            if (ns != null && ns.getNode() instanceof TTable) {
230                return (TTable) ns.getNode();
231            }
232        }
233        return null;
234    }
235
236    /**
237     * Gets the first namespace that supports star columns or dynamic inference.
238     * Used for resolving unqualified column references.
239     *
240     * @return The preferred star namespace, or null if none exists
241     */
242    public INamespace getPreferredStarNamespace() {
243        return preferredStarNamespace;
244    }
245
246    /**
247     * Finds a candidate namespace for column resolution.
248     *
249     * <p>If a table prefix is provided, looks up by that prefix.
250     * Otherwise, returns the first star/dynamic namespace for unqualified columns.</p>
251     *
252     * @param tablePrefix The table prefix (alias or name), or null for unqualified columns
253     * @return The candidate namespace, or null if not found
254     */
255    public INamespace findCandidateNamespace(String tablePrefix) {
256        if (tablePrefix != null && !tablePrefix.isEmpty()) {
257            // Qualified reference - find by prefix
258            ScopeChild child = findByQualifier(tablePrefix);
259            if (child != null) {
260                return child.getNamespace();
261            }
262            return null;
263        } else {
264            // Unqualified reference - use star namespace
265            return preferredStarNamespace;
266        }
267    }
268
269    /**
270     * Gets the total number of children indexed.
271     *
272     * @return The child count
273     */
274    public int getChildCount() {
275        return childCount;
276    }
277
278    /**
279     * Checks if the index is empty.
280     *
281     * @return true if no children are indexed
282     */
283    public boolean isEmpty() {
284        return childCount == 0;
285    }
286
287    /**
288     * Creates an index from a scope, extracting the FromScope if needed.
289     * Walks the scope chain to find the {@link GlobalScope}'s name matcher
290     * so vendor-specific identifier rules apply (S2).
291     *
292     * @param scope The scope (SelectScope, UpdateScope, or FromScope)
293     * @return A new FromScopeIndex, or null if no FromScope is found
294     */
295    public static FromScopeIndex fromScope(IScope scope) {
296        if (scope == null) {
297            return null;
298        }
299
300        IScope fromScope = null;
301
302        if (scope instanceof SelectScope) {
303            fromScope = ((SelectScope) scope).getFromScope();
304        } else if (scope instanceof gudusoft.gsqlparser.resolver2.scope.UpdateScope) {
305            fromScope = ((gudusoft.gsqlparser.resolver2.scope.UpdateScope) scope).getFromScope();
306        } else if (scope instanceof gudusoft.gsqlparser.resolver2.scope.FromScope) {
307            fromScope = scope;
308        }
309
310        if (fromScope == null) {
311            return null;
312        }
313
314        return new FromScopeIndex(fromScope.getChildren(), findNameMatcher(fromScope));
315    }
316
317    /**
318     * Walk the parent chain to find the {@link GlobalScope}'s name matcher.
319     */
320    private static INameMatcher findNameMatcher(IScope cursor) {
321        while (cursor != null) {
322            if (cursor instanceof GlobalScope) {
323                INameMatcher m = ((GlobalScope) cursor).getNameMatcher();
324                if (m != null) {
325                    return m;
326                }
327                break;
328            }
329            IScope next = cursor.getParent();
330            if (next == cursor) break;
331            cursor = next;
332        }
333        return new DefaultNameMatcher();
334    }
335
336    @Override
337    public String toString() {
338        return String.format("FromScopeIndex(aliases=%d, names=%d, hasStar=%b)",
339            aliasMap.size(), nameMap.size(), preferredStarNamespace != null);
340    }
341}