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        // The FROM scope's own relations. The FROM scope's parent is this scope, so asking it for ITS visible
092        // namespaces would come straight back here and never terminate (Mantis 4820).
093        addOwnNamespaces(visible, fromScope);
094
095        // Add parent's namespaces
096        addAllDistinct(visible, getLookupParent().getVisibleNamespaces());
097
098        return visible;
099    }
100
101    /**
102     * Resolve a name within this UPDATE scope.
103     *
104     * Resolution strategy:
105     * 1. For unqualified names (col): first check parent scope for PL/SQL variables,
106     *    then search all visible namespaces in FROM scope
107     * 2. For qualified names (t.col): find table t in FROM scope, then resolve col
108     * 3. Delegate to parent for names not found locally
109     */
110    @Override
111    public void resolve(List<String> names,
112                       INameMatcher matcher,
113                       boolean deep,
114                       IResolved resolved) {
115        if (names.isEmpty()) {
116            return;
117        }
118
119        boolean foundLocally = false;
120
121        // For single-part names (unqualified), first check if any parent scope has this
122        // as a PL/SQL variable. PL/SQL variables should take precedence over table columns.
123        // We need to check the entire parent chain because variables can be in outer scopes.
124        if (names.size() == 1) {
125            String varName = names.get(0);
126            IScope current = parent;
127            while (current != null && !(current instanceof GlobalScope)) {
128                if (current instanceof PlsqlBlockScope) {
129                    PlsqlBlockScope plsqlScope = (PlsqlBlockScope) current;
130                    ColumnLevel level = plsqlScope.getVariableNamespace().hasColumn(varName);
131                    if (level == ColumnLevel.EXISTS) {
132                        // This is a PL/SQL variable - resolve it through the scope that has it.
133                        // This prevents the variable from being incorrectly linked to a table column.
134                        plsqlScope.resolve(names, matcher, deep, resolved);
135                        return;
136                    }
137                }
138                current = current.getParent();
139            }
140        }
141
142        // Try to resolve via FROM scope
143        if (fromScope != null) {
144            if (names.size() >= 2) {
145                // Qualified name like "t.col" - let FROM scope handle it
146                String firstName = names.get(0);
147
148                // Check if first name matches any child in FROM scope
149                // Track matched aliases to avoid duplicate matches for same table name
150                java.util.Set<String> matchedAliases = new java.util.HashSet<>();
151                for (ScopeChild child : fromScope.getChildren()) {
152                    String alias = child.getAlias();
153                    if (matcher.matches(alias, firstName) && !matchedAliases.contains(alias)) {
154                        // Found matching table/subquery - only add once per alias
155                        matchedAliases.add(alias);
156                        INamespace namespace = child.getNamespace();
157                        List<String> remaining = names.subList(1, names.size());
158                        resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining);
159                        foundLocally = true;
160                    }
161                }
162            } else {
163                // Single name - could be a table alias or unqualified column
164                String name = names.get(0);
165
166                // First check if it's a table alias
167                for (ScopeChild child : fromScope.getChildren()) {
168                    if (matcher.matches(child.getAlias(), name)) {
169                        // It's a table alias - resolve as table
170                        resolved.found(child.getNamespace(), child.isNullable(),
171                                      this, new ResolvePath(), Collections.emptyList());
172                        foundLocally = true;
173                    }
174                }
175
176                // If not a table alias, try to resolve as unqualified column
177                if (!foundLocally) {
178                    // First, determine if we have a "sole implicit derived table" scenario
179                    // According to teradata_implicit_derived_tables_zh.md:
180                    // "当前作用域只可见 1 张表:未限定列默认归属该表"
181                    // If there are NO explicit tables AND exactly 1 implicit derived table,
182                    // unqualified columns should be linked to that implicit derived table
183                    List<INamespace> explicitTables = new ArrayList<>();
184                    List<INamespace> implicitDerivedTables = new ArrayList<>();
185
186                    for (ScopeChild child : fromScope.getChildren()) {
187                        INamespace ns = child.getNamespace();
188                        TTable table = ns.getFinalTable();
189                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
190                            implicitDerivedTables.add(ns);
191                        } else {
192                            explicitTables.add(ns);
193                        }
194                    }
195
196                    // If no explicit tables and exactly 1 implicit derived table,
197                    // include it for unqualified column resolution
198                    boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() &&
199                                                           implicitDerivedTables.size() == 1;
200
201                    // Search through all visible namespaces
202                    List<INamespace> candidates = new ArrayList<>();
203                    List<INamespace> maybeCandidates = new ArrayList<>();
204                    List<INamespace> inferredCandidates = new ArrayList<>();
205
206                    for (ScopeChild child : fromScope.getChildren()) {
207                        INamespace ns = child.getNamespace();
208
209                        // Skip implicit lateral derived tables (Teradata-specific)
210                        // They should only match qualified references like "employee.employee_id"
211                        // UNLESS this is a "sole implicit derived table" scenario where there
212                        // are no explicit tables and exactly 1 implicit derived table
213                        TTable table = ns.getFinalTable();
214                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
215                            if (!useSoleImplicitDerivedTable) {
216                                continue;
217                            }
218                        }
219
220                        ColumnLevel level = ns.hasColumn(name);
221
222                        if (level == ColumnLevel.EXISTS) {
223                            candidates.add(ns);
224                        } else if (level == ColumnLevel.MAYBE) {
225                            // MAYBE means the column might exist (no metadata to confirm)
226                            // For subqueries/CTEs with star columns, add to maybeCandidates
227                            // For physical tables without metadata, add to inferredCandidates
228                            // so we can detect ambiguity when multiple tables could have the column
229                            if (ns.supportsDynamicInference()) {
230                                maybeCandidates.add(ns);
231                            } else {
232                                // Physical tables without metadata - potential candidates for ambiguity
233                                inferredCandidates.add(ns);
234                            }
235                        }
236                    }
237
238                    // If we found exact matches, use them
239                    if (!candidates.isEmpty()) {
240                        for (INamespace ns : candidates) {
241                            resolved.found(ns, false, this, new ResolvePath(), names);
242                        }
243                        foundLocally = true;
244                    }
245                    // If we found MAYBE matches (star columns), only use if exactly one
246                    // Multiple MAYBE candidates means ambiguous - don't resolve
247                    else if (maybeCandidates.size() == 1) {
248                        resolved.found(maybeCandidates.get(0), false, this, new ResolvePath(), names);
249                        foundLocally = true;
250                    }
251                    // If multiple tables without metadata, report all for ambiguity detection
252                    // This allows NameResolver to detect that the column is ambiguous
253                    // and populate candidateTables with all potential sources
254                    else if (inferredCandidates.size() > 1) {
255                        for (INamespace ns : inferredCandidates) {
256                            resolved.found(ns, false, this, new ResolvePath(), names);
257                        }
258                        foundLocally = true;
259                    }
260                    // Single inferred candidate with no maybeCandidates - resolve to it
261                    // This handles the case where other tables definitively don't have the column
262                    // (e.g., subqueries with explicit SELECT lists that don't expose this column)
263                    // and only one table might have it (physical table without schema metadata)
264                    else if (inferredCandidates.size() == 1 && maybeCandidates.isEmpty()) {
265                        resolved.found(inferredCandidates.get(0), false, this, new ResolvePath(), names);
266                        foundLocally = true;
267                    }
268                    // Multiple maybe candidates means ambiguous with star columns - don't resolve
269                }
270            }
271        }
272
273        // If not found locally, delegate to parent
274        if (!foundLocally) {
275            parent.resolve(names, matcher, deep, resolved);
276        }
277    }
278
279    @Override
280    public String toString() {
281        return String.format("UpdateScope(from=%s)", fromScope != null ? "present" : "null");
282    }
283}