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.TDeleteSqlStatement; 012 013import java.util.ArrayList; 014import java.util.Collections; 015import java.util.List; 016 017/** 018 * Scope for DELETE statement. 019 * 020 * Contains: 021 * - Target table (the DELETE table) 022 * - FROM clause scope (optional - for DELETE...FROM syntax in SQL Server) 023 * - WHERE clause scope 024 * 025 * Example (SQL Server): 026 * DELETE FROM Sales.SalesPersonQuotaHistory 027 * FROM Sales.SalesPersonQuotaHistory AS spqh 028 * INNER JOIN Sales.SalesPerson AS sp 029 * ON spqh.SalesPersonID = sp.SalesPersonID 030 * WHERE sp.SalesYTD > 2500000.00; 031 * 032 * Resolution strategy: 033 * - WHERE clause columns resolve from all visible tables (target table + FROM clause tables) 034 * - JOIN ON clause columns resolve from the joined tables 035 */ 036public class DeleteScope extends AbstractScope { 037 038 /** FROM clause scope (includes target table and FROM clause tables) */ 039 private FromScope fromScope; 040 041 /** WHERE clause scope */ 042 private IScope whereScope; 043 044 /** The DELETE statement */ 045 private final TDeleteSqlStatement deleteStatement; 046 047 public DeleteScope(IScope parent, TDeleteSqlStatement stmt) { 048 super(parent, stmt, ScopeType.DELETE); 049 this.deleteStatement = stmt; 050 } 051 052 public void setFromScope(FromScope fromScope) { 053 this.fromScope = fromScope; 054 } 055 056 public FromScope getFromScope() { 057 return fromScope; 058 } 059 060 public void setWhereScope(IScope whereScope) { 061 this.whereScope = whereScope; 062 } 063 064 public IScope getWhereScope() { 065 return whereScope; 066 } 067 068 public TDeleteSqlStatement getDeleteStatement() { 069 return deleteStatement; 070 } 071 072 @Override 073 public INamespace resolveTable(String tableName) { 074 // First try FROM scope 075 if (fromScope != null) { 076 INamespace ns = fromScope.resolveTable(tableName); 077 if (ns != null) { 078 return ns; 079 } 080 } 081 082 // Then delegate to parent 083 return super.resolveTable(tableName); 084 } 085 086 @Override 087 public List<INamespace> getVisibleNamespaces() { 088 List<INamespace> visible = new ArrayList<>(); 089 090 // Add FROM scope's namespaces 091 if (fromScope != null) { 092 visible.addAll(fromScope.getVisibleNamespaces()); 093 } 094 095 // Add parent's namespaces 096 visible.addAll(parent.getVisibleNamespaces()); 097 098 return visible; 099 } 100 101 /** 102 * Resolve a name within this DELETE scope. 103 * 104 * Resolution strategy (same as UpdateScope): 105 * 1. For qualified names (t.col): find table t in FROM scope, then resolve col 106 * 2. For unqualified names (col): search all visible namespaces in FROM scope 107 * 3. Delegate to parent for names not found locally 108 */ 109 @Override 110 public void resolve(List<String> names, 111 INameMatcher matcher, 112 boolean deep, 113 IResolved resolved) { 114 if (names.isEmpty()) { 115 return; 116 } 117 118 boolean foundLocally = false; 119 120 // First, try to resolve via FROM scope 121 if (fromScope != null) { 122 if (names.size() >= 2) { 123 // Qualified name like "t.col" - let FROM scope handle it 124 String firstName = names.get(0); 125 126 // Check if first name matches any child in FROM scope 127 // Track matched aliases to avoid duplicate matches for same table name 128 java.util.Set<String> matchedAliases = new java.util.HashSet<>(); 129 for (ScopeChild child : fromScope.getChildren()) { 130 String alias = child.getAlias(); 131 if (matcher.matches(alias, firstName) && !matchedAliases.contains(alias)) { 132 // Found matching table/subquery - only add once per alias 133 matchedAliases.add(alias); 134 INamespace namespace = child.getNamespace(); 135 List<String> remaining = names.subList(1, names.size()); 136 resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining); 137 foundLocally = true; 138 } 139 } 140 } else { 141 // Single name - could be a table alias or unqualified column 142 String name = names.get(0); 143 144 // First check if it's a table alias 145 for (ScopeChild child : fromScope.getChildren()) { 146 if (matcher.matches(child.getAlias(), name)) { 147 // It's a table alias - resolve as table 148 resolved.found(child.getNamespace(), child.isNullable(), 149 this, new ResolvePath(), Collections.emptyList()); 150 foundLocally = true; 151 } 152 } 153 154 // If not a table alias, try to resolve as unqualified column 155 if (!foundLocally) { 156 // First, determine if we have a "sole implicit derived table" scenario 157 // According to teradata_implicit_derived_tables_zh.md: 158 // "当前作用域只可见 1 张表:未限定列默认归属该表" 159 // If there are NO explicit tables AND exactly 1 implicit derived table, 160 // unqualified columns should be linked to that implicit derived table 161 List<INamespace> explicitTables = new ArrayList<>(); 162 List<INamespace> implicitDerivedTables = new ArrayList<>(); 163 164 for (ScopeChild child : fromScope.getChildren()) { 165 INamespace ns = child.getNamespace(); 166 TTable table = ns.getFinalTable(); 167 if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) { 168 implicitDerivedTables.add(ns); 169 } else { 170 explicitTables.add(ns); 171 } 172 } 173 174 // If no explicit tables and exactly 1 implicit derived table, 175 // include it for unqualified column resolution 176 boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() && 177 implicitDerivedTables.size() == 1; 178 179 // Search through all visible namespaces. 180 // 181 // Mirrors UpdateScope.resolve: physical tables without 182 // metadata (no catalog supplied) report ColumnLevel.MAYBE 183 // but do NOT support dynamic inference. They land in 184 // {@code inferredCandidates} so a single-target DELETE 185 // without a catalog still resolves its WHERE columns to 186 // the lone visible base table (EXACT_MATCH downstream). 187 // Without this branch, the slice-77 / slice-81 path that 188 // collects column refs via NameBindingProvider.bindColumn 189 // fails with NOT_FOUND for every DELETE WHERE column 190 // when no catalog is supplied. 191 List<INamespace> candidates = new ArrayList<>(); 192 List<INamespace> maybeCandidates = new ArrayList<>(); 193 List<INamespace> inferredCandidates = new ArrayList<>(); 194 195 for (ScopeChild child : fromScope.getChildren()) { 196 INamespace ns = child.getNamespace(); 197 198 // Skip implicit lateral derived tables (Teradata-specific) 199 // They should only match qualified references like "employee.employee_id" 200 // UNLESS this is a "sole implicit derived table" scenario where there 201 // are no explicit tables and exactly 1 implicit derived table 202 TTable table = ns.getFinalTable(); 203 if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) { 204 if (!useSoleImplicitDerivedTable) { 205 continue; 206 } 207 } 208 209 ColumnLevel level = ns.hasColumn(name); 210 211 if (level == ColumnLevel.EXISTS) { 212 candidates.add(ns); 213 } else if (level == ColumnLevel.MAYBE) { 214 // MAYBE means the column might exist (no metadata to confirm). 215 // For subqueries/CTEs with star columns add to maybeCandidates; 216 // for physical tables without metadata add to inferredCandidates 217 // so we can detect ambiguity when multiple tables could have 218 // the column AND resolve unambiguously when only one could. 219 if (ns.supportsDynamicInference()) { 220 maybeCandidates.add(ns); 221 } else { 222 inferredCandidates.add(ns); 223 } 224 } 225 } 226 227 // If we found exact matches, use them 228 if (!candidates.isEmpty()) { 229 for (INamespace ns : candidates) { 230 resolved.found(ns, false, this, new ResolvePath(), names); 231 } 232 foundLocally = true; 233 } 234 // If we found MAYBE matches (star columns), only use if exactly one 235 // Multiple MAYBE candidates means ambiguous - don't resolve 236 else if (maybeCandidates.size() == 1) { 237 resolved.found(maybeCandidates.get(0), false, this, new ResolvePath(), names); 238 foundLocally = true; 239 } 240 // If multiple tables without metadata, report all for ambiguity detection. 241 // This allows NameResolver to detect that the column is ambiguous 242 // and populate candidateTables with all potential sources. 243 else if (inferredCandidates.size() > 1) { 244 for (INamespace ns : inferredCandidates) { 245 resolved.found(ns, false, this, new ResolvePath(), names); 246 } 247 foundLocally = true; 248 } 249 // Single inferred candidate with no maybeCandidates: resolve to it. 250 // For single-target DELETE without a catalog this is the typical 251 // case — the lone employees table becomes the EXACT_MATCH source. 252 else if (inferredCandidates.size() == 1 && maybeCandidates.isEmpty()) { 253 resolved.found(inferredCandidates.get(0), false, this, new ResolvePath(), names); 254 foundLocally = true; 255 } 256 // Multiple maybe candidates means ambiguous with star columns - don't resolve 257 } 258 } 259 } 260 261 // If not found locally, delegate to parent 262 if (!foundLocally) { 263 parent.resolve(names, matcher, deep, resolved); 264 } 265 } 266 267 @Override 268 public String toString() { 269 return String.format("DeleteScope(from=%s)", fromScope != null ? "present" : "null"); 270 } 271}