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 List<INamespace> candidates = new ArrayList<>(); 181 List<INamespace> maybeCandidates = new ArrayList<>(); 182 183 for (ScopeChild child : fromScope.getChildren()) { 184 INamespace ns = child.getNamespace(); 185 186 // Skip implicit lateral derived tables (Teradata-specific) 187 // They should only match qualified references like "employee.employee_id" 188 // UNLESS this is a "sole implicit derived table" scenario where there 189 // are no explicit tables and exactly 1 implicit derived table 190 TTable table = ns.getFinalTable(); 191 if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) { 192 if (!useSoleImplicitDerivedTable) { 193 continue; 194 } 195 } 196 197 ColumnLevel level = ns.hasColumn(name); 198 199 if (level == ColumnLevel.EXISTS) { 200 candidates.add(ns); 201 } else if (level == ColumnLevel.MAYBE) { 202 // Only include MAYBE matches for namespaces that support 203 // dynamic inference (subqueries/CTEs with star columns) 204 // Physical tables without metadata should NOT be candidates 205 if (ns.supportsDynamicInference()) { 206 maybeCandidates.add(ns); 207 } 208 } 209 } 210 211 // If we found exact matches, use them 212 if (!candidates.isEmpty()) { 213 for (INamespace ns : candidates) { 214 resolved.found(ns, false, this, new ResolvePath(), names); 215 } 216 foundLocally = true; 217 } 218 // If we found MAYBE matches (star columns), only use if exactly one 219 // Multiple MAYBE candidates means ambiguous - don't resolve 220 else if (maybeCandidates.size() == 1) { 221 resolved.found(maybeCandidates.get(0), false, this, new ResolvePath(), names); 222 foundLocally = true; 223 } 224 // If multiple MAYBE candidates, leave as unresolved (ambiguous) 225 } 226 } 227 } 228 229 // If not found locally, delegate to parent 230 if (!foundLocally) { 231 parent.resolve(names, matcher, deep, resolved); 232 } 233 } 234 235 @Override 236 public String toString() { 237 return String.format("DeleteScope(from=%s)", fromScope != null ? "present" : "null"); 238 } 239}