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;
012import gudusoft.gsqlparser.stmt.TUpdateSqlStatement;
013
014import java.util.ArrayList;
015import java.util.Collections;
016import java.util.List;
017
018/**
019 * Scope for UPDATE statement.
020 *
021 * Contains:
022 * - Target table (the UPDATE table)
023 * - FROM clause scope (optional - for UPDATE...FROM syntax)
024 * - SET clause columns
025 * - WHERE clause scope
026 *
027 * Example:
028 * UPDATE titles
029 * SET total_sales = total_sales + qty
030 * FROM titles, salesdetail, sales
031 * WHERE titles.title_id = salesdetail.title_id
032 *
033 * Resolution strategy:
034 * - SET clause columns resolve from target table or FROM clause tables
035 * - WHERE clause columns resolve from all visible tables
036 */
037public class UpdateScope extends AbstractScope {
038
039    /** FROM clause scope (includes target table and FROM clause tables) */
040    private FromScope fromScope;
041
042    /** WHERE clause scope */
043    private IScope whereScope;
044
045    /** The UPDATE statement */
046    private final TUpdateSqlStatement updateStatement;
047
048    public UpdateScope(IScope parent, TUpdateSqlStatement stmt) {
049        super(parent, stmt, ScopeType.UPDATE);
050        this.updateStatement = stmt;
051    }
052
053    public void setFromScope(FromScope fromScope) {
054        this.fromScope = fromScope;
055    }
056
057    public FromScope getFromScope() {
058        return fromScope;
059    }
060
061    public void setWhereScope(IScope whereScope) {
062        this.whereScope = whereScope;
063    }
064
065    public IScope getWhereScope() {
066        return whereScope;
067    }
068
069    public TUpdateSqlStatement getUpdateStatement() {
070        return updateStatement;
071    }
072
073    @Override
074    public INamespace resolveTable(String tableName) {
075        // First try FROM scope
076        if (fromScope != null) {
077            INamespace ns = fromScope.resolveTable(tableName);
078            if (ns != null) {
079                return ns;
080            }
081        }
082
083        // Then delegate to parent
084        return super.resolveTable(tableName);
085    }
086
087    @Override
088    public List<INamespace> getVisibleNamespaces() {
089        List<INamespace> visible = new ArrayList<>();
090
091        // Add FROM scope's namespaces
092        if (fromScope != null) {
093            visible.addAll(fromScope.getVisibleNamespaces());
094        }
095
096        // Add parent's namespaces
097        visible.addAll(parent.getVisibleNamespaces());
098
099        return visible;
100    }
101
102    /**
103     * Resolve a name within this UPDATE scope.
104     *
105     * Resolution strategy:
106     * 1. For unqualified names (col): first check parent scope for PL/SQL variables,
107     *    then search all visible namespaces in FROM scope
108     * 2. For qualified names (t.col): find table t in FROM scope, then resolve col
109     * 3. Delegate to parent for names not found locally
110     */
111    @Override
112    public void resolve(List<String> names,
113                       INameMatcher matcher,
114                       boolean deep,
115                       IResolved resolved) {
116        if (names.isEmpty()) {
117            return;
118        }
119
120        boolean foundLocally = false;
121
122        // For single-part names (unqualified), first check if any parent scope has this
123        // as a PL/SQL variable. PL/SQL variables should take precedence over table columns.
124        // We need to check the entire parent chain because variables can be in outer scopes.
125        if (names.size() == 1) {
126            String varName = names.get(0);
127            IScope current = parent;
128            while (current != null && !(current instanceof GlobalScope)) {
129                if (current instanceof PlsqlBlockScope) {
130                    PlsqlBlockScope plsqlScope = (PlsqlBlockScope) current;
131                    ColumnLevel level = plsqlScope.getVariableNamespace().hasColumn(varName);
132                    if (level == ColumnLevel.EXISTS) {
133                        // This is a PL/SQL variable - resolve it through the scope that has it.
134                        // This prevents the variable from being incorrectly linked to a table column.
135                        plsqlScope.resolve(names, matcher, deep, resolved);
136                        return;
137                    }
138                }
139                current = current.getParent();
140            }
141        }
142
143        // Try to resolve via FROM scope
144        if (fromScope != null) {
145            if (names.size() >= 2) {
146                // Qualified name like "t.col" - let FROM scope handle it
147                String firstName = names.get(0);
148
149                // Check if first name matches any child in FROM scope
150                // Track matched aliases to avoid duplicate matches for same table name
151                java.util.Set<String> matchedAliases = new java.util.HashSet<>();
152                for (ScopeChild child : fromScope.getChildren()) {
153                    String alias = child.getAlias();
154                    if (matcher.matches(alias, firstName) && !matchedAliases.contains(alias)) {
155                        // Found matching table/subquery - only add once per alias
156                        matchedAliases.add(alias);
157                        INamespace namespace = child.getNamespace();
158                        List<String> remaining = names.subList(1, names.size());
159                        resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining);
160                        foundLocally = true;
161                    }
162                }
163            } else {
164                // Single name - could be a table alias or unqualified column
165                String name = names.get(0);
166
167                // First check if it's a table alias
168                for (ScopeChild child : fromScope.getChildren()) {
169                    if (matcher.matches(child.getAlias(), name)) {
170                        // It's a table alias - resolve as table
171                        resolved.found(child.getNamespace(), child.isNullable(),
172                                      this, new ResolvePath(), Collections.emptyList());
173                        foundLocally = true;
174                    }
175                }
176
177                // If not a table alias, try to resolve as unqualified column
178                if (!foundLocally) {
179                    // First, determine if we have a "sole implicit derived table" scenario
180                    // According to teradata_implicit_derived_tables_zh.md:
181                    // "当前作用域只可见 1 张表:未限定列默认归属该表"
182                    // If there are NO explicit tables AND exactly 1 implicit derived table,
183                    // unqualified columns should be linked to that implicit derived table
184                    List<INamespace> explicitTables = new ArrayList<>();
185                    List<INamespace> implicitDerivedTables = new ArrayList<>();
186
187                    for (ScopeChild child : fromScope.getChildren()) {
188                        INamespace ns = child.getNamespace();
189                        TTable table = ns.getFinalTable();
190                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
191                            implicitDerivedTables.add(ns);
192                        } else {
193                            explicitTables.add(ns);
194                        }
195                    }
196
197                    // If no explicit tables and exactly 1 implicit derived table,
198                    // include it for unqualified column resolution
199                    boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() &&
200                                                           implicitDerivedTables.size() == 1;
201
202                    // Search through all visible namespaces
203                    List<INamespace> candidates = new ArrayList<>();
204                    List<INamespace> maybeCandidates = new ArrayList<>();
205                    List<INamespace> inferredCandidates = new ArrayList<>();
206
207                    for (ScopeChild child : fromScope.getChildren()) {
208                        INamespace ns = child.getNamespace();
209
210                        // Skip implicit lateral derived tables (Teradata-specific)
211                        // They should only match qualified references like "employee.employee_id"
212                        // UNLESS this is a "sole implicit derived table" scenario where there
213                        // are no explicit tables and exactly 1 implicit derived table
214                        TTable table = ns.getFinalTable();
215                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
216                            if (!useSoleImplicitDerivedTable) {
217                                continue;
218                            }
219                        }
220
221                        ColumnLevel level = ns.hasColumn(name);
222
223                        if (level == ColumnLevel.EXISTS) {
224                            candidates.add(ns);
225                        } else if (level == ColumnLevel.MAYBE) {
226                            // MAYBE means the column might exist (no metadata to confirm)
227                            // For subqueries/CTEs with star columns, add to maybeCandidates
228                            // For physical tables without metadata, add to inferredCandidates
229                            // so we can detect ambiguity when multiple tables could have the column
230                            if (ns.supportsDynamicInference()) {
231                                maybeCandidates.add(ns);
232                            } else {
233                                // Physical tables without metadata - potential candidates for ambiguity
234                                inferredCandidates.add(ns);
235                            }
236                        }
237                    }
238
239                    // If we found exact matches, use them
240                    if (!candidates.isEmpty()) {
241                        for (INamespace ns : candidates) {
242                            resolved.found(ns, false, this, new ResolvePath(), names);
243                        }
244                        foundLocally = true;
245                    }
246                    // If we found MAYBE matches (star columns), only use if exactly one
247                    // Multiple MAYBE candidates means ambiguous - don't resolve
248                    else if (maybeCandidates.size() == 1) {
249                        resolved.found(maybeCandidates.get(0), false, this, new ResolvePath(), names);
250                        foundLocally = true;
251                    }
252                    // If multiple tables without metadata, report all for ambiguity detection
253                    // This allows NameResolver to detect that the column is ambiguous
254                    // and populate candidateTables with all potential sources
255                    else if (inferredCandidates.size() > 1) {
256                        for (INamespace ns : inferredCandidates) {
257                            resolved.found(ns, false, this, new ResolvePath(), names);
258                        }
259                        foundLocally = true;
260                    }
261                    // Single inferred candidate with no maybeCandidates - resolve to it
262                    // This handles the case where other tables definitively don't have the column
263                    // (e.g., subqueries with explicit SELECT lists that don't expose this column)
264                    // and only one table might have it (physical table without schema metadata)
265                    else if (inferredCandidates.size() == 1 && maybeCandidates.isEmpty()) {
266                        resolved.found(inferredCandidates.get(0), false, this, new ResolvePath(), names);
267                        foundLocally = true;
268                    }
269                    // Multiple maybe candidates means ambiguous with star columns - don't resolve
270                }
271            }
272        }
273
274        // If not found locally, delegate to parent
275        if (!foundLocally) {
276            parent.resolve(names, matcher, deep, resolved);
277        }
278    }
279
280    @Override
281    public String toString() {
282        return String.format("UpdateScope(from=%s)", fromScope != null ? "present" : "null");
283    }
284}