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}