001package gudusoft.gsqlparser.resolver2.scope;
002
003import gudusoft.gsqlparser.nodes.TParseTreeNode;
004import gudusoft.gsqlparser.resolver2.ScopeType;
005import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher;
006import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
007import gudusoft.gsqlparser.resolver2.matcher.VendorNameMatcher;
008import gudusoft.gsqlparser.resolver2.model.ResolvePath;
009import gudusoft.gsqlparser.resolver2.model.ScopeChild;
010import gudusoft.gsqlparser.resolver2.namespace.INamespace;
011import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
012
013import java.util.ArrayList;
014import java.util.Collections;
015import java.util.List;
016
017/**
018 * Base class for scopes that manage a list of child namespaces.
019 * Used for FROM clauses, which contain multiple tables/subqueries.
020 *
021 * Key features:
022 * 1. Maintains ordered list of children
023 * 2. Resolves names by searching through children
024 * 3. Supports aliases
025 */
026public abstract class ListBasedScope extends AbstractScope {
027
028    /** Child namespaces in this scope */
029    protected final List<ScopeChild> children = new ArrayList<>();
030
031    protected ListBasedScope(IScope parent, TParseTreeNode node, ScopeType scopeType) {
032        super(parent, node, scopeType);
033    }
034
035    @Override
036    public void addChild(INamespace namespace, String alias, boolean nullable) {
037        if (alias == null) {
038            throw new IllegalArgumentException("Alias cannot be null in ListBasedScope");
039        }
040        children.add(new ScopeChild(children.size(), alias, namespace, nullable));
041    }
042
043    @Override
044    public List<ScopeChild> getChildren() {
045        return Collections.unmodifiableList(children);
046    }
047
048    @Override
049    public INamespace resolveTable(String tableName) {
050        // Search in children only - do NOT delegate to parent
051        // The parent (SelectScope) already handles fallback to outer scopes
052        // Delegating to parent causes infinite recursion:
053        // SelectScope.resolveTable -> fromScope.resolveTable -> parent.resolveTable -> SelectScope.resolveTable -> ...
054        // Slice S15: route alias compare through INameMatcher so per-dialect
055        // quoted-vs-unquoted rules apply (e.g. Oracle quoted alias is
056        // case-sensitive, unquoted folds to upper). The matcher is sourced
057        // from the GlobalScope at the root of the scope chain. When the
058        // matcher is a {@link VendorNameMatcher}, route through {@link
059        // ESQLDataObjectType#dotTable} explicitly because BigQuery and
060        // MySQL have different rules between table and column object types
061        // (BigQuery: tables SENSITIVE, columns INSENSITIVE; MySQL: tables
062        // depend on lower_case_table_names, columns always INSENSITIVE).
063        INameMatcher matcher = findNameMatcher();
064        for (ScopeChild child : children) {
065            if (aliasMatches(matcher, child.getAlias(), tableName)) {
066                return child.getNamespace();
067            }
068        }
069
070        // Not found - return null, let caller handle fallback
071        return null;
072    }
073
074    private static boolean aliasMatches(INameMatcher matcher, String childAlias, String tableName) {
075        if (matcher instanceof VendorNameMatcher) {
076            return ((VendorNameMatcher) matcher).matches(childAlias, tableName, ESQLDataObjectType.dotTable);
077        }
078        return matcher.matches(childAlias, tableName);
079    }
080
081    /**
082     * Walk the parent chain to find the {@link GlobalScope}'s name matcher.
083     * Falls back to a {@link DefaultNameMatcher} if the chain does not
084     * include a {@code GlobalScope} (e.g. unit tests with synthetic scopes).
085     */
086    private INameMatcher findNameMatcher() {
087        IScope cursor = this;
088        while (cursor != null && !(cursor instanceof EmptyScope)) {
089            if (cursor instanceof GlobalScope) {
090                INameMatcher m = ((GlobalScope) cursor).getNameMatcher();
091                if (m != null) return m;
092                break;
093            }
094            IScope next = cursor.getParent();
095            if (next == cursor) break;
096            cursor = next;
097        }
098        return new DefaultNameMatcher();
099    }
100
101    @Override
102    public void resolve(List<String> names,
103                       INameMatcher matcher,
104                       boolean deep,
105                       IResolved resolved) {
106        if (names.isEmpty()) {
107            return;
108        }
109
110        String firstName = names.get(0);
111
112        // Search through children
113        for (ScopeChild child : children) {
114            if (matcher.matches(child.getAlias(), firstName)) {
115                // Found matching child
116                INamespace namespace = child.getNamespace();
117                ResolvePath path = new ResolvePath();
118
119                if (names.size() == 1) {
120                    // Just the table alias
121                    resolved.found(namespace, child.isNullable(), this, path, Collections.emptyList());
122                } else {
123                    // Resolve remaining parts in the namespace
124                    List<String> remaining = names.subList(1, names.size());
125                    resolveInNamespace(namespace, child.isNullable(), remaining, matcher, path, resolved);
126                }
127            }
128        }
129
130        // Also try parent scope
131        parent.resolve(names, matcher, deep, resolved);
132    }
133
134    /**
135     * Resolve remaining name parts within a namespace.
136     * For example, if we found table "t" and need to resolve "t.col1",
137     * this method handles resolving "col1" within table t.
138     */
139    protected void resolveInNamespace(INamespace namespace,
140                                     boolean nullable,
141                                     List<String> remainingNames,
142                                     INameMatcher matcher,
143                                     ResolvePath path,
144                                     IResolved resolved) {
145        if (remainingNames.isEmpty()) {
146            resolved.found(namespace, nullable, this, path, Collections.emptyList());
147            return;
148        }
149
150        // For now, simple implementation - just mark as found with remaining names
151        // Full implementation would traverse into structured types
152        resolved.found(namespace, nullable, this, path, remainingNames);
153    }
154
155    @Override
156    public List<INamespace> getVisibleNamespaces() {
157        List<INamespace> visible = new ArrayList<>();
158        for (ScopeChild child : children) {
159            visible.add(child.getNamespace());
160        }
161        // Add parent's visible namespaces
162        visible.addAll(parent.getVisibleNamespaces());
163        return visible;
164    }
165
166    @Override
167    public String toString() {
168        return String.format("%s(children=%d)", scopeType, children.size());
169    }
170}