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}