001package gudusoft.gsqlparser.resolver2.scope;
002
003import gudusoft.gsqlparser.ETableEffectType;
004import gudusoft.gsqlparser.nodes.TTable;
005import gudusoft.gsqlparser.resolver2.ColumnLevel;
006import gudusoft.gsqlparser.resolver2.ScopeType;
007import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
008import gudusoft.gsqlparser.resolver2.model.ResolvePath;
009import gudusoft.gsqlparser.resolver2.model.ScopeChild;
010import gudusoft.gsqlparser.resolver2.namespace.INamespace;
011import gudusoft.gsqlparser.stmt.TMergeSqlStatement;
012
013import java.util.ArrayList;
014import java.util.Collections;
015import java.util.List;
016
017/**
018 * Scope for MERGE statement.
019 *
020 * Contains:
021 * - Target table (MERGE INTO table)
022 * - Source table/subquery (USING clause)
023 * - ON clause condition
024 * - WHEN MATCHED/NOT MATCHED clauses
025 *
026 * Example:
027 * MERGE INTO archive ar
028 * USING (SELECT activity, description FROM activities) ac
029 * ON (ar.activity = ac.activity)
030 * WHEN MATCHED THEN UPDATE SET description = ac.description
031 * WHEN NOT MATCHED THEN INSERT (activity, description) VALUES (ac.activity, ac.description)
032 *
033 * Resolution strategy:
034 * - All column references resolve from target table or source table/subquery
035 * - Qualified names (ar.col, ac.col) resolve to specific table
036 * - Unqualified names search all visible tables
037 */
038public class MergeScope extends AbstractScope {
039
040    /** FROM clause scope (contains target table and using table) */
041    private FromScope fromScope;
042
043    /** The MERGE statement */
044    private final TMergeSqlStatement mergeStatement;
045
046    public MergeScope(IScope parent, TMergeSqlStatement stmt) {
047        super(parent, stmt, ScopeType.MERGE);
048        this.mergeStatement = stmt;
049    }
050
051    public void setFromScope(FromScope fromScope) {
052        this.fromScope = fromScope;
053    }
054
055    public FromScope getFromScope() {
056        return fromScope;
057    }
058
059    public TMergeSqlStatement getMergeStatement() {
060        return mergeStatement;
061    }
062
063    @Override
064    public INamespace resolveTable(String tableName) {
065        // First try FROM scope
066        if (fromScope != null) {
067            INamespace ns = fromScope.resolveTable(tableName);
068            if (ns != null) {
069                return ns;
070            }
071        }
072
073        // Then delegate to parent
074        return super.resolveTable(tableName);
075    }
076
077    @Override
078    public List<INamespace> getVisibleNamespaces() {
079        List<INamespace> visible = new ArrayList<>();
080
081        // Add FROM scope's namespaces
082        if (fromScope != null) {
083            visible.addAll(fromScope.getVisibleNamespaces());
084        }
085
086        // Add parent's namespaces
087        visible.addAll(parent.getVisibleNamespaces());
088
089        return visible;
090    }
091
092    /**
093     * Resolve a name within this MERGE scope.
094     *
095     * Resolution strategy:
096     * 1. For qualified names (t.col): find table t in FROM scope, then resolve col
097     * 2. For unqualified names (col): search all visible namespaces in FROM scope
098     * 3. Delegate to parent for names not found locally
099     */
100    @Override
101    public void resolve(List<String> names,
102                       INameMatcher matcher,
103                       boolean deep,
104                       IResolved resolved) {
105        if (names.isEmpty()) {
106            return;
107        }
108
109        boolean foundLocally = false;
110
111        // First, try to resolve via FROM scope
112        if (fromScope != null) {
113            if (names.size() >= 2) {
114                // Qualified name like "t.col" - let FROM scope handle it
115                String firstName = names.get(0);
116
117                // Check if first name matches any child in FROM scope
118                java.util.Set<String> matchedAliases = new java.util.HashSet<>();
119                for (ScopeChild child : fromScope.getChildren()) {
120                    String alias = child.getAlias();
121                    if (matcher.matches(alias, firstName) && !matchedAliases.contains(alias)) {
122                        matchedAliases.add(alias);
123                        INamespace namespace = child.getNamespace();
124                        List<String> remaining = names.subList(1, names.size());
125                        resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining);
126                        foundLocally = true;
127                    }
128                }
129            } else {
130                // Single name - could be a table alias or unqualified column
131                String name = names.get(0);
132
133                // First check if it's a table alias
134                for (ScopeChild child : fromScope.getChildren()) {
135                    if (matcher.matches(child.getAlias(), name)) {
136                        // It's a table alias - resolve as table
137                        resolved.found(child.getNamespace(), child.isNullable(),
138                                      this, new ResolvePath(), Collections.emptyList());
139                        foundLocally = true;
140                    }
141                }
142
143                // If not a table alias, try to resolve as unqualified column
144                if (!foundLocally) {
145                    // First, determine if we have a "sole implicit derived table" scenario
146                    // According to teradata_implicit_derived_tables_zh.md:
147                    // "当前作用域只可见 1 张表:未限定列默认归属该表"
148                    // If there are NO explicit tables AND exactly 1 implicit derived table,
149                    // unqualified columns should be linked to that implicit derived table
150                    List<INamespace> explicitTables = new ArrayList<>();
151                    List<INamespace> implicitDerivedTables = new ArrayList<>();
152
153                    for (ScopeChild child : fromScope.getChildren()) {
154                        INamespace ns = child.getNamespace();
155                        TTable table = ns.getFinalTable();
156                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
157                            implicitDerivedTables.add(ns);
158                        } else {
159                            explicitTables.add(ns);
160                        }
161                    }
162
163                    // If no explicit tables and exactly 1 implicit derived table,
164                    // include it for unqualified column resolution
165                    boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() &&
166                                                           implicitDerivedTables.size() == 1;
167
168                    // Search through all visible namespaces
169                    List<INamespace> candidates = new ArrayList<>();
170                    List<INamespace> maybeCandidates = new ArrayList<>();
171                    List<INamespace> inferredCandidates = 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                            // MAYBE means the column might exist (no metadata to confirm)
193                            // For subqueries/CTEs with star columns, add to maybeCandidates
194                            // For physical tables without metadata, add to inferredCandidates
195                            // so we can detect ambiguity when multiple tables could have the column
196                            if (ns.supportsDynamicInference()) {
197                                maybeCandidates.add(ns);
198                            } else {
199                                // Physical tables without metadata - potential candidates for ambiguity
200                                inferredCandidates.add(ns);
201                            }
202                        }
203                    }
204
205                    // If we found exact matches, use them
206                    if (!candidates.isEmpty()) {
207                        for (INamespace ns : candidates) {
208                            resolved.found(ns, false, this, new ResolvePath(), names);
209                        }
210                        foundLocally = true;
211                    }
212                    // If we found MAYBE matches (star columns), only use if exactly one
213                    // Multiple MAYBE candidates means ambiguous - don't resolve
214                    else if (maybeCandidates.size() == 1) {
215                        resolved.found(maybeCandidates.get(0), false, this, new ResolvePath(), names);
216                        foundLocally = true;
217                    }
218                    // Single inferred candidate with no maybeCandidates - resolve to it
219                    // This handles the case where other tables definitively don't have the column
220                    // (e.g., subqueries with explicit SELECT lists that don't expose this column)
221                    // and only one table might have it (physical table without schema metadata)
222                    else if (inferredCandidates.size() == 1 && maybeCandidates.isEmpty()) {
223                        resolved.found(inferredCandidates.get(0), false, this, new ResolvePath(), names);
224                        foundLocally = true;
225                    }
226                    // Multiple inferred candidates or multiple maybe candidates - leave unresolved
227                    // The column will be handled by the old resolver or formatter's linkOrphanColumnToFirstTable
228                }
229            }
230        }
231
232        // If not found locally, delegate to parent
233        if (!foundLocally) {
234            parent.resolve(names, matcher, deep, resolved);
235        }
236    }
237
238    @Override
239    public String toString() {
240        return String.format("MergeScope(from=%s)", fromScope != null ? "present" : "null");
241    }
242}