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; 012 013import java.util.ArrayList; 014import java.util.Collections; 015import java.util.List; 016 017/** 018 * Scope for SELECT statement. 019 * This is typically the root scope for a query. 020 * 021 * Contains: 022 * - FROM clause scope (child) 023 * - WHERE clause scope (child) 024 * - SELECT list namespace (for ORDER BY to reference) 025 */ 026public class SelectScope extends AbstractScope { 027 028 /** FROM clause scope */ 029 private FromScope fromScope; 030 031 /** WHERE clause scope */ 032 private IScope whereScope; 033 034 /** Namespace representing the SELECT list output */ 035 private INamespace selectListNamespace; 036 037 public SelectScope(IScope parent, TParseTreeNode node) { 038 super(parent, node, ScopeType.SELECT); 039 } 040 041 public void setFromScope(FromScope fromScope) { 042 this.fromScope = fromScope; 043 } 044 045 public FromScope getFromScope() { 046 return fromScope; 047 } 048 049 public void setWhereScope(IScope whereScope) { 050 this.whereScope = whereScope; 051 } 052 053 public IScope getWhereScope() { 054 return whereScope; 055 } 056 057 public void setSelectListNamespace(INamespace namespace) { 058 this.selectListNamespace = namespace; 059 } 060 061 public INamespace getSelectListNamespace() { 062 return selectListNamespace; 063 } 064 065 @Override 066 public INamespace resolveTable(String tableName) { 067 // First try FROM scope 068 if (fromScope != null) { 069 INamespace ns = fromScope.resolveTable(tableName); 070 if (ns != null) { 071 return ns; 072 } 073 } 074 075 // Then delegate to parent 076 return super.resolveTable(tableName); 077 } 078 079 @Override 080 public List<INamespace> getVisibleNamespaces() { 081 List<INamespace> visible = new ArrayList<>(); 082 083 // Add FROM scope's namespaces 084 if (fromScope != null) { 085 visible.addAll(fromScope.getVisibleNamespaces()); 086 } 087 088 // Add parent's namespaces 089 visible.addAll(parent.getVisibleNamespaces()); 090 091 return visible; 092 } 093 094 /** 095 * Resolve a name within this SELECT scope. 096 * 097 * Resolution strategy: 098 * 1. For qualified names (t.col): find table t in FROM scope, then resolve col 099 * 2. For unqualified names (col): search all visible namespaces in FROM scope 100 * 3. Delegate to parent for names not found locally 101 */ 102 @Override 103 public void resolve(List<String> names, 104 INameMatcher matcher, 105 boolean deep, 106 IResolved resolved) { 107 if (names.isEmpty()) { 108 return; 109 } 110 111 boolean foundLocally = false; 112 113 // First, try to resolve via FROM scope 114 if (fromScope != null) { 115 if (names.size() >= 2) { 116 // Qualified name like "t.col" - let FROM scope handle it 117 // But don't let FROM scope delegate back to parent (we'll do that) 118 String firstName = names.get(0); 119 120 // Check if first name matches any child in FROM scope 121 for (ScopeChild child : fromScope.getChildren()) { 122 if (matcher.matches(child.getAlias(), firstName)) { 123 // Found matching table/subquery 124 INamespace namespace = child.getNamespace(); 125 List<String> remaining = names.subList(1, names.size()); 126 resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining); 127 foundLocally = true; 128 } 129 } 130 } else { 131 // Single name - could be a table alias or unqualified column 132 String name = names.get(0); 133 134 // First check if it's a table alias 135 for (ScopeChild child : fromScope.getChildren()) { 136 if (matcher.matches(child.getAlias(), name)) { 137 // It's a table alias - resolve as table 138 resolved.found(child.getNamespace(), child.isNullable(), 139 this, new ResolvePath(), Collections.emptyList()); 140 foundLocally = true; 141 } 142 } 143 144 // If not a table alias, try to resolve as unqualified column 145 if (!foundLocally) { 146 // First, determine if we have a "sole implicit derived table" scenario 147 // According to teradata_implicit_derived_tables_zh.md: 148 // "当前作用域只可见 1 张表:未限定列默认归属该表" 149 // If there are NO explicit tables AND exactly 1 implicit derived table, 150 // unqualified columns should be linked to that implicit derived table 151 List<INamespace> explicitTables = new ArrayList<>(); 152 List<INamespace> implicitDerivedTables = new ArrayList<>(); 153 154 for (ScopeChild child : fromScope.getChildren()) { 155 INamespace ns = child.getNamespace(); 156 TTable table = ns.getFinalTable(); 157 if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) { 158 implicitDerivedTables.add(ns); 159 } else { 160 explicitTables.add(ns); 161 } 162 } 163 164 // If no explicit tables and exactly 1 implicit derived table, 165 // include it for unqualified column resolution 166 boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() && 167 implicitDerivedTables.size() == 1; 168 169 // Search through all visible namespaces 170 List<INamespace> candidates = new ArrayList<>(); 171 List<INamespace> maybeCandidates = 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 // Star column or table without metadata - might have this column 193 maybeCandidates.add(ns); 194 } 195 } 196 197 // If we found exact matches, use them 198 if (!candidates.isEmpty()) { 199 for (INamespace ns : candidates) { 200 resolved.found(ns, false, this, new ResolvePath(), names); 201 } 202 foundLocally = true; 203 } 204 // If we found MAYBE matches (star columns), prioritize subqueries that can 205 // trace columns through qualified references over tables with just inferred columns 206 else if (!maybeCandidates.isEmpty()) { 207 // Separate subqueries that can trace the column from tables that can't 208 List<INamespace> tracedCandidates = new ArrayList<>(); 209 List<INamespace> inferredCandidates = new ArrayList<>(); 210 211 for (INamespace ns : maybeCandidates) { 212 if (ns instanceof gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) { 213 gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace subNs = 214 (gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) ns; 215 // Check if this subquery can trace the column through qualified refs 216 // A subquery with SELECT * and JOIN condition can trace columns 217 if (subNs.hasStarColumn()) { 218 // Try to resolve the column - if it returns a source with specific table, prefer it 219 gudusoft.gsqlparser.resolver2.model.ColumnSource source = subNs.resolveColumn(name); 220 if (source != null) { 221 // Check if the source has a specific table (unique resolution) 222 // or has evidence of qualified/traced resolution 223 if (source.getFinalTable() != null || 224 (source.getEvidence() != null && 225 (source.getEvidence().contains("traced") || 226 source.getEvidence().contains("qualified") || 227 source.getConfidence() >= 0.9))) { 228 tracedCandidates.add(ns); 229 } else if (!subNs.hasAmbiguousStar()) { 230 // Only add to inferred if NOT ambiguous star 231 // Ambiguous star (SELECT * from multiple tables) means column is truly ambiguous 232 inferredCandidates.add(ns); 233 } 234 // If hasAmbiguousStar() and no traced resolution, don't add - will be "missed" 235 } else if (!subNs.hasAmbiguousStar()) { 236 // Only add to inferred if NOT ambiguous star 237 inferredCandidates.add(ns); 238 } 239 // If hasAmbiguousStar() and source is null, don't add - will be "missed" 240 } else { 241 tracedCandidates.add(ns); // Non-star subquery, use as-is 242 } 243 } else { 244 // TableNamespace without metadata - add as candidate 245 inferredCandidates.add(ns); 246 } 247 } 248 249 // Prefer traced candidates over inferred candidates 250 if (!tracedCandidates.isEmpty()) { 251 for (INamespace ns : tracedCandidates) { 252 resolved.found(ns, false, this, new ResolvePath(), names); 253 } 254 foundLocally = true; 255 } else if (!inferredCandidates.isEmpty()) { 256 // Report all inferred candidates - let NameResolver handle ambiguity 257 // This populates candidateTables so formatter can use linkOrphanColumnToFirstTable 258 for (INamespace ns : inferredCandidates) { 259 resolved.found(ns, false, this, new ResolvePath(), names); 260 } 261 foundLocally = true; 262 } 263 } 264 } 265 } 266 } 267 268 // If not found locally, delegate to parent (but NOT back through FROM scope) 269 if (!foundLocally) { 270 parent.resolve(names, matcher, deep, resolved); 271 } 272 } 273 274 @Override 275 public String toString() { 276 return String.format("SelectScope(from=%s)", fromScope != null ? "present" : "null"); 277 } 278}