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    /**
038     * One {@code JOIN ... USING} merge in this scope: the names it merged, the relations that took
039     * part, in JOIN ORDER, and the join type.
040     *
041     * <p>SQL exposes a merged column ONCE, so an unqualified reference to it is not ambiguous
042     * across the sides that merged it. All three fields are needed to say that safely:
043     * <ul>
044     *   <li>the NAMES, because only those columns merge;
045     *   <li>the PARTICIPANTS, because a third relation in the same scope carrying the same name is
046     *       still a genuine ambiguity — {@code SELECT c FROM a JOIN b USING(c), d} has three
047     *       candidates and only two of them merged;
048     *   <li>the ORDER and the join TYPE, because which side the merged value comes from depends on
049     *       them: the left for INNER and LEFT, the right for RIGHT, and both for FULL.
050     * </ul>
051     */
052    public static final class UsingMerge {
053        private final java.util.List<String> columnNames = new java.util.ArrayList<String>();
054        private final java.util.List<gudusoft.gsqlparser.nodes.TTable> participants =
055                new java.util.ArrayList<gudusoft.gsqlparser.nodes.TTable>();
056        private final gudusoft.gsqlparser.EJoinType joinType;
057
058        public UsingMerge(gudusoft.gsqlparser.EJoinType joinType) {
059            this.joinType = joinType;
060        }
061
062        public void addColumnName(String name) {
063            if (name != null && !name.isEmpty()) {
064                columnNames.add(name);
065            }
066        }
067
068        /** Participants are added LEFT side first, right side last, and the order is meaningful. */
069        public void addParticipant(gudusoft.gsqlparser.nodes.TTable table) {
070            if (table != null && !containsIdentical(table)) {
071                participants.add(table);
072            }
073        }
074
075        private boolean containsIdentical(gudusoft.gsqlparser.nodes.TTable table) {
076            for (gudusoft.gsqlparser.nodes.TTable seen : participants) {
077                if (seen == table) {
078                    return true;
079                }
080            }
081            return false;
082        }
083
084        /** Identifiers fold by the vendor's rules, so never compared with equals. */
085        public boolean merges(gudusoft.gsqlparser.EDbVendor vendor, String columnName) {
086            return merges(gudusoft.gsqlparser.sqlenv.IdentifierService.defaultServiceFor(vendor), columnName);
087        }
088
089        /** {@link #merges(gudusoft.gsqlparser.EDbVendor, String)} under one analysis's identifier rules. */
090        public boolean merges(gudusoft.gsqlparser.sqlenv.IdentifierService names, String columnName) {
091            if (columnName == null) {
092                return false;
093            }
094            for (String merged : columnNames) {
095                if (names.areEqual(merged, columnName, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType.dotColumn)) {
096                    return true;
097                }
098            }
099            return false;
100        }
101
102        public boolean hasParticipant(gudusoft.gsqlparser.nodes.TTable table) {
103            return table != null && containsIdentical(table);
104        }
105
106        public java.util.List<gudusoft.gsqlparser.nodes.TTable> getParticipants() {
107            return java.util.Collections.unmodifiableList(participants);
108        }
109
110        public gudusoft.gsqlparser.EJoinType getJoinType() {
111            return joinType;
112        }
113    }
114
115    private final java.util.List<UsingMerge> usingMerges = new java.util.ArrayList<UsingMerge>();
116
117    public SelectScope(IScope parent, TParseTreeNode node) {
118        super(parent, node, ScopeType.SELECT);
119    }
120
121    public void setFromScope(FromScope fromScope) {
122        this.fromScope = fromScope;
123    }
124
125    public FromScope getFromScope() {
126        return fromScope;
127    }
128
129    public void setWhereScope(IScope whereScope) {
130        this.whereScope = whereScope;
131    }
132
133    public IScope getWhereScope() {
134        return whereScope;
135    }
136
137    public void setSelectListNamespace(INamespace namespace) {
138        this.selectListNamespace = namespace;
139    }
140
141    public INamespace getSelectListNamespace() {
142        return selectListNamespace;
143    }
144
145    @Override
146    public INamespace resolveTable(String tableName) {
147        // First try FROM scope
148        if (fromScope != null) {
149            INamespace ns = fromScope.resolveTable(tableName);
150            if (ns != null) {
151                return ns;
152            }
153        }
154
155        // Then delegate to parent
156        return super.resolveTable(tableName);
157    }
158
159    @Override
160    public List<INamespace> getVisibleNamespaces() {
161        List<INamespace> visible = new ArrayList<>();
162
163        // The FROM scope's own relations. The FROM scope's parent is this scope, so asking it for ITS visible
164        // namespaces would come straight back here and never terminate (Mantis 4820).
165        addOwnNamespaces(visible, fromScope);
166
167        // Add parent's namespaces
168        addAllDistinct(visible, getLookupParent().getVisibleNamespaces());
169
170        return visible;
171    }
172
173    /**
174     * Resolve a name within this SELECT scope.
175     *
176     * Resolution strategy:
177     * 1. For qualified names (t.col): find table t in FROM scope, then resolve col
178     * 2. For unqualified names (col): search all visible namespaces in FROM scope
179     * 3. Delegate to parent for names not found locally
180     */
181    @Override
182    public void resolve(List<String> names,
183                       INameMatcher matcher,
184                       boolean deep,
185                       IResolved resolved) {
186        if (names.isEmpty()) {
187            return;
188        }
189
190        boolean foundLocally = false;
191
192        // First, try to resolve via FROM scope
193        if (fromScope != null) {
194            if (names.size() >= 2) {
195                // Qualified name like "t.col" - let FROM scope handle it
196                // But don't let FROM scope delegate back to parent (we'll do that)
197                String firstName = names.get(0);
198
199                // Check if first name matches any child in FROM scope
200                for (ScopeChild child : fromScope.getChildren()) {
201                    if (matcher.matches(child.getAlias(), firstName)) {
202                        // Found matching table/subquery
203                        INamespace namespace = child.getNamespace();
204                        List<String> remaining = names.subList(1, names.size());
205                        resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining);
206                        foundLocally = true;
207                    }
208                }
209            } else {
210                // Single name - could be a table alias or unqualified column
211                String name = names.get(0);
212
213                // First check if it's a table alias
214                for (ScopeChild child : fromScope.getChildren()) {
215                    if (matcher.matches(child.getAlias(), name)) {
216                        // It's a table alias - resolve as table
217                        resolved.found(child.getNamespace(), child.isNullable(),
218                                      this, new ResolvePath(), Collections.emptyList());
219                        foundLocally = true;
220                    }
221                }
222
223                // If not a table alias, try to resolve as unqualified column
224                if (!foundLocally) {
225                    // First, determine if we have a "sole implicit derived table" scenario
226                    // According to teradata_implicit_derived_tables_zh.md:
227                    // "当前作用域只可见 1 张表:未限定列默认归属该表"
228                    // If there are NO explicit tables AND exactly 1 implicit derived table,
229                    // unqualified columns should be linked to that implicit derived table
230                    List<INamespace> explicitTables = new ArrayList<>();
231                    List<INamespace> implicitDerivedTables = new ArrayList<>();
232
233                    for (ScopeChild child : fromScope.getChildren()) {
234                        INamespace ns = child.getNamespace();
235                        TTable table = ns.getFinalTable();
236                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
237                            implicitDerivedTables.add(ns);
238                        } else {
239                            explicitTables.add(ns);
240                        }
241                    }
242
243                    // If no explicit tables and exactly 1 implicit derived table,
244                    // include it for unqualified column resolution
245                    boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() &&
246                                                           implicitDerivedTables.size() == 1;
247
248                    // Search through all visible namespaces
249                    List<INamespace> candidates = new ArrayList<>();
250                    List<INamespace> maybeCandidates = new ArrayList<>();
251
252                    for (ScopeChild child : fromScope.getChildren()) {
253                        INamespace ns = child.getNamespace();
254
255                        // Skip implicit lateral derived tables (Teradata-specific)
256                        // They should only match qualified references like "employee.employee_id"
257                        // UNLESS this is a "sole implicit derived table" scenario where there
258                        // are no explicit tables and exactly 1 implicit derived table
259                        TTable table = ns.getFinalTable();
260                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
261                            if (!useSoleImplicitDerivedTable) {
262                                continue;
263                            }
264                        }
265
266                        ColumnLevel level = ns.hasColumn(name);
267
268                        if (level == ColumnLevel.EXISTS) {
269                            candidates.add(ns);
270                        } else if (level == ColumnLevel.MAYBE) {
271                            // Star column or table without metadata - might have this column
272                            maybeCandidates.add(ns);
273                        }
274                    }
275
276                    // If we found exact matches, use them
277                    if (!candidates.isEmpty()) {
278                        for (INamespace ns : candidates) {
279                            resolved.found(ns, false, this, new ResolvePath(), names);
280                        }
281                        foundLocally = true;
282                    }
283                    // If we found MAYBE matches (star columns), prioritize subqueries that can
284                    // trace columns through qualified references over tables with just inferred columns
285                    else if (!maybeCandidates.isEmpty()) {
286                        // Separate subqueries that can trace the column from tables that can't
287                        List<INamespace> tracedCandidates = new ArrayList<>();
288                        List<INamespace> inferredCandidates = new ArrayList<>();
289                        // Mantis 4550: an ambiguous-star subquery (SELECT * over a
290                        // multi-table FROM) cannot trace an unqualified column to a
291                        // single base table, so it was historically dropped here
292                        // ("will be missed"). But at THIS (outer) scope the subquery
293                        // as a whole is a legitimate source for the column, so when
294                        // there is at least one OTHER candidate the column is genuinely
295                        // ambiguous across the derived tables. Collect these separately
296                        // and only report them when doing so yields >= 2 candidates,
297                        // preserving the "missed" behavior for a lone ambiguous star.
298                        List<INamespace> ambiguousStarMaybeCandidates = new ArrayList<>();
299
300                        for (INamespace ns : maybeCandidates) {
301                            if (ns instanceof gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) {
302                                gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace subNs =
303                                    (gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) ns;
304                                // Check if this subquery can trace the column through qualified refs
305                                // A subquery with SELECT * and JOIN condition can trace columns
306                                if (subNs.hasStarColumn()) {
307                                    // Try to resolve the column - if it returns a source with specific table, prefer it
308                                    gudusoft.gsqlparser.resolver2.model.ColumnSource source = subNs.resolveColumn(name);
309                                    if (source != null) {
310                                        // Check if the source has a specific table (unique resolution)
311                                        // or has evidence of qualified/traced resolution
312                                        if (source.getFinalTable() != null ||
313                                            (source.getEvidence() != null &&
314                                             (source.getEvidence().contains("traced") ||
315                                              source.getEvidence().contains("qualified") ||
316                                              source.getConfidence() >= 0.9))) {
317                                            tracedCandidates.add(ns);
318                                        } else if (!subNs.hasAmbiguousStar()) {
319                                            // Only add to inferred if NOT ambiguous star
320                                            // Ambiguous star (SELECT * from multiple tables) means column is truly ambiguous
321                                            inferredCandidates.add(ns);
322                                        } else {
323                                            // Ambiguous star, no traced resolution: outer-scope candidate
324                                            ambiguousStarMaybeCandidates.add(ns);
325                                        }
326                                    } else if (!subNs.hasAmbiguousStar()) {
327                                        // Only add to inferred if NOT ambiguous star
328                                        inferredCandidates.add(ns);
329                                    } else {
330                                        // Ambiguous star, column not traceable: outer-scope candidate
331                                        ambiguousStarMaybeCandidates.add(ns);
332                                    }
333                                } else {
334                                    tracedCandidates.add(ns); // Non-star subquery, use as-is
335                                }
336                            } else {
337                                // TableNamespace without metadata - add as candidate
338                                inferredCandidates.add(ns);
339                            }
340                        }
341
342                        // Prefer traced candidates over inferred candidates. In either
343                        // case, also surface ambiguous-star siblings when including them
344                        // produces a genuinely ambiguous (>= 2 candidate) result, so the
345                        // outer column is reported as ambiguous across all derived tables
346                        // rather than collapsing onto the single traceable one.
347                        List<INamespace> primary = !tracedCandidates.isEmpty() ? tracedCandidates
348                                : (!inferredCandidates.isEmpty() ? inferredCandidates : null);
349                        int totalWithAmbStar = (primary != null ? primary.size() : 0)
350                                + ambiguousStarMaybeCandidates.size();
351                        boolean includeAmbStar = !ambiguousStarMaybeCandidates.isEmpty()
352                                && totalWithAmbStar >= 2;
353                        if (primary != null) {
354                            for (INamespace ns : primary) {
355                                resolved.found(ns, false, this, new ResolvePath(), names);
356                            }
357                            if (includeAmbStar) {
358                                for (INamespace ns : ambiguousStarMaybeCandidates) {
359                                    resolved.found(ns, false, this, new ResolvePath(), names);
360                                }
361                            }
362                            foundLocally = true;
363                        } else if (includeAmbStar) {
364                            for (INamespace ns : ambiguousStarMaybeCandidates) {
365                                resolved.found(ns, false, this, new ResolvePath(), names);
366                            }
367                            foundLocally = true;
368                        }
369                    }
370                }
371            }
372        }
373
374        // If not found locally, delegate to parent (but NOT back through FROM scope)
375        if (!foundLocally) {
376            getLookupParent().resolve(names, matcher, deep, resolved);
377        }
378    }
379
380    @Override
381    public String toString() {
382        return String.format("SelectScope(from=%s)", fromScope != null ? "present" : "null");
383    }
384
385    /** Records a JOIN USING merge of this scope. */
386    public void addUsingMerge(UsingMerge merge) {
387        if (merge != null) {
388            usingMerges.add(merge);
389        }
390    }
391
392    /**
393     * The merge that covers this column name and EVERY one of the given relations, or null.
394     *
395     * <p>Requiring the merge to cover every candidate is what keeps a third relation carrying the
396     * same name from being swallowed: those candidates are not merged with anything and the
397     * reference really is ambiguous.
398     */
399    public UsingMerge findCoveringMerge(gudusoft.gsqlparser.EDbVendor vendor, String columnName,
400                                        java.util.List<gudusoft.gsqlparser.nodes.TTable> candidateTables) {
401        return findCoveringMerge(gudusoft.gsqlparser.sqlenv.IdentifierService.defaultServiceFor(vendor),
402                columnName, candidateTables);
403    }
404
405    /**
406     * {@link #findCoveringMerge(gudusoft.gsqlparser.EDbVendor, String, java.util.List)} with the
407     * column names compared under one analysis's identifier rules (a configured profile).
408     */
409    public UsingMerge findCoveringMerge(gudusoft.gsqlparser.sqlenv.IdentifierService names, String columnName,
410                                        java.util.List<gudusoft.gsqlparser.nodes.TTable> candidateTables) {
411        if (candidateTables == null || candidateTables.isEmpty()) {
412            return null;
413        }
414        for (UsingMerge merge : usingMerges) {
415            if (!merge.merges(names, columnName)) {
416                continue;
417            }
418            boolean coversAll = true;
419            for (gudusoft.gsqlparser.nodes.TTable table : candidateTables) {
420                if (!merge.hasParticipant(table)) {
421                    coversAll = false;
422                    break;
423                }
424            }
425            if (coversAll) {
426                return merge;
427            }
428        }
429        return null;
430    }
431
432}