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 // Add FROM scope's namespaces 092 if (fromScope != null) { 093 visible.addAll(fromScope.getVisibleNamespaces()); 094 } 095 096 // Add parent's namespaces 097 visible.addAll(parent.getVisibleNamespaces()); 098 099 return visible; 100 } 101 102 /** 103 * Resolve a name within this UPDATE scope. 104 * 105 * Resolution strategy: 106 * 1. For unqualified names (col): first check parent scope for PL/SQL variables, 107 * then search all visible namespaces in FROM scope 108 * 2. For qualified names (t.col): find table t in FROM scope, then resolve col 109 * 3. Delegate to parent for names not found locally 110 */ 111 @Override 112 public void resolve(List<String> names, 113 INameMatcher matcher, 114 boolean deep, 115 IResolved resolved) { 116 if (names.isEmpty()) { 117 return; 118 } 119 120 boolean foundLocally = false; 121 122 // For single-part names (unqualified), first check if any parent scope has this 123 // as a PL/SQL variable. PL/SQL variables should take precedence over table columns. 124 // We need to check the entire parent chain because variables can be in outer scopes. 125 if (names.size() == 1) { 126 String varName = names.get(0); 127 IScope current = parent; 128 while (current != null && !(current instanceof GlobalScope)) { 129 if (current instanceof PlsqlBlockScope) { 130 PlsqlBlockScope plsqlScope = (PlsqlBlockScope) current; 131 ColumnLevel level = plsqlScope.getVariableNamespace().hasColumn(varName); 132 if (level == ColumnLevel.EXISTS) { 133 // This is a PL/SQL variable - resolve it through the scope that has it. 134 // This prevents the variable from being incorrectly linked to a table column. 135 plsqlScope.resolve(names, matcher, deep, resolved); 136 return; 137 } 138 } 139 current = current.getParent(); 140 } 141 } 142 143 // Try to resolve via FROM scope 144 if (fromScope != null) { 145 if (names.size() >= 2) { 146 // Qualified name like "t.col" - let FROM scope handle it 147 String firstName = names.get(0); 148 149 // Check if first name matches any child in FROM scope 150 // Track matched aliases to avoid duplicate matches for same table name 151 java.util.Set<String> matchedAliases = new java.util.HashSet<>(); 152 for (ScopeChild child : fromScope.getChildren()) { 153 String alias = child.getAlias(); 154 if (matcher.matches(alias, firstName) && !matchedAliases.contains(alias)) { 155 // Found matching table/subquery - only add once per alias 156 matchedAliases.add(alias); 157 INamespace namespace = child.getNamespace(); 158 List<String> remaining = names.subList(1, names.size()); 159 resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining); 160 foundLocally = true; 161 } 162 } 163 } else { 164 // Single name - could be a table alias or unqualified column 165 String name = names.get(0); 166 167 // First check if it's a table alias 168 for (ScopeChild child : fromScope.getChildren()) { 169 if (matcher.matches(child.getAlias(), name)) { 170 // It's a table alias - resolve as table 171 resolved.found(child.getNamespace(), child.isNullable(), 172 this, new ResolvePath(), Collections.emptyList()); 173 foundLocally = true; 174 } 175 } 176 177 // If not a table alias, try to resolve as unqualified column 178 if (!foundLocally) { 179 // First, determine if we have a "sole implicit derived table" scenario 180 // According to teradata_implicit_derived_tables_zh.md: 181 // "当前作用域只可见 1 张表:未限定列默认归属该表" 182 // If there are NO explicit tables AND exactly 1 implicit derived table, 183 // unqualified columns should be linked to that implicit derived table 184 List<INamespace> explicitTables = new ArrayList<>(); 185 List<INamespace> implicitDerivedTables = new ArrayList<>(); 186 187 for (ScopeChild child : fromScope.getChildren()) { 188 INamespace ns = child.getNamespace(); 189 TTable table = ns.getFinalTable(); 190 if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) { 191 implicitDerivedTables.add(ns); 192 } else { 193 explicitTables.add(ns); 194 } 195 } 196 197 // If no explicit tables and exactly 1 implicit derived table, 198 // include it for unqualified column resolution 199 boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() && 200 implicitDerivedTables.size() == 1; 201 202 // Search through all visible namespaces 203 List<INamespace> candidates = new ArrayList<>(); 204 List<INamespace> maybeCandidates = new ArrayList<>(); 205 List<INamespace> inferredCandidates = new ArrayList<>(); 206 207 for (ScopeChild child : fromScope.getChildren()) { 208 INamespace ns = child.getNamespace(); 209 210 // Skip implicit lateral derived tables (Teradata-specific) 211 // They should only match qualified references like "employee.employee_id" 212 // UNLESS this is a "sole implicit derived table" scenario where there 213 // are no explicit tables and exactly 1 implicit derived table 214 TTable table = ns.getFinalTable(); 215 if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) { 216 if (!useSoleImplicitDerivedTable) { 217 continue; 218 } 219 } 220 221 ColumnLevel level = ns.hasColumn(name); 222 223 if (level == ColumnLevel.EXISTS) { 224 candidates.add(ns); 225 } else if (level == ColumnLevel.MAYBE) { 226 // MAYBE means the column might exist (no metadata to confirm) 227 // For subqueries/CTEs with star columns, add to maybeCandidates 228 // For physical tables without metadata, add to inferredCandidates 229 // so we can detect ambiguity when multiple tables could have the column 230 if (ns.supportsDynamicInference()) { 231 maybeCandidates.add(ns); 232 } else { 233 // Physical tables without metadata - potential candidates for ambiguity 234 inferredCandidates.add(ns); 235 } 236 } 237 } 238 239 // If we found exact matches, use them 240 if (!candidates.isEmpty()) { 241 for (INamespace ns : candidates) { 242 resolved.found(ns, false, this, new ResolvePath(), names); 243 } 244 foundLocally = true; 245 } 246 // If we found MAYBE matches (star columns), only use if exactly one 247 // Multiple MAYBE candidates means ambiguous - don't resolve 248 else if (maybeCandidates.size() == 1) { 249 resolved.found(maybeCandidates.get(0), false, this, new ResolvePath(), names); 250 foundLocally = true; 251 } 252 // If multiple tables without metadata, report all for ambiguity detection 253 // This allows NameResolver to detect that the column is ambiguous 254 // and populate candidateTables with all potential sources 255 else if (inferredCandidates.size() > 1) { 256 for (INamespace ns : inferredCandidates) { 257 resolved.found(ns, false, this, new ResolvePath(), names); 258 } 259 foundLocally = true; 260 } 261 // Single inferred candidate with no maybeCandidates - resolve to it 262 // This handles the case where other tables definitively don't have the column 263 // (e.g., subqueries with explicit SELECT lists that don't expose this column) 264 // and only one table might have it (physical table without schema metadata) 265 else if (inferredCandidates.size() == 1 && maybeCandidates.isEmpty()) { 266 resolved.found(inferredCandidates.get(0), false, this, new ResolvePath(), names); 267 foundLocally = true; 268 } 269 // Multiple maybe candidates means ambiguous with star columns - don't resolve 270 } 271 } 272 } 273 274 // If not found locally, delegate to parent 275 if (!foundLocally) { 276 parent.resolve(names, matcher, deep, resolved); 277 } 278 } 279 280 @Override 281 public String toString() { 282 return String.format("UpdateScope(from=%s)", fromScope != null ? "present" : "null"); 283 } 284}