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 /** 038 * One {@code JOIN ... USING} merge in this scope: the names it merged, the relations that took 039 * part, in JOIN ORDER, and the join type. 040 * 041 * <p>SQL exposes a merged column ONCE, so an unqualified reference to it is not ambiguous 042 * across the sides that merged it. All three fields are needed to say that safely: 043 * <ul> 044 * <li>the NAMES, because only those columns merge; 045 * <li>the PARTICIPANTS, because a third relation in the same scope carrying the same name is 046 * still a genuine ambiguity — {@code SELECT c FROM a JOIN b USING(c), d} has three 047 * candidates and only two of them merged; 048 * <li>the ORDER and the join TYPE, because which side the merged value comes from depends on 049 * them: the left for INNER and LEFT, the right for RIGHT, and both for FULL. 050 * </ul> 051 */ 052 public static final class UsingMerge { 053 private final java.util.List<String> columnNames = new java.util.ArrayList<String>(); 054 private final java.util.List<gudusoft.gsqlparser.nodes.TTable> participants = 055 new java.util.ArrayList<gudusoft.gsqlparser.nodes.TTable>(); 056 private final gudusoft.gsqlparser.EJoinType joinType; 057 058 public UsingMerge(gudusoft.gsqlparser.EJoinType joinType) { 059 this.joinType = joinType; 060 } 061 062 public void addColumnName(String name) { 063 if (name != null && !name.isEmpty()) { 064 columnNames.add(name); 065 } 066 } 067 068 /** Participants are added LEFT side first, right side last, and the order is meaningful. */ 069 public void addParticipant(gudusoft.gsqlparser.nodes.TTable table) { 070 if (table != null && !containsIdentical(table)) { 071 participants.add(table); 072 } 073 } 074 075 private boolean containsIdentical(gudusoft.gsqlparser.nodes.TTable table) { 076 for (gudusoft.gsqlparser.nodes.TTable seen : participants) { 077 if (seen == table) { 078 return true; 079 } 080 } 081 return false; 082 } 083 084 /** Identifiers fold by the vendor's rules, so never compared with equals. */ 085 public boolean merges(gudusoft.gsqlparser.EDbVendor vendor, String columnName) { 086 return merges(gudusoft.gsqlparser.sqlenv.IdentifierService.defaultServiceFor(vendor), columnName); 087 } 088 089 /** {@link #merges(gudusoft.gsqlparser.EDbVendor, String)} under one analysis's identifier rules. */ 090 public boolean merges(gudusoft.gsqlparser.sqlenv.IdentifierService names, String columnName) { 091 if (columnName == null) { 092 return false; 093 } 094 for (String merged : columnNames) { 095 if (names.areEqual(merged, columnName, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType.dotColumn)) { 096 return true; 097 } 098 } 099 return false; 100 } 101 102 public boolean hasParticipant(gudusoft.gsqlparser.nodes.TTable table) { 103 return table != null && containsIdentical(table); 104 } 105 106 public java.util.List<gudusoft.gsqlparser.nodes.TTable> getParticipants() { 107 return java.util.Collections.unmodifiableList(participants); 108 } 109 110 public gudusoft.gsqlparser.EJoinType getJoinType() { 111 return joinType; 112 } 113 } 114 115 private final java.util.List<UsingMerge> usingMerges = new java.util.ArrayList<UsingMerge>(); 116 117 public SelectScope(IScope parent, TParseTreeNode node) { 118 super(parent, node, ScopeType.SELECT); 119 } 120 121 public void setFromScope(FromScope fromScope) { 122 this.fromScope = fromScope; 123 } 124 125 public FromScope getFromScope() { 126 return fromScope; 127 } 128 129 public void setWhereScope(IScope whereScope) { 130 this.whereScope = whereScope; 131 } 132 133 public IScope getWhereScope() { 134 return whereScope; 135 } 136 137 public void setSelectListNamespace(INamespace namespace) { 138 this.selectListNamespace = namespace; 139 } 140 141 public INamespace getSelectListNamespace() { 142 return selectListNamespace; 143 } 144 145 @Override 146 public INamespace resolveTable(String tableName) { 147 // First try FROM scope 148 if (fromScope != null) { 149 INamespace ns = fromScope.resolveTable(tableName); 150 if (ns != null) { 151 return ns; 152 } 153 } 154 155 // Then delegate to parent 156 return super.resolveTable(tableName); 157 } 158 159 @Override 160 public List<INamespace> getVisibleNamespaces() { 161 List<INamespace> visible = new ArrayList<>(); 162 163 // The FROM scope's own relations. The FROM scope's parent is this scope, so asking it for ITS visible 164 // namespaces would come straight back here and never terminate (Mantis 4820). 165 addOwnNamespaces(visible, fromScope); 166 167 // Add parent's namespaces 168 addAllDistinct(visible, getLookupParent().getVisibleNamespaces()); 169 170 return visible; 171 } 172 173 /** 174 * Resolve a name within this SELECT scope. 175 * 176 * Resolution strategy: 177 * 1. For qualified names (t.col): find table t in FROM scope, then resolve col 178 * 2. For unqualified names (col): search all visible namespaces in FROM scope 179 * 3. Delegate to parent for names not found locally 180 */ 181 @Override 182 public void resolve(List<String> names, 183 INameMatcher matcher, 184 boolean deep, 185 IResolved resolved) { 186 if (names.isEmpty()) { 187 return; 188 } 189 190 boolean foundLocally = false; 191 192 // First, try to resolve via FROM scope 193 if (fromScope != null) { 194 if (names.size() >= 2) { 195 // Qualified name like "t.col" - let FROM scope handle it 196 // But don't let FROM scope delegate back to parent (we'll do that) 197 String firstName = names.get(0); 198 199 // Check if first name matches any child in FROM scope 200 for (ScopeChild child : fromScope.getChildren()) { 201 if (matcher.matches(child.getAlias(), firstName)) { 202 // Found matching table/subquery 203 INamespace namespace = child.getNamespace(); 204 List<String> remaining = names.subList(1, names.size()); 205 resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining); 206 foundLocally = true; 207 } 208 } 209 } else { 210 // Single name - could be a table alias or unqualified column 211 String name = names.get(0); 212 213 // First check if it's a table alias 214 for (ScopeChild child : fromScope.getChildren()) { 215 if (matcher.matches(child.getAlias(), name)) { 216 // It's a table alias - resolve as table 217 resolved.found(child.getNamespace(), child.isNullable(), 218 this, new ResolvePath(), Collections.emptyList()); 219 foundLocally = true; 220 } 221 } 222 223 // If not a table alias, try to resolve as unqualified column 224 if (!foundLocally) { 225 // First, determine if we have a "sole implicit derived table" scenario 226 // According to teradata_implicit_derived_tables_zh.md: 227 // "当前作用域只可见 1 张表:未限定列默认归属该表" 228 // If there are NO explicit tables AND exactly 1 implicit derived table, 229 // unqualified columns should be linked to that implicit derived table 230 List<INamespace> explicitTables = new ArrayList<>(); 231 List<INamespace> implicitDerivedTables = new ArrayList<>(); 232 233 for (ScopeChild child : fromScope.getChildren()) { 234 INamespace ns = child.getNamespace(); 235 TTable table = ns.getFinalTable(); 236 if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) { 237 implicitDerivedTables.add(ns); 238 } else { 239 explicitTables.add(ns); 240 } 241 } 242 243 // If no explicit tables and exactly 1 implicit derived table, 244 // include it for unqualified column resolution 245 boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() && 246 implicitDerivedTables.size() == 1; 247 248 // Search through all visible namespaces 249 List<INamespace> candidates = new ArrayList<>(); 250 List<INamespace> maybeCandidates = new ArrayList<>(); 251 252 for (ScopeChild child : fromScope.getChildren()) { 253 INamespace ns = child.getNamespace(); 254 255 // Skip implicit lateral derived tables (Teradata-specific) 256 // They should only match qualified references like "employee.employee_id" 257 // UNLESS this is a "sole implicit derived table" scenario where there 258 // are no explicit tables and exactly 1 implicit derived table 259 TTable table = ns.getFinalTable(); 260 if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) { 261 if (!useSoleImplicitDerivedTable) { 262 continue; 263 } 264 } 265 266 ColumnLevel level = ns.hasColumn(name); 267 268 if (level == ColumnLevel.EXISTS) { 269 candidates.add(ns); 270 } else if (level == ColumnLevel.MAYBE) { 271 // Star column or table without metadata - might have this column 272 maybeCandidates.add(ns); 273 } 274 } 275 276 // If we found exact matches, use them 277 if (!candidates.isEmpty()) { 278 for (INamespace ns : candidates) { 279 resolved.found(ns, false, this, new ResolvePath(), names); 280 } 281 foundLocally = true; 282 } 283 // If we found MAYBE matches (star columns), prioritize subqueries that can 284 // trace columns through qualified references over tables with just inferred columns 285 else if (!maybeCandidates.isEmpty()) { 286 // Separate subqueries that can trace the column from tables that can't 287 List<INamespace> tracedCandidates = new ArrayList<>(); 288 List<INamespace> inferredCandidates = new ArrayList<>(); 289 // Mantis 4550: an ambiguous-star subquery (SELECT * over a 290 // multi-table FROM) cannot trace an unqualified column to a 291 // single base table, so it was historically dropped here 292 // ("will be missed"). But at THIS (outer) scope the subquery 293 // as a whole is a legitimate source for the column, so when 294 // there is at least one OTHER candidate the column is genuinely 295 // ambiguous across the derived tables. Collect these separately 296 // and only report them when doing so yields >= 2 candidates, 297 // preserving the "missed" behavior for a lone ambiguous star. 298 List<INamespace> ambiguousStarMaybeCandidates = new ArrayList<>(); 299 300 for (INamespace ns : maybeCandidates) { 301 if (ns instanceof gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) { 302 gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace subNs = 303 (gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) ns; 304 // Check if this subquery can trace the column through qualified refs 305 // A subquery with SELECT * and JOIN condition can trace columns 306 if (subNs.hasStarColumn()) { 307 // Try to resolve the column - if it returns a source with specific table, prefer it 308 gudusoft.gsqlparser.resolver2.model.ColumnSource source = subNs.resolveColumn(name); 309 if (source != null) { 310 // Check if the source has a specific table (unique resolution) 311 // or has evidence of qualified/traced resolution 312 if (source.getFinalTable() != null || 313 (source.getEvidence() != null && 314 (source.getEvidence().contains("traced") || 315 source.getEvidence().contains("qualified") || 316 source.getConfidence() >= 0.9))) { 317 tracedCandidates.add(ns); 318 } else if (!subNs.hasAmbiguousStar()) { 319 // Only add to inferred if NOT ambiguous star 320 // Ambiguous star (SELECT * from multiple tables) means column is truly ambiguous 321 inferredCandidates.add(ns); 322 } else { 323 // Ambiguous star, no traced resolution: outer-scope candidate 324 ambiguousStarMaybeCandidates.add(ns); 325 } 326 } else if (!subNs.hasAmbiguousStar()) { 327 // Only add to inferred if NOT ambiguous star 328 inferredCandidates.add(ns); 329 } else { 330 // Ambiguous star, column not traceable: outer-scope candidate 331 ambiguousStarMaybeCandidates.add(ns); 332 } 333 } else { 334 tracedCandidates.add(ns); // Non-star subquery, use as-is 335 } 336 } else { 337 // TableNamespace without metadata - add as candidate 338 inferredCandidates.add(ns); 339 } 340 } 341 342 // Prefer traced candidates over inferred candidates. In either 343 // case, also surface ambiguous-star siblings when including them 344 // produces a genuinely ambiguous (>= 2 candidate) result, so the 345 // outer column is reported as ambiguous across all derived tables 346 // rather than collapsing onto the single traceable one. 347 List<INamespace> primary = !tracedCandidates.isEmpty() ? tracedCandidates 348 : (!inferredCandidates.isEmpty() ? inferredCandidates : null); 349 int totalWithAmbStar = (primary != null ? primary.size() : 0) 350 + ambiguousStarMaybeCandidates.size(); 351 boolean includeAmbStar = !ambiguousStarMaybeCandidates.isEmpty() 352 && totalWithAmbStar >= 2; 353 if (primary != null) { 354 for (INamespace ns : primary) { 355 resolved.found(ns, false, this, new ResolvePath(), names); 356 } 357 if (includeAmbStar) { 358 for (INamespace ns : ambiguousStarMaybeCandidates) { 359 resolved.found(ns, false, this, new ResolvePath(), names); 360 } 361 } 362 foundLocally = true; 363 } else if (includeAmbStar) { 364 for (INamespace ns : ambiguousStarMaybeCandidates) { 365 resolved.found(ns, false, this, new ResolvePath(), names); 366 } 367 foundLocally = true; 368 } 369 } 370 } 371 } 372 } 373 374 // If not found locally, delegate to parent (but NOT back through FROM scope) 375 if (!foundLocally) { 376 getLookupParent().resolve(names, matcher, deep, resolved); 377 } 378 } 379 380 @Override 381 public String toString() { 382 return String.format("SelectScope(from=%s)", fromScope != null ? "present" : "null"); 383 } 384 385 /** Records a JOIN USING merge of this scope. */ 386 public void addUsingMerge(UsingMerge merge) { 387 if (merge != null) { 388 usingMerges.add(merge); 389 } 390 } 391 392 /** 393 * The merge that covers this column name and EVERY one of the given relations, or null. 394 * 395 * <p>Requiring the merge to cover every candidate is what keeps a third relation carrying the 396 * same name from being swallowed: those candidates are not merged with anything and the 397 * reference really is ambiguous. 398 */ 399 public UsingMerge findCoveringMerge(gudusoft.gsqlparser.EDbVendor vendor, String columnName, 400 java.util.List<gudusoft.gsqlparser.nodes.TTable> candidateTables) { 401 return findCoveringMerge(gudusoft.gsqlparser.sqlenv.IdentifierService.defaultServiceFor(vendor), 402 columnName, candidateTables); 403 } 404 405 /** 406 * {@link #findCoveringMerge(gudusoft.gsqlparser.EDbVendor, String, java.util.List)} with the 407 * column names compared under one analysis's identifier rules (a configured profile). 408 */ 409 public UsingMerge findCoveringMerge(gudusoft.gsqlparser.sqlenv.IdentifierService names, String columnName, 410 java.util.List<gudusoft.gsqlparser.nodes.TTable> candidateTables) { 411 if (candidateTables == null || candidateTables.isEmpty()) { 412 return null; 413 } 414 for (UsingMerge merge : usingMerges) { 415 if (!merge.merges(names, columnName)) { 416 continue; 417 } 418 boolean coversAll = true; 419 for (gudusoft.gsqlparser.nodes.TTable table : candidateTables) { 420 if (!merge.hasParticipant(table)) { 421 coversAll = false; 422 break; 423 } 424 } 425 if (coversAll) { 426 return merge; 427 } 428 } 429 return null; 430 } 431 432}