001package gudusoft.gsqlparser.resolver2.scope;
002
003import gudusoft.gsqlparser.ETableEffectType;
004import gudusoft.gsqlparser.nodes.TParseTreeNode;
005import gudusoft.gsqlparser.nodes.TTable;
006import gudusoft.gsqlparser.resolver2.ColumnLevel;
007import gudusoft.gsqlparser.resolver2.ScopeType;
008import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
009import gudusoft.gsqlparser.resolver2.model.ResolvePath;
010import gudusoft.gsqlparser.resolver2.model.ScopeChild;
011import gudusoft.gsqlparser.resolver2.namespace.INamespace;
012
013import java.util.ArrayList;
014import java.util.Collections;
015import java.util.List;
016
017/**
018 * Scope for SELECT statement.
019 * This is typically the root scope for a query.
020 *
021 * Contains:
022 * - FROM clause scope (child)
023 * - WHERE clause scope (child)
024 * - SELECT list namespace (for ORDER BY to reference)
025 */
026public class SelectScope extends AbstractScope {
027
028    /** FROM clause scope */
029    private FromScope fromScope;
030
031    /** WHERE clause scope */
032    private IScope whereScope;
033
034    /** Namespace representing the SELECT list output */
035    private INamespace selectListNamespace;
036
037    public SelectScope(IScope parent, TParseTreeNode node) {
038        super(parent, node, ScopeType.SELECT);
039    }
040
041    public void setFromScope(FromScope fromScope) {
042        this.fromScope = fromScope;
043    }
044
045    public FromScope getFromScope() {
046        return fromScope;
047    }
048
049    public void setWhereScope(IScope whereScope) {
050        this.whereScope = whereScope;
051    }
052
053    public IScope getWhereScope() {
054        return whereScope;
055    }
056
057    public void setSelectListNamespace(INamespace namespace) {
058        this.selectListNamespace = namespace;
059    }
060
061    public INamespace getSelectListNamespace() {
062        return selectListNamespace;
063    }
064
065    @Override
066    public INamespace resolveTable(String tableName) {
067        // First try FROM scope
068        if (fromScope != null) {
069            INamespace ns = fromScope.resolveTable(tableName);
070            if (ns != null) {
071                return ns;
072            }
073        }
074
075        // Then delegate to parent
076        return super.resolveTable(tableName);
077    }
078
079    @Override
080    public List<INamespace> getVisibleNamespaces() {
081        List<INamespace> visible = new ArrayList<>();
082
083        // Add FROM scope's namespaces
084        if (fromScope != null) {
085            visible.addAll(fromScope.getVisibleNamespaces());
086        }
087
088        // Add parent's namespaces
089        visible.addAll(parent.getVisibleNamespaces());
090
091        return visible;
092    }
093
094    /**
095     * Resolve a name within this SELECT scope.
096     *
097     * Resolution strategy:
098     * 1. For qualified names (t.col): find table t in FROM scope, then resolve col
099     * 2. For unqualified names (col): search all visible namespaces in FROM scope
100     * 3. Delegate to parent for names not found locally
101     */
102    @Override
103    public void resolve(List<String> names,
104                       INameMatcher matcher,
105                       boolean deep,
106                       IResolved resolved) {
107        if (names.isEmpty()) {
108            return;
109        }
110
111        boolean foundLocally = false;
112
113        // First, try to resolve via FROM scope
114        if (fromScope != null) {
115            if (names.size() >= 2) {
116                // Qualified name like "t.col" - let FROM scope handle it
117                // But don't let FROM scope delegate back to parent (we'll do that)
118                String firstName = names.get(0);
119
120                // Check if first name matches any child in FROM scope
121                for (ScopeChild child : fromScope.getChildren()) {
122                    if (matcher.matches(child.getAlias(), firstName)) {
123                        // Found matching table/subquery
124                        INamespace namespace = child.getNamespace();
125                        List<String> remaining = names.subList(1, names.size());
126                        resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining);
127                        foundLocally = true;
128                    }
129                }
130            } else {
131                // Single name - could be a table alias or unqualified column
132                String name = names.get(0);
133
134                // First check if it's a table alias
135                for (ScopeChild child : fromScope.getChildren()) {
136                    if (matcher.matches(child.getAlias(), name)) {
137                        // It's a table alias - resolve as table
138                        resolved.found(child.getNamespace(), child.isNullable(),
139                                      this, new ResolvePath(), Collections.emptyList());
140                        foundLocally = true;
141                    }
142                }
143
144                // If not a table alias, try to resolve as unqualified column
145                if (!foundLocally) {
146                    // First, determine if we have a "sole implicit derived table" scenario
147                    // According to teradata_implicit_derived_tables_zh.md:
148                    // "当前作用域只可见 1 张表:未限定列默认归属该表"
149                    // If there are NO explicit tables AND exactly 1 implicit derived table,
150                    // unqualified columns should be linked to that implicit derived table
151                    List<INamespace> explicitTables = new ArrayList<>();
152                    List<INamespace> implicitDerivedTables = new ArrayList<>();
153
154                    for (ScopeChild child : fromScope.getChildren()) {
155                        INamespace ns = child.getNamespace();
156                        TTable table = ns.getFinalTable();
157                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
158                            implicitDerivedTables.add(ns);
159                        } else {
160                            explicitTables.add(ns);
161                        }
162                    }
163
164                    // If no explicit tables and exactly 1 implicit derived table,
165                    // include it for unqualified column resolution
166                    boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() &&
167                                                           implicitDerivedTables.size() == 1;
168
169                    // Search through all visible namespaces
170                    List<INamespace> candidates = new ArrayList<>();
171                    List<INamespace> maybeCandidates = new ArrayList<>();
172
173                    for (ScopeChild child : fromScope.getChildren()) {
174                        INamespace ns = child.getNamespace();
175
176                        // Skip implicit lateral derived tables (Teradata-specific)
177                        // They should only match qualified references like "employee.employee_id"
178                        // UNLESS this is a "sole implicit derived table" scenario where there
179                        // are no explicit tables and exactly 1 implicit derived table
180                        TTable table = ns.getFinalTable();
181                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
182                            if (!useSoleImplicitDerivedTable) {
183                                continue;
184                            }
185                        }
186
187                        ColumnLevel level = ns.hasColumn(name);
188
189                        if (level == ColumnLevel.EXISTS) {
190                            candidates.add(ns);
191                        } else if (level == ColumnLevel.MAYBE) {
192                            // Star column or table without metadata - might have this column
193                            maybeCandidates.add(ns);
194                        }
195                    }
196
197                    // If we found exact matches, use them
198                    if (!candidates.isEmpty()) {
199                        for (INamespace ns : candidates) {
200                            resolved.found(ns, false, this, new ResolvePath(), names);
201                        }
202                        foundLocally = true;
203                    }
204                    // If we found MAYBE matches (star columns), prioritize subqueries that can
205                    // trace columns through qualified references over tables with just inferred columns
206                    else if (!maybeCandidates.isEmpty()) {
207                        // Separate subqueries that can trace the column from tables that can't
208                        List<INamespace> tracedCandidates = new ArrayList<>();
209                        List<INamespace> inferredCandidates = new ArrayList<>();
210                        // Mantis 4550: an ambiguous-star subquery (SELECT * over a
211                        // multi-table FROM) cannot trace an unqualified column to a
212                        // single base table, so it was historically dropped here
213                        // ("will be missed"). But at THIS (outer) scope the subquery
214                        // as a whole is a legitimate source for the column, so when
215                        // there is at least one OTHER candidate the column is genuinely
216                        // ambiguous across the derived tables. Collect these separately
217                        // and only report them when doing so yields >= 2 candidates,
218                        // preserving the "missed" behavior for a lone ambiguous star.
219                        List<INamespace> ambiguousStarMaybeCandidates = new ArrayList<>();
220
221                        for (INamespace ns : maybeCandidates) {
222                            if (ns instanceof gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) {
223                                gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace subNs =
224                                    (gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) ns;
225                                // Check if this subquery can trace the column through qualified refs
226                                // A subquery with SELECT * and JOIN condition can trace columns
227                                if (subNs.hasStarColumn()) {
228                                    // Try to resolve the column - if it returns a source with specific table, prefer it
229                                    gudusoft.gsqlparser.resolver2.model.ColumnSource source = subNs.resolveColumn(name);
230                                    if (source != null) {
231                                        // Check if the source has a specific table (unique resolution)
232                                        // or has evidence of qualified/traced resolution
233                                        if (source.getFinalTable() != null ||
234                                            (source.getEvidence() != null &&
235                                             (source.getEvidence().contains("traced") ||
236                                              source.getEvidence().contains("qualified") ||
237                                              source.getConfidence() >= 0.9))) {
238                                            tracedCandidates.add(ns);
239                                        } else if (!subNs.hasAmbiguousStar()) {
240                                            // Only add to inferred if NOT ambiguous star
241                                            // Ambiguous star (SELECT * from multiple tables) means column is truly ambiguous
242                                            inferredCandidates.add(ns);
243                                        } else {
244                                            // Ambiguous star, no traced resolution: outer-scope candidate
245                                            ambiguousStarMaybeCandidates.add(ns);
246                                        }
247                                    } else if (!subNs.hasAmbiguousStar()) {
248                                        // Only add to inferred if NOT ambiguous star
249                                        inferredCandidates.add(ns);
250                                    } else {
251                                        // Ambiguous star, column not traceable: outer-scope candidate
252                                        ambiguousStarMaybeCandidates.add(ns);
253                                    }
254                                } else {
255                                    tracedCandidates.add(ns); // Non-star subquery, use as-is
256                                }
257                            } else {
258                                // TableNamespace without metadata - add as candidate
259                                inferredCandidates.add(ns);
260                            }
261                        }
262
263                        // Prefer traced candidates over inferred candidates. In either
264                        // case, also surface ambiguous-star siblings when including them
265                        // produces a genuinely ambiguous (>= 2 candidate) result, so the
266                        // outer column is reported as ambiguous across all derived tables
267                        // rather than collapsing onto the single traceable one.
268                        List<INamespace> primary = !tracedCandidates.isEmpty() ? tracedCandidates
269                                : (!inferredCandidates.isEmpty() ? inferredCandidates : null);
270                        int totalWithAmbStar = (primary != null ? primary.size() : 0)
271                                + ambiguousStarMaybeCandidates.size();
272                        boolean includeAmbStar = !ambiguousStarMaybeCandidates.isEmpty()
273                                && totalWithAmbStar >= 2;
274                        if (primary != null) {
275                            for (INamespace ns : primary) {
276                                resolved.found(ns, false, this, new ResolvePath(), names);
277                            }
278                            if (includeAmbStar) {
279                                for (INamespace ns : ambiguousStarMaybeCandidates) {
280                                    resolved.found(ns, false, this, new ResolvePath(), names);
281                                }
282                            }
283                            foundLocally = true;
284                        } else if (includeAmbStar) {
285                            for (INamespace ns : ambiguousStarMaybeCandidates) {
286                                resolved.found(ns, false, this, new ResolvePath(), names);
287                            }
288                            foundLocally = true;
289                        }
290                    }
291                }
292            }
293        }
294
295        // If not found locally, delegate to parent (but NOT back through FROM scope)
296        if (!foundLocally) {
297            parent.resolve(names, matcher, deep, resolved);
298        }
299    }
300
301    @Override
302    public String toString() {
303        return String.format("SelectScope(from=%s)", fromScope != null ? "present" : "null");
304    }
305}