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
211                        for (INamespace ns : maybeCandidates) {
212                            if (ns instanceof gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) {
213                                gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace subNs =
214                                    (gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) ns;
215                                // Check if this subquery can trace the column through qualified refs
216                                // A subquery with SELECT * and JOIN condition can trace columns
217                                if (subNs.hasStarColumn()) {
218                                    // Try to resolve the column - if it returns a source with specific table, prefer it
219                                    gudusoft.gsqlparser.resolver2.model.ColumnSource source = subNs.resolveColumn(name);
220                                    if (source != null) {
221                                        // Check if the source has a specific table (unique resolution)
222                                        // or has evidence of qualified/traced resolution
223                                        if (source.getFinalTable() != null ||
224                                            (source.getEvidence() != null &&
225                                             (source.getEvidence().contains("traced") ||
226                                              source.getEvidence().contains("qualified") ||
227                                              source.getConfidence() >= 0.9))) {
228                                            tracedCandidates.add(ns);
229                                        } else if (!subNs.hasAmbiguousStar()) {
230                                            // Only add to inferred if NOT ambiguous star
231                                            // Ambiguous star (SELECT * from multiple tables) means column is truly ambiguous
232                                            inferredCandidates.add(ns);
233                                        }
234                                        // If hasAmbiguousStar() and no traced resolution, don't add - will be "missed"
235                                    } else if (!subNs.hasAmbiguousStar()) {
236                                        // Only add to inferred if NOT ambiguous star
237                                        inferredCandidates.add(ns);
238                                    }
239                                    // If hasAmbiguousStar() and source is null, don't add - will be "missed"
240                                } else {
241                                    tracedCandidates.add(ns); // Non-star subquery, use as-is
242                                }
243                            } else {
244                                // TableNamespace without metadata - add as candidate
245                                inferredCandidates.add(ns);
246                            }
247                        }
248
249                        // Prefer traced candidates over inferred candidates
250                        if (!tracedCandidates.isEmpty()) {
251                            for (INamespace ns : tracedCandidates) {
252                                resolved.found(ns, false, this, new ResolvePath(), names);
253                            }
254                            foundLocally = true;
255                        } else if (!inferredCandidates.isEmpty()) {
256                            // Report all inferred candidates - let NameResolver handle ambiguity
257                            // This populates candidateTables so formatter can use linkOrphanColumnToFirstTable
258                            for (INamespace ns : inferredCandidates) {
259                                resolved.found(ns, false, this, new ResolvePath(), names);
260                            }
261                            foundLocally = true;
262                        }
263                    }
264                }
265            }
266        }
267
268        // If not found locally, delegate to parent (but NOT back through FROM scope)
269        if (!foundLocally) {
270            parent.resolve(names, matcher, deep, resolved);
271        }
272    }
273
274    @Override
275    public String toString() {
276        return String.format("SelectScope(from=%s)", fromScope != null ? "present" : "null");
277    }
278}