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        // The FROM scope's own relations. The FROM scope's parent is this scope, so asking it for ITS visible
082        // namespaces would come straight back here and never terminate (Mantis 4820).
083        addOwnNamespaces(visible, fromScope);
084
085        // Add parent's namespaces
086        addAllDistinct(visible, getLookupParent().getVisibleNamespaces());
087
088        return visible;
089    }
090
091    /**
092     * Resolve a name within this MERGE scope.
093     *
094     * Resolution strategy:
095     * 1. For qualified names (t.col): find table t in FROM scope, then resolve col
096     * 2. For unqualified names (col): search all visible namespaces in FROM scope
097     * 3. Delegate to parent for names not found locally
098     */
099    @Override
100    public void resolve(List<String> names,
101                       INameMatcher matcher,
102                       boolean deep,
103                       IResolved resolved) {
104        if (names.isEmpty()) {
105            return;
106        }
107
108        boolean foundLocally = false;
109
110        // First, try to resolve via FROM scope
111        if (fromScope != null) {
112            if (names.size() >= 2) {
113                // Qualified name like "t.col" - let FROM scope handle it
114                String firstName = names.get(0);
115
116                // Check if first name matches any child in FROM scope
117                java.util.Set<String> matchedAliases = new java.util.HashSet<>();
118                for (ScopeChild child : fromScope.getChildren()) {
119                    String alias = child.getAlias();
120                    if (matcher.matches(alias, firstName) && !matchedAliases.contains(alias)) {
121                        matchedAliases.add(alias);
122                        INamespace namespace = child.getNamespace();
123                        List<String> remaining = names.subList(1, names.size());
124                        resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining);
125                        foundLocally = true;
126                    }
127                }
128            } else {
129                // Single name - could be a table alias or unqualified column
130                String name = names.get(0);
131
132                // First check if it's a table alias
133                for (ScopeChild child : fromScope.getChildren()) {
134                    if (matcher.matches(child.getAlias(), name)) {
135                        // It's a table alias - resolve as table
136                        resolved.found(child.getNamespace(), child.isNullable(),
137                                      this, new ResolvePath(), Collections.emptyList());
138                        foundLocally = true;
139                    }
140                }
141
142                // If not a table alias, try to resolve as unqualified column
143                if (!foundLocally) {
144                    // First, determine if we have a "sole implicit derived table" scenario
145                    // According to teradata_implicit_derived_tables_zh.md:
146                    // "当前作用域只可见 1 张表:未限定列默认归属该表"
147                    // If there are NO explicit tables AND exactly 1 implicit derived table,
148                    // unqualified columns should be linked to that implicit derived table
149                    List<INamespace> explicitTables = new ArrayList<>();
150                    List<INamespace> implicitDerivedTables = new ArrayList<>();
151
152                    for (ScopeChild child : fromScope.getChildren()) {
153                        INamespace ns = child.getNamespace();
154                        TTable table = ns.getFinalTable();
155                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
156                            implicitDerivedTables.add(ns);
157                        } else {
158                            explicitTables.add(ns);
159                        }
160                    }
161
162                    // If no explicit tables and exactly 1 implicit derived table,
163                    // include it for unqualified column resolution
164                    boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() &&
165                                                           implicitDerivedTables.size() == 1;
166
167                    // Search through all visible namespaces
168                    List<INamespace> candidates = new ArrayList<>();
169                    List<INamespace> maybeCandidates = new ArrayList<>();
170                    List<INamespace> inferredCandidates = new ArrayList<>();
171
172                    for (ScopeChild child : fromScope.getChildren()) {
173                        INamespace ns = child.getNamespace();
174
175                        // Skip implicit lateral derived tables (Teradata-specific)
176                        // They should only match qualified references like "employee.employee_id"
177                        // UNLESS this is a "sole implicit derived table" scenario where there
178                        // are no explicit tables and exactly 1 implicit derived table
179                        TTable table = ns.getFinalTable();
180                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
181                            if (!useSoleImplicitDerivedTable) {
182                                continue;
183                            }
184                        }
185
186                        ColumnLevel level = ns.hasColumn(name);
187
188                        if (level == ColumnLevel.EXISTS) {
189                            candidates.add(ns);
190                        } else if (level == ColumnLevel.MAYBE) {
191                            // MAYBE means the column might exist (no metadata to confirm)
192                            // For subqueries/CTEs with star columns, add to maybeCandidates
193                            // For physical tables without metadata, add to inferredCandidates
194                            // so we can detect ambiguity when multiple tables could have the column
195                            if (ns.supportsDynamicInference()) {
196                                maybeCandidates.add(ns);
197                            } else {
198                                // Physical tables without metadata - potential candidates for ambiguity
199                                inferredCandidates.add(ns);
200                            }
201                        }
202                    }
203
204                    // If we found exact matches, use them
205                    if (!candidates.isEmpty()) {
206                        for (INamespace ns : candidates) {
207                            resolved.found(ns, false, this, new ResolvePath(), names);
208                        }
209                        foundLocally = true;
210                    }
211                    // If we found MAYBE matches (star columns), only use if exactly one
212                    // Multiple MAYBE candidates means ambiguous - don't resolve
213                    else if (maybeCandidates.size() == 1) {
214                        resolved.found(maybeCandidates.get(0), false, this, new ResolvePath(), names);
215                        foundLocally = true;
216                    }
217                    // Single inferred candidate with no maybeCandidates - resolve to it
218                    // This handles the case where other tables definitively don't have the column
219                    // (e.g., subqueries with explicit SELECT lists that don't expose this column)
220                    // and only one table might have it (physical table without schema metadata)
221                    else if (inferredCandidates.size() == 1 && maybeCandidates.isEmpty()) {
222                        resolved.found(inferredCandidates.get(0), false, this, new ResolvePath(), names);
223                        foundLocally = true;
224                    }
225                    // Multiple inferred candidates or multiple maybe candidates - leave unresolved
226                    // The column will be handled by the old resolver or formatter's linkOrphanColumnToFirstTable
227                }
228            }
229        }
230
231        // If not found locally, delegate to parent
232        if (!foundLocally) {
233            parent.resolve(names, matcher, deep, resolved);
234        }
235    }
236
237    @Override
238    public String toString() {
239        return String.format("MergeScope(from=%s)", fromScope != null ? "present" : "null");
240    }
241}