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}