001package gudusoft.gsqlparser.resolver2.namespace; 002 003import gudusoft.gsqlparser.EExpressionType; 004import gudusoft.gsqlparser.nodes.TResultColumn; 005import gudusoft.gsqlparser.nodes.TResultColumnList; 006import gudusoft.gsqlparser.nodes.TTable; 007import gudusoft.gsqlparser.resolver2.ColumnLevel; 008import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 009import gudusoft.gsqlparser.resolver2.model.ColumnSource; 010import gudusoft.gsqlparser.sqlenv.TSQLEnv; 011import gudusoft.gsqlparser.stmt.TSelectSqlStatement; 012 013import java.util.ArrayDeque; 014import java.util.ArrayList; 015import java.util.Collections; 016import java.util.Deque; 017import java.util.HashSet; 018import java.util.LinkedHashMap; 019import java.util.List; 020import java.util.Map; 021import java.util.Set; 022 023/** 024 * Namespace representing a subquery. 025 * Provides columns from the subquery's SELECT list. 026 * 027 * Example: 028 * FROM (SELECT id, name FROM users) AS t 029 * ^^^^^^^^^^^^^^^^^^^^^^^^ 030 * SubqueryNamespace exposes columns: id, name 031 */ 032public class SubqueryNamespace extends AbstractNamespace { 033 034 private final TSelectSqlStatement subquery; 035 private final String alias; 036 037 /** Inferred columns from star push-down */ 038 private Map<String, ColumnSource> inferredColumns; 039 040 /** Track inferred column names for getInferredColumns() */ 041 private Set<String> inferredColumnNames; 042 043 /** Traced columns from qualified references (e.g., table1.x in ON conditions) */ 044 private Map<String, TTable> tracedColumnTables; 045 046 /** Whether this namespace was created from a TABLE function */ 047 private final boolean fromTableFunction; 048 049 /** 050 * Cache for nested SubqueryNamespace objects to avoid repeated creation. 051 * Key: TSelectSqlStatement, Value: validated SubqueryNamespace 052 */ 053 private Map<TSelectSqlStatement, SubqueryNamespace> nestedSubqueryCache; 054 055 /** 056 * Cache for nested TableNamespace objects to avoid repeated creation. 057 * Key: TTable, Value: validated TableNamespace 058 */ 059 private Map<TTable, TableNamespace> nestedTableCache; 060 061 /** The TTable that wraps this subquery (for legacy sync) */ 062 private TTable sourceTable; 063 064 /** TSQLEnv for looking up table metadata during star column resolution */ 065 private TSQLEnv sqlEnv; 066 067 public SubqueryNamespace(TSelectSqlStatement subquery, 068 String alias, 069 INameMatcher nameMatcher) { 070 this(subquery, alias, nameMatcher, false); 071 } 072 073 public SubqueryNamespace(TSelectSqlStatement subquery, 074 String alias, 075 INameMatcher nameMatcher, 076 boolean fromTableFunction) { 077 super(subquery, nameMatcher); 078 this.subquery = subquery; 079 this.alias = alias; 080 this.fromTableFunction = fromTableFunction; 081 } 082 083 public SubqueryNamespace(TSelectSqlStatement subquery, String alias) { 084 super(subquery); 085 this.subquery = subquery; 086 this.alias = alias; 087 this.fromTableFunction = false; 088 } 089 090 /** 091 * Gets or creates a cached nested SubqueryNamespace that inherits the guessColumnStrategy from this namespace. 092 * This ensures config-based isolation propagates through nested resolution. 093 * The namespace is cached to avoid repeated creation and validation for the same subquery. 094 * Also propagates sqlEnv for metadata lookup in star column resolution. 095 * 096 * @param subquery The subquery statement 097 * @param alias The alias for the subquery (not used as cache key, only for display) 098 * @return A validated SubqueryNamespace (cached or newly created) 099 */ 100 private SubqueryNamespace getOrCreateNestedNamespace(TSelectSqlStatement subquery, String alias) { 101 if (subquery == null) { 102 return null; 103 } 104 // Initialize cache lazily 105 if (nestedSubqueryCache == null) { 106 nestedSubqueryCache = new java.util.HashMap<>(); 107 } 108 // Check cache first 109 SubqueryNamespace cached = nestedSubqueryCache.get(subquery); 110 if (cached != null) { 111 return cached; 112 } 113 // Create new namespace 114 SubqueryNamespace nested = new SubqueryNamespace(subquery, alias, nameMatcher); 115 // Propagate guessColumnStrategy for config-based isolation 116 if (this.guessColumnStrategy >= 0) { 117 nested.setGuessColumnStrategy(this.guessColumnStrategy); 118 } 119 // Propagate sqlEnv for metadata lookup during star column resolution 120 if (this.sqlEnv != null) { 121 nested.setSqlEnv(this.sqlEnv); 122 } 123 // Validate and cache 124 nested.validate(); 125 nestedSubqueryCache.put(subquery, nested); 126 return nested; 127 } 128 129 /** 130 * Gets or creates a cached TableNamespace. 131 * The namespace is cached to avoid repeated creation and validation for the same table. 132 * Passes sqlEnv to enable metadata lookup for star column resolution. 133 * 134 * @param table The table 135 * @return A validated TableNamespace (cached or newly created) 136 */ 137 private TableNamespace getOrCreateTableNamespace(TTable table) { 138 if (table == null) { 139 return null; 140 } 141 // Initialize cache lazily 142 if (nestedTableCache == null) { 143 nestedTableCache = new java.util.HashMap<>(); 144 } 145 // Check cache first 146 TableNamespace cached = nestedTableCache.get(table); 147 if (cached != null) { 148 return cached; 149 } 150 // Create new namespace with sqlEnv for metadata lookup 151 TableNamespace tableNs = new TableNamespace(table, nameMatcher, sqlEnv); 152 // Validate and cache 153 tableNs.validate(); 154 nestedTableCache.put(table, tableNs); 155 return tableNs; 156 } 157 158 /** 159 * Creates a nested SubqueryNamespace that inherits the guessColumnStrategy from this namespace. 160 * This method is deprecated - use getOrCreateNestedNamespace for caching support. 161 * 162 * @param subquery The subquery statement 163 * @param alias The alias for the subquery 164 * @return A new SubqueryNamespace with inherited guessColumnStrategy and sqlEnv 165 * @deprecated Use getOrCreateNestedNamespace instead 166 */ 167 @Deprecated 168 private SubqueryNamespace createNestedNamespace(TSelectSqlStatement subquery, String alias) { 169 SubqueryNamespace nested = new SubqueryNamespace(subquery, alias, nameMatcher); 170 // Propagate guessColumnStrategy for config-based isolation 171 if (this.guessColumnStrategy >= 0) { 172 nested.setGuessColumnStrategy(this.guessColumnStrategy); 173 } 174 // Propagate sqlEnv for metadata lookup 175 if (this.sqlEnv != null) { 176 nested.setSqlEnv(this.sqlEnv); 177 } 178 return nested; 179 } 180 181 /** 182 * Returns true if this namespace was created from a TABLE function's subquery. 183 */ 184 public boolean isFromTableFunction() { 185 return fromTableFunction; 186 } 187 188 /** 189 * Set the TTable that wraps this subquery. 190 * Used by ScopeBuilder for legacy sync support. 191 * 192 * @param sourceTable the TTable that contains this subquery 193 */ 194 public void setSourceTable(TTable sourceTable) { 195 this.sourceTable = sourceTable; 196 } 197 198 @Override 199 public TTable getSourceTable() { 200 return sourceTable; 201 } 202 203 /** 204 * Set the TSQLEnv for metadata lookup. 205 * Used when resolving star columns to find which columns exist in underlying tables. 206 * 207 * @param sqlEnv the SQL environment containing table metadata 208 */ 209 public void setSqlEnv(TSQLEnv sqlEnv) { 210 this.sqlEnv = sqlEnv; 211 } 212 213 /** 214 * Get the TSQLEnv used for metadata lookup. 215 * 216 * @return the SQL environment, or null if not set 217 */ 218 public TSQLEnv getSqlEnv() { 219 return sqlEnv; 220 } 221 222 @Override 223 public String getDisplayName() { 224 return alias != null ? alias : "<subquery>"; 225 } 226 227 @Override 228 public TTable getFinalTable() { 229 // Try to find the underlying table from the FROM clause 230 // This works for both star subqueries (SELECT *) and explicit column subqueries 231 if (subquery != null && subquery.tables != null && subquery.tables.size() > 0) { 232 // First check if there's a qualified star column (e.g., ta.* or tb.*) 233 // If so, we should return the table matching that alias 234 TTable qualifiedStarTable = findTableFromQualifiedStar(); 235 if (qualifiedStarTable != null) { 236 return qualifiedStarTable; 237 } 238 239 // For single-table subqueries, return that table 240 // For multi-table (JOIN/comma) subqueries, only return if we can determine source 241 TTable firstTable = subquery.tables.getTable(0); 242 if (firstTable != null) { 243 // If it's a physical table, return it - BUT only if we can identify the source 244 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 245 // For multi-table subqueries without qualified star, we can't determine 246 // which table the column comes from. Don't blindly return first table. 247 if (subquery.tables.size() > 1 && qualifiedStarTable == null) { 248 return null; 249 } 250 // Check if it's a CTE reference - if so, trace through to physical table 251 if (firstTable.isCTEName() && firstTable.getCTE() != null) { 252 return traceTableThroughCTE(firstTable.getCTE()); 253 } 254 return firstTable; 255 } 256 // If it's a subquery, recursively trace (use cached namespace) 257 if (firstTable.getSubquery() != null) { 258 SubqueryNamespace nestedNs = getOrCreateNestedNamespace( 259 firstTable.getSubquery(), 260 firstTable.getAliasName() 261 ); 262 return nestedNs != null ? nestedNs.getFinalTable() : null; 263 } 264 // If it's a join, get the first base table from the join 265 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 266 return findFirstPhysicalTableFromJoin(firstTable); 267 } 268 } 269 } 270 return null; 271 } 272 273 /** 274 * Find the table referenced by a qualified star column (e.g., ta.* -> table_a). 275 * Returns null if there's no qualified star or if the star is unqualified (*). 276 */ 277 private TTable findTableFromQualifiedStar() { 278 if (subquery == null || subquery.getResultColumnList() == null) { 279 return null; 280 } 281 282 TResultColumnList selectList = subquery.getResultColumnList(); 283 for (int i = 0; i < selectList.size(); i++) { 284 TResultColumn resultCol = selectList.getResultColumn(i); 285 if (resultCol == null) continue; 286 287 String colStr = resultCol.toString().trim(); 288 // Check if it's a qualified star (contains . before *) 289 if (colStr.endsWith("*") && colStr.contains(".")) { 290 // Extract the table alias/name prefix (e.g., "ta" from "ta.*") 291 int dotIndex = colStr.lastIndexOf('.'); 292 if (dotIndex > 0) { 293 String tablePrefix = colStr.substring(0, dotIndex).trim(); 294 // Find the table with this alias or name 295 TTable matchingTable = findTableByAliasOrName(tablePrefix); 296 if (matchingTable != null) { 297 // Check for CTE reference first - trace through to physical table 298 if (matchingTable.isCTEName() && matchingTable.getCTE() != null) { 299 return traceTableThroughCTE(matchingTable.getCTE()); 300 } 301 // If the matching table is itself a subquery, trace through it (use cached namespace) 302 if (matchingTable.getSubquery() != null) { 303 SubqueryNamespace nestedNs = getOrCreateNestedNamespace( 304 matchingTable.getSubquery(), 305 matchingTable.getAliasName() 306 ); 307 return nestedNs != null ? nestedNs.getFinalTable() : null; 308 } 309 return matchingTable; 310 } 311 } 312 } 313 } 314 return null; 315 } 316 317 /** 318 * Find a table in the FROM clause by alias or table name. 319 * Also searches through JOIN expressions to find tables like PRD2_ODW.USI 320 * that are joined via LEFT OUTER JOIN. 321 */ 322 private TTable findTableByAliasOrName(String nameOrAlias) { 323 if (subquery == null || nameOrAlias == null) { 324 return null; 325 } 326 327 // Use getRelations() to get ALL tables including those in JOINs 328 java.util.ArrayList<TTable> relations = subquery.getRelations(); 329 if (relations != null) { 330 for (TTable table : relations) { 331 if (table == null) continue; 332 333 // For JOIN type tables, search inside the join expression 334 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 335 TTable found = findTableInJoinExpr(table.getJoinExpr(), nameOrAlias); 336 if (found != null) { 337 return found; 338 } 339 continue; 340 } 341 342 // Check alias first 343 String alias = table.getAliasName(); 344 if (alias != null && nameMatcher.matches(alias, nameOrAlias)) { 345 return table; 346 } 347 348 // Check table name (for physical tables) 349 if (table.getTableName() != null) { 350 String tableName = table.getTableName().toString(); 351 if (nameMatcher.matches(tableName, nameOrAlias)) { 352 return table; 353 } 354 // Also check just the table name part (without schema) 355 String shortName = table.getName(); 356 if (shortName != null && nameMatcher.matches(shortName, nameOrAlias)) { 357 return table; 358 } 359 } 360 } 361 } 362 363 // Fallback to subquery.tables if getRelations() didn't find it 364 if (subquery.tables != null) { 365 for (int i = 0; i < subquery.tables.size(); i++) { 366 TTable table = subquery.tables.getTable(i); 367 if (table == null) continue; 368 369 // Check alias first 370 String alias = table.getAliasName(); 371 if (alias != null && nameMatcher.matches(alias, nameOrAlias)) { 372 return table; 373 } 374 375 // Check table name 376 if (table.getTableName() != null) { 377 String tableName = table.getTableName().toString(); 378 if (nameMatcher.matches(tableName, nameOrAlias)) { 379 return table; 380 } 381 } 382 } 383 } 384 return null; 385 } 386 387 /** 388 * Find a table within a JOIN expression by alias or table name. 389 * Recursively searches both left and right sides of the join. 390 */ 391 private TTable findTableInJoinExpr(gudusoft.gsqlparser.nodes.TJoinExpr joinExpr, String nameOrAlias) { 392 if (joinExpr == null || nameOrAlias == null) { 393 return null; 394 } 395 396 // Check left table 397 TTable leftTable = joinExpr.getLeftTable(); 398 if (leftTable != null) { 399 TTable found = matchTableByAliasOrName(leftTable, nameOrAlias); 400 if (found != null) { 401 return found; 402 } 403 // Recursively check if left is also a join 404 if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join && leftTable.getJoinExpr() != null) { 405 found = findTableInJoinExpr(leftTable.getJoinExpr(), nameOrAlias); 406 if (found != null) { 407 return found; 408 } 409 } 410 } 411 412 // Check right table 413 TTable rightTable = joinExpr.getRightTable(); 414 if (rightTable != null) { 415 TTable found = matchTableByAliasOrName(rightTable, nameOrAlias); 416 if (found != null) { 417 return found; 418 } 419 // Recursively check if right is also a join 420 if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.join && rightTable.getJoinExpr() != null) { 421 found = findTableInJoinExpr(rightTable.getJoinExpr(), nameOrAlias); 422 if (found != null) { 423 return found; 424 } 425 } 426 } 427 428 return null; 429 } 430 431 /** 432 * Check if a table matches the given alias or name. 433 */ 434 private TTable matchTableByAliasOrName(TTable table, String nameOrAlias) { 435 if (table == null || nameOrAlias == null) { 436 return null; 437 } 438 439 // Check alias first 440 String alias = table.getAliasName(); 441 if (alias != null && nameMatcher.matches(alias, nameOrAlias)) { 442 return table; 443 } 444 445 // Check table name (for physical tables) 446 if (table.getTableName() != null) { 447 String tableName = table.getTableName().toString(); 448 if (nameMatcher.matches(tableName, nameOrAlias)) { 449 return table; 450 } 451 // Also check just the table name part (without schema) 452 // For "PRD2_ODW.USI", getName() returns "USI" 453 String shortName = table.getName(); 454 if (shortName != null && nameMatcher.matches(shortName, nameOrAlias)) { 455 return table; 456 } 457 } 458 459 return null; 460 } 461 462 /** 463 * Find the first physical table from a JOIN expression. 464 * Recursively traverses left side of joins. 465 */ 466 private TTable findFirstPhysicalTableFromJoin(TTable joinTable) { 467 if (joinTable == null) { 468 return null; 469 } 470 471 gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = joinTable.getJoinExpr(); 472 if (joinExpr != null && joinExpr.getLeftTable() != null) { 473 TTable leftTable = joinExpr.getLeftTable(); 474 475 if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 476 // Check if it's a CTE reference - trace through if so 477 if (leftTable.isCTEName() && leftTable.getCTE() != null) { 478 return traceTableThroughCTE(leftTable.getCTE()); 479 } 480 return leftTable; 481 } 482 if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 483 return findFirstPhysicalTableFromJoin(leftTable); 484 } 485 // If it's a subquery, trace through it (use cached namespace) 486 if (leftTable.getSubquery() != null) { 487 SubqueryNamespace nestedNs = getOrCreateNestedNamespace( 488 leftTable.getSubquery(), 489 leftTable.getAliasName() 490 ); 491 return nestedNs != null ? nestedNs.getFinalTable() : null; 492 } 493 } 494 return null; 495 } 496 497 /** 498 * Trace through a CTE to find its underlying physical table. 499 * This handles CTE chains like: CTE1 -> CTE2 -> CTE3 -> physical_table 500 */ 501 private TTable traceTableThroughCTE(gudusoft.gsqlparser.nodes.TCTE cteNode) { 502 if (cteNode == null || cteNode.getSubquery() == null) { 503 return null; 504 } 505 506 gudusoft.gsqlparser.stmt.TSelectSqlStatement cteSubquery = cteNode.getSubquery(); 507 508 // Handle UNION in the CTE 509 if (cteSubquery.isCombinedQuery()) { 510 // For UNION, trace the left branch 511 gudusoft.gsqlparser.stmt.TSelectSqlStatement leftStmt = cteSubquery.getLeftStmt(); 512 if (leftStmt != null && leftStmt.tables != null && leftStmt.tables.size() > 0) { 513 cteSubquery = leftStmt; 514 } 515 } 516 517 if (cteSubquery.tables == null || cteSubquery.tables.size() == 0) { 518 return null; 519 } 520 521 TTable firstTable = cteSubquery.tables.getTable(0); 522 if (firstTable == null) { 523 return null; 524 } 525 526 // If it's a physical table (not CTE), we found it 527 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !firstTable.isCTEName()) { 528 return firstTable; 529 } 530 531 // If it's another CTE reference, continue tracing 532 if (firstTable.isCTEName() && firstTable.getCTE() != null) { 533 return traceTableThroughCTE(firstTable.getCTE()); 534 } 535 536 // If it's a subquery, trace through it (use cached namespace) 537 if (firstTable.getSubquery() != null) { 538 SubqueryNamespace nestedNs = getOrCreateNestedNamespace( 539 firstTable.getSubquery(), 540 firstTable.getAliasName() 541 ); 542 return nestedNs != null ? nestedNs.getFinalTable() : null; 543 } 544 545 // If it's a join, get the first base table 546 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 547 return findFirstPhysicalTableFromJoin(firstTable); 548 } 549 550 return null; 551 } 552 553 @Override 554 public List<TTable> getAllFinalTables() { 555 // For non-UNION subqueries, return the single final table 556 // Multiple tables only make sense for UNION queries where a column 557 // can come from multiple branches (handled by UnionNamespace) 558 TTable finalTable = getFinalTable(); 559 if (finalTable != null) { 560 return Collections.singletonList(finalTable); 561 } 562 563 return Collections.emptyList(); 564 } 565 566 @Override 567 protected void doValidate() { 568 // Extract columns from SELECT list 569 columnSources = new LinkedHashMap<>(); 570 571 TResultColumnList selectList = subquery.getResultColumnList(); 572 if (selectList == null) { 573 return; 574 } 575 576 for (int i = 0; i < selectList.size(); i++) { 577 TResultColumn resultCol = selectList.getResultColumn(i); 578 579 // Determine column name 580 String colName = getColumnName(resultCol); 581 if (colName == null) { 582 // Unnamed expression, use ordinal 583 colName = "col_" + (i + 1); 584 } 585 586 // Find the source table for this column (if it's a qualified column reference) 587 TTable sourceTable = findSourceTableForResultColumn(resultCol); 588 589 // Create column source with override table if found 590 ColumnSource source; 591 if (sourceTable != null) { 592 source = new ColumnSource( 593 this, 594 colName, 595 resultCol, 596 1.0, // Definite - from SELECT list 597 "subquery_select_list_qualified", 598 sourceTable // Override table for proper tracing 599 ); 600 } else { 601 source = new ColumnSource( 602 this, 603 colName, 604 resultCol, 605 1.0, // Definite - from SELECT list 606 "subquery_select_list" 607 ); 608 } 609 610 columnSources.put(colName, source); 611 } 612 } 613 614 /** 615 * Find the source table for a result column's expression. 616 * For qualified column references like SUBS_CUST.FIRST_TP_ID, this returns 617 * the SUBS_CUST table from the FROM clause. 618 * For star columns like SUBSCR.*, this returns the SUBSCR table. 619 * 620 * <p>IMPORTANT: For subquery aliases, we return the subquery's TTable directly 621 * and do NOT trace through. The column should be attributed to the subquery 622 * alias (e.g., SUBS_CUST.FIRST_TP_ID), not to tables inside the subquery.</p> 623 * 624 * @param resultCol The result column to analyze 625 * @return The source table, or null if not determinable 626 */ 627 private TTable findSourceTableForResultColumn(TResultColumn resultCol) { 628 if (resultCol == null || resultCol.getExpr() == null) { 629 return null; 630 } 631 632 gudusoft.gsqlparser.nodes.TExpression expr = resultCol.getExpr(); 633 634 // Handle simple column reference (e.g., SUBS_CUST.FIRST_TP_ID or FIRST_TP_ID) 635 if (expr.getExpressionType() == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) { 636 gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand(); 637 if (objName != null) { 638 String tablePrefix = objName.getTableString(); 639 if (tablePrefix != null && !tablePrefix.isEmpty()) { 640 // Qualified reference - find the table 641 TTable table = findTableByAliasOrName(tablePrefix); 642 if (table != null) { 643 // Return the table directly - don't trace through subqueries 644 // This keeps columns attributed to their immediate source 645 // (e.g., SUBS_CUST.FIRST_TP_ID stays as SUBS_CUST.FIRST_TP_ID) 646 return table; 647 } 648 } 649 } 650 } 651 652 // Handle star column (e.g., SUBSCR.* or *) 653 String colStr = resultCol.toString().trim(); 654 if (colStr.endsWith("*") && colStr.contains(".")) { 655 // Qualified star - find the table 656 int dotIndex = colStr.lastIndexOf('.'); 657 if (dotIndex > 0) { 658 String tablePrefix = colStr.substring(0, dotIndex).trim(); 659 TTable table = findTableByAliasOrName(tablePrefix); 660 if (table != null) { 661 // For star columns, we DO trace through to get all underlying columns 662 // This is different from regular columns because star needs to expand (use cached namespace) 663 if (table.getSubquery() != null) { 664 SubqueryNamespace nestedNs = getOrCreateNestedNamespace( 665 table.getSubquery(), table.getAliasName()); 666 return nestedNs != null ? nestedNs.getFinalTable() : null; 667 } 668 return table; 669 } 670 } 671 } 672 673 return null; 674 } 675 676 /** 677 * Extract column name from TResultColumn. 678 * Handles aliases and expression columns. 679 */ 680 private String getColumnName(TResultColumn resultCol) { 681 // Check for alias 682 if (resultCol.getAliasClause() != null && 683 resultCol.getAliasClause().getAliasName() != null) { 684 return resultCol.getAliasClause().getAliasName().toString(); 685 } 686 687 // Check for simple column reference 688 if (resultCol.getExpr() != null) { 689 gudusoft.gsqlparser.nodes.TExpression expr = resultCol.getExpr(); 690 // Check if it's a simple object reference 691 if (expr.getExpressionType() == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) { 692 gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand(); 693 if (objName != null) { 694 return objName.getColumnNameOnly(); 695 } 696 } 697 } 698 699 // Complex expression - no name 700 return null; 701 } 702 703 @Override 704 public ColumnLevel hasColumn(String columnName) { 705 ensureValidated(); 706 707 // Check in SELECT list (explicit columns) — matcher-aware. 708 if (containsColumnByMatcher(columnSources, columnName)) { 709 return ColumnLevel.EXISTS; 710 } 711 712 // Check in inferred columns (from star push-down). The map is raw- 713 // keyed (= ColumnSource.exposedName); the matcher-aware helper 714 // applies per-dialect rules including SQL Server COLLATION_BASED. 715 if (containsColumnByMatcher(inferredColumns, columnName)) { 716 return ColumnLevel.EXISTS; 717 } 718 719 // If subquery has SELECT *, unknown columns MAYBE exist 720 // They need to be resolved through star push-down 721 if (hasStarColumn()) { 722 return ColumnLevel.MAYBE; 723 } 724 725 return ColumnLevel.NOT_EXISTS; 726 } 727 728 public TSelectSqlStatement getSubquery() { 729 return subquery; 730 } 731 732 @Override 733 public TSelectSqlStatement getSelectStatement() { 734 return subquery; 735 } 736 737 @Override 738 public boolean hasStarColumn() { 739 if (subquery == null || subquery.getResultColumnList() == null) { 740 return false; 741 } 742 743 TResultColumnList selectList = subquery.getResultColumnList(); 744 for (int i = 0; i < selectList.size(); i++) { 745 TResultColumn resultCol = selectList.getResultColumn(i); 746 if (isStarResultColumn(resultCol)) { 747 return true; 748 } 749 } 750 return false; 751 } 752 753 private static boolean isStarResultColumn(TResultColumn resultCol) { 754 if (resultCol == null) { 755 return false; 756 } 757 if (resultCol.getExceptColumnList() != null 758 || (resultCol.getReplaceExprAsIdentifiers() != null 759 && !resultCol.getReplaceExprAsIdentifiers().isEmpty()) 760 || (resultCol.getExprAsIdentifiers() != null 761 && !resultCol.getExprAsIdentifiers().isEmpty())) { 762 return true; 763 } 764 if (resultCol.getExpr() != null 765 && resultCol.getExpr().getExpressionType() == EExpressionType.simple_object_name_t 766 && resultCol.getExpr().getObjectOperand() != null) { 767 String starText = resultCol.getExpr().getObjectOperand().toString(); 768 if (starText != null) { 769 starText = starText.trim(); 770 if ("*".equals(starText) || starText.endsWith(".*")) { 771 return true; 772 } 773 } 774 } 775 String text = resultCol.toString(); 776 if (text == null) { 777 return false; 778 } 779 text = text.trim(); 780 return "*".equals(text) || text.endsWith(".*"); 781 } 782 783 /** 784 * Slice S4 (plan §5.5): a derived subquery's projection is authoritative 785 * once validated AND at least one named (non-star) column exists. A 786 * subquery whose only projection is {@code SELECT *} is reported as 787 * METADATA_UNAVAILABLE here — S10 refines this when the underlying tables' 788 * star expansion fully resolves into named columns. 789 */ 790 @Override 791 public MetadataState getMetadataState() { 792 ensureValidated(); 793 if (columnSources != null && !columnSources.isEmpty() && !hasStarColumn()) { 794 return MetadataState.FOUND; 795 } 796 return MetadataState.METADATA_UNAVAILABLE; 797 } 798 799 /** 800 * Binding-diagnostic view of the derived-table output schema. 801 * 802 * <p>This deliberately avoids the legacy resolution fallbacks in 803 * {@link #resolveColumn(String)}. Binding diagnostics need the SQL-visible 804 * derived projection: named SELECT-list columns and already inferred star 805 * output columns are visible; hidden base-table columns are not. When the 806 * projection includes a star and the requested column is not already known, 807 * the output is not fully authoritative, so callers must not report a 808 * missing-output error.</p> 809 */ 810 public ColumnLevel hasAuthoritativeOutputColumn(String columnName) { 811 ensureValidated(); 812 813 if (columnName == null || columnName.isEmpty()) { 814 return ColumnLevel.MAYBE; 815 } 816 817 if (containsColumnByMatcher(columnSources, columnName)) { 818 return ColumnLevel.EXISTS; 819 } 820 821 if (containsColumnByMatcher(inferredColumns, columnName)) { 822 return ColumnLevel.EXISTS; 823 } 824 825 if (hasStarColumn()) { 826 return ColumnLevel.MAYBE; 827 } 828 829 if (columnSources != null && !columnSources.isEmpty()) { 830 return ColumnLevel.NOT_EXISTS; 831 } 832 833 return ColumnLevel.MAYBE; 834 } 835 836 /** 837 * Get the first star column (TResultColumn) from this subquery's SELECT list. 838 * Used to track the definition node for columns inferred from the star. 839 * 840 * @return The star column, or null if no star column exists 841 */ 842 public TResultColumn getStarColumn() { 843 if (subquery == null || subquery.getResultColumnList() == null) { 844 return null; 845 } 846 847 TResultColumnList selectList = subquery.getResultColumnList(); 848 for (int i = 0; i < selectList.size(); i++) { 849 TResultColumn resultCol = selectList.getResultColumn(i); 850 if (isStarResultColumn(resultCol)) { 851 return resultCol; 852 } 853 } 854 return null; 855 } 856 857 /** 858 * Check if this subquery has an unqualified star with multiple tables. 859 * In this case, columns are ambiguous and should NOT be auto-resolved. 860 * 861 * Example: 862 * SELECT * FROM table_a, table_c -- ambiguous, columns could come from either table 863 * SELECT ta.* FROM table_a ta, table_c tc -- NOT ambiguous, star is qualified 864 * SELECT * FROM table_a JOIN table_c ON ... -- ambiguous, columns from both tables 865 * 866 * Uses getRelations() and TJoinExpr to properly count tables in JOINs. 867 */ 868 public boolean hasAmbiguousStar() { 869 if (subquery == null || subquery.getResultColumnList() == null) { 870 return false; 871 } 872 873 // Count all sources (tables, subqueries, joins) using getRelations() 874 int sourceCount = 0; 875 java.util.ArrayList<TTable> relations = subquery.getRelations(); 876 if (relations != null) { 877 for (TTable table : relations) { 878 if (table == null) continue; 879 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 880 // For JOIN type, count tables from the JoinExpr 881 List<TTable> joinTables = new ArrayList<>(); 882 collectPhysicalTablesFromJoinExpr(table.getJoinExpr(), joinTables); 883 sourceCount += joinTables.size(); 884 } else if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 885 sourceCount++; 886 } else if (table.getSubquery() != null) { 887 // Subqueries also count as sources 888 sourceCount++; 889 } 890 } 891 } 892 893 // If only one source, not ambiguous 894 if (sourceCount <= 1) { 895 return false; 896 } 897 898 // Check for unqualified star (just "*" without table prefix) 899 TResultColumnList selectList = subquery.getResultColumnList(); 900 for (int i = 0; i < selectList.size(); i++) { 901 TResultColumn resultCol = selectList.getResultColumn(i); 902 if (resultCol == null) continue; 903 904 String colStr = resultCol.toString().trim(); 905 // Check if it's an unqualified star (just "*", not "ta.*") 906 if (colStr.equals("*")) { 907 return true; 908 } 909 } 910 911 return false; 912 } 913 914 /** 915 * Count the number of "real" tables in the FROM clause, excluding implicit lateral 916 * derived tables (Teradata feature where references to undeclared tables in WHERE 917 * clause create implicit table references). 918 * 919 * @return The count of real tables (excluding implicit lateral derived tables) 920 */ 921 private int countRealTablesInFromClause() { 922 if (subquery == null || subquery.tables == null) { 923 return 0; 924 } 925 int count = 0; 926 for (int i = 0; i < subquery.tables.size(); i++) { 927 TTable table = subquery.tables.getTable(i); 928 if (table != null && 929 table.getEffectType() != gudusoft.gsqlparser.ETableEffectType.tetImplicitLateralDerivedTable) { 930 count++; 931 } 932 } 933 return count; 934 } 935 936 /** 937 * Get the single real table from the FROM clause when there's exactly one. 938 * This is used as a fallback for star column push-down when resolveColumnInFromScope() 939 * can't find the column (e.g., no SQLEnv metadata). 940 * 941 * @return The single real table, or null if there's not exactly one 942 */ 943 private TTable getSingleRealTableFromFromClause() { 944 if (subquery == null || subquery.tables == null) { 945 return null; 946 } 947 TTable result = null; 948 for (int i = 0; i < subquery.tables.size(); i++) { 949 TTable table = subquery.tables.getTable(i); 950 if (table != null && 951 table.getEffectType() != gudusoft.gsqlparser.ETableEffectType.tetImplicitLateralDerivedTable) { 952 if (result != null) { 953 // Multiple tables - can't determine which one 954 return null; 955 } 956 result = table; 957 } 958 } 959 // If the single table is itself a subquery, trace through it (use cached namespace) 960 if (result != null && result.getSubquery() != null) { 961 SubqueryNamespace nestedNs = getOrCreateNestedNamespace(result.getSubquery(), result.getAliasName()); 962 return nestedNs != null ? nestedNs.getFinalTable() : null; 963 } 964 965 // If the single table is a CTE reference, trace through the CTE to find the physical table 966 // This is critical for star column push-down when SELECT * FROM cte_name 967 if (result != null && result.isCTEName() && result.getCTE() != null) { 968 gudusoft.gsqlparser.nodes.TCTE cte = result.getCTE(); 969 if (cte.getSubquery() != null) { 970 // Create a temporary CTENamespace to trace through 971 CTENamespace cteNs = new CTENamespace( 972 cte, 973 cte.getTableName() != null ? cte.getTableName().toString() : result.getName(), 974 cte.getSubquery(), 975 nameMatcher 976 ); 977 cteNs.setReferencingTable(result); 978 cteNs.validate(); 979 TTable tracedTable = cteNs.getFinalTable(); 980 // Return the physical table if found, otherwise return the CTE table as fallback 981 return tracedTable != null ? tracedTable : result; 982 } 983 } 984 985 return result; 986 } 987 988 @Override 989 public boolean supportsDynamicInference() { 990 // Support dynamic inference if this subquery has SELECT * 991 return hasStarColumn(); 992 } 993 994 @Override 995 public boolean addInferredColumn(String columnName, double confidence, String evidence) { 996 if (columnName == null || columnName.isEmpty()) { 997 return false; 998 } 999 1000 // Initialize maps if needed 1001 if (inferredColumns == null) { 1002 inferredColumns = new LinkedHashMap<>(); 1003 } 1004 if (inferredColumnNames == null) { 1005 inferredColumnNames = new HashSet<>(); 1006 } 1007 1008 // Slice S1: dedupe through the matcher-aware helper so per-vendor 1009 // identifier rules govern whether two case-only-different inputs are 1010 // the same column. Without this, BigQuery / MySQL / SQL Server would 1011 // accept "MyCol" and "MYCOL" as two separate inferred entries and 1012 // downstream lookups would non-deterministically pick one of them. 1013 // Codex round 2: storage uses the raw (exposedName) key — two 1014 // matcher-distinct identifiers that happen to normalize equally 1015 // (Postgres "mycol" vs unquoted MYCOL) keep separate entries. 1016 if (containsColumnByMatcher(columnSources, columnName)) { 1017 return false; 1018 } 1019 if (containsColumnByMatcher(inferredColumns, columnName)) { 1020 return false; 1021 } 1022 1023 // For star columns (SELECT *), trace the column to its underlying table. 1024 // This is critical for Teradata UPDATE...FROM...SET syntax where columns 1025 // reference subqueries with SELECT *. 1026 // 1027 // IMPORTANT: Only trace when there's exactly ONE real table (excluding implicit 1028 // lateral derived tables). If there are multiple tables, the column source 1029 // is ambiguous unless the star is qualified (handled elsewhere). 1030 TTable overrideTable = null; 1031 if (hasStarColumn() && !hasAmbiguousStar()) { 1032 // Count real tables (excluding implicit lateral derived tables) 1033 int realTableCount = countRealTablesInFromClause(); 1034 if (realTableCount == 1) { 1035 ColumnSource fromSource = resolveColumnInFromScope(columnName); 1036 if (fromSource != null) { 1037 // Get the table from the underlying source 1038 overrideTable = fromSource.getFinalTable(); 1039 if (overrideTable == null && fromSource.getSourceNamespace() instanceof TableNamespace) { 1040 // Try to get table directly from TableNamespace 1041 overrideTable = ((TableNamespace) fromSource.getSourceNamespace()).getTable(); 1042 } 1043 } 1044 1045 // Fallback: If resolveColumnInFromScope couldn't find the column (no SQLEnv metadata), 1046 // but we have exactly one table with SELECT *, assume the column comes from that table. 1047 // This is the expected behavior for star column push-down in most real-world scenarios 1048 // where the column name is not provable but is inferred from the context. 1049 if (overrideTable == null) { 1050 overrideTable = getSingleRealTableFromFromClause(); 1051 } 1052 } 1053 } 1054 1055 // Create inferred column source with overrideTable if found 1056 // Note: Do NOT set the star column as definitionNode here. 1057 // The definitionNode is used by the formatter to attribute columns to tables. 1058 // Star-inferred columns should be attributed based on the overrideTable, not the star column. 1059 // The legacy sourceColumn for star-inferred columns should remain null since they don't 1060 // have a direct 1:1 relationship with a specific TResultColumn. 1061 ColumnSource source = new ColumnSource( 1062 this, 1063 columnName, 1064 null, // No definition node for inferred columns 1065 confidence, 1066 evidence, 1067 overrideTable 1068 ); 1069 1070 inferredColumns.put(columnName, source); 1071 inferredColumnNames.add(columnName); 1072 return true; 1073 } 1074 1075 @Override 1076 public Set<String> getInferredColumns() { 1077 if (inferredColumnNames == null) { 1078 return java.util.Collections.emptySet(); 1079 } 1080 return java.util.Collections.unmodifiableSet(inferredColumnNames); 1081 } 1082 1083 @Override 1084 public ColumnSource resolveColumn(String columnName) { 1085 ensureValidated(); 1086 1087 // First check explicit columns from this subquery's SELECT list 1088 ColumnSource source = super.resolveColumn(columnName); 1089 if (source != null) { 1090 return source; 1091 } 1092 1093 // Then check inferred columns. Slice S1 + codex round 2: the map is 1094 // raw-keyed (= ColumnSource.exposedName), so the exact-match probe 1095 // is O(1) for the same identifier queried again. Matcher loop walks 1096 // values via getExposedName() so quote state is preserved. 1097 if (inferredColumns != null) { 1098 ColumnSource exact = inferredColumns.get(columnName); 1099 if (exact != null) { 1100 return exact; 1101 } 1102 for (ColumnSource entry : inferredColumns.values()) { 1103 String exposed = entry != null ? entry.getExposedName() : null; 1104 if (exposed != null && nameMatcher.matches(exposed, columnName)) { 1105 return entry; 1106 } 1107 } 1108 } 1109 1110 // For ambiguous star cases, try to find the column in explicit subquery sources 1111 // This handles cases like: SELECT * FROM table_a ta, (SELECT col1 FROM table_c) tc 1112 // where col1 can be uniquely traced to tc -> table_c 1113 if (hasAmbiguousStar()) { 1114 ColumnSource explicitSource = findColumnInExplicitSources(columnName); 1115 if (explicitSource != null) { 1116 return explicitSource; 1117 } 1118 1119 // Try to trace the column through qualified references in the FROM clause 1120 // This handles JOINs where ON condition uses qualified names like table1.x 1121 ColumnSource tracedSource = traceColumnThroughQualifiedReferences(columnName); 1122 if (tracedSource != null) { 1123 return tracedSource; 1124 } 1125 1126 // Try to resolve the column using sqlenv metadata from the FROM clause tables 1127 // This uses TableNamespace.resolveColumn() which has access to sqlenv 1128 ColumnSource fromScopeSource = resolveColumnInFromScope(columnName); 1129 if (fromScopeSource != null) { 1130 return fromScopeSource; 1131 } 1132 1133 // Column not found in any explicit source, and we have multiple physical tables 1134 // Apply GUESS_COLUMN_STRATEGY to pick a table or leave unresolved 1135 return applyGuessColumnStrategy(columnName); 1136 } 1137 1138 // If hasColumn returns MAYBE (we have SELECT *), auto-infer this column 1139 // This enables on-demand inference during resolution 1140 if (hasStarColumn()) { 1141 // Add as inferred column with moderate confidence 1142 // The confidence is lower because we're inferring from outer reference 1143 boolean added = addInferredColumn(columnName, 0.8, "auto_inferred_from_outer_reference"); 1144 if (added && inferredColumns != null) { 1145 ColumnSource inferredSource = inferredColumns.get(columnName); 1146 if (inferredSource != null) { 1147 return inferredSource; 1148 } 1149 } 1150 } 1151 1152 return null; 1153 } 1154 1155 /** 1156 * Resolve a column in the FROM scope (child namespaces). 1157 * This finds where a column originates from in the FROM clause tables/subqueries. 1158 * 1159 * <p>Unlike resolveColumn() which returns columns from THIS subquery's SELECT list, 1160 * this method looks into the FROM clause to find the underlying definition.</p> 1161 * 1162 * <p>IMPORTANT: For TableNamespace, this only returns columns that exist in 1163 * actual metadata (from DDL or SQLEnv). It does NOT use inferred columns. 1164 * This is critical for multi-table star resolution where we need to know 1165 * which table actually has the column based on metadata, not inference.</p> 1166 * 1167 * @param columnName The column name to find 1168 * @return ColumnSource from the FROM clause, or null if not found 1169 */ 1170 public ColumnSource resolveColumnInFromScope(String columnName) { 1171 if (subquery == null || subquery.tables == null || columnName == null) { 1172 return null; 1173 } 1174 1175 // Search through tables in the FROM clause 1176 for (int i = 0; i < subquery.tables.size(); i++) { 1177 TTable table = subquery.tables.getTable(i); 1178 if (table == null) continue; 1179 1180 // For subqueries, look in their column sources (use cached namespace) 1181 if (table.getSubquery() != null) { 1182 SubqueryNamespace nestedNs = getOrCreateNestedNamespace( 1183 table.getSubquery(), table.getAliasName()); 1184 if (nestedNs != null) { 1185 ColumnSource source = nestedNs.resolveColumn(columnName); 1186 if (source != null) { 1187 return source; 1188 } 1189 } 1190 } 1191 1192 // For physical tables, create a TableNamespace and resolve (use cached namespace) 1193 // IMPORTANT: Only return column if the table has actual metadata (DDL or SQLEnv) 1194 // This prevents inferring columns for ambiguous star resolution 1195 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1196 TableNamespace tableNs = getOrCreateTableNamespace(table); 1197 if (tableNs != null && tableNs.hasMetadata()) { 1198 ColumnSource source = tableNs.resolveColumn(columnName); 1199 if (source != null) { 1200 return source; 1201 } 1202 } 1203 } 1204 1205 // For joins, recursively search 1206 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1207 ColumnSource source = resolveColumnInJoin(table.getJoinExpr(), columnName); 1208 if (source != null) { 1209 return source; 1210 } 1211 } 1212 } 1213 1214 return null; 1215 } 1216 1217 /** 1218 * Resolve a column within a JOIN expression. 1219 * Only returns columns from tables with actual metadata (DDL or SQLEnv). 1220 */ 1221 private ColumnSource resolveColumnInJoin(gudusoft.gsqlparser.nodes.TJoinExpr joinExpr, String columnName) { 1222 if (joinExpr == null) return null; 1223 1224 // Check left table (use cached namespaces) 1225 TTable leftTable = joinExpr.getLeftTable(); 1226 if (leftTable != null) { 1227 if (leftTable.getSubquery() != null) { 1228 SubqueryNamespace nestedNs = getOrCreateNestedNamespace( 1229 leftTable.getSubquery(), leftTable.getAliasName()); 1230 if (nestedNs != null) { 1231 ColumnSource source = nestedNs.resolveColumn(columnName); 1232 if (source != null) { 1233 return source; 1234 } 1235 } 1236 } else if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1237 TableNamespace tableNs = getOrCreateTableNamespace(leftTable); 1238 // Only return if table has metadata - don't use inferred columns 1239 if (tableNs != null && tableNs.hasMetadata()) { 1240 ColumnSource source = tableNs.resolveColumn(columnName); 1241 if (source != null) { 1242 return source; 1243 } 1244 } 1245 } else if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1246 ColumnSource source = resolveColumnInJoin(leftTable.getJoinExpr(), columnName); 1247 if (source != null) { 1248 return source; 1249 } 1250 } 1251 } 1252 1253 // Check right table (use cached namespaces) 1254 TTable rightTable = joinExpr.getRightTable(); 1255 if (rightTable != null) { 1256 if (rightTable.getSubquery() != null) { 1257 SubqueryNamespace nestedNs = getOrCreateNestedNamespace( 1258 rightTable.getSubquery(), rightTable.getAliasName()); 1259 if (nestedNs != null) { 1260 ColumnSource source = nestedNs.resolveColumn(columnName); 1261 if (source != null) { 1262 return source; 1263 } 1264 } 1265 } else if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1266 TableNamespace tableNs = getOrCreateTableNamespace(rightTable); 1267 // Only return if table has metadata - don't use inferred columns 1268 if (tableNs != null && tableNs.hasMetadata()) { 1269 ColumnSource source = tableNs.resolveColumn(columnName); 1270 if (source != null) { 1271 return source; 1272 } 1273 } 1274 } else if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1275 ColumnSource source = resolveColumnInJoin(rightTable.getJoinExpr(), columnName); 1276 if (source != null) { 1277 return source; 1278 } 1279 } 1280 } 1281 1282 return null; 1283 } 1284 1285 /** 1286 * Apply GUESS_COLUMN_STRATEGY to pick a table for an ambiguous column. 1287 * Used when a star column could come from multiple tables. 1288 * 1289 * For NOT_PICKUP strategy, returns a ColumnSource with all candidate tables 1290 * stored so end users can access them via getCandidateTables(). 1291 * 1292 * @param columnName The column name to resolve 1293 * @return ColumnSource (may have multiple candidate tables for NOT_PICKUP) 1294 */ 1295 private ColumnSource applyGuessColumnStrategy(String columnName) { 1296 // Get the strategy from instance field (which falls back to TBaseType if not set) 1297 int strategy = getGuessColumnStrategy(); 1298 1299 // Collect all physical tables from the FROM clause 1300 List<TTable> physicalTables = new ArrayList<>(); 1301 java.util.ArrayList<TTable> relations = subquery.getRelations(); 1302 if (relations != null) { 1303 for (TTable table : relations) { 1304 if (table == null) continue; 1305 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1306 collectPhysicalTablesFromJoinExpr(table.getJoinExpr(), physicalTables); 1307 } else if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1308 physicalTables.add(table); 1309 } 1310 } 1311 } 1312 1313 if (physicalTables.isEmpty()) { 1314 return null; 1315 } 1316 1317 if (strategy == gudusoft.gsqlparser.TBaseType.GUESS_COLUMN_STRATEGY_NOT_PICKUP) { 1318 // Don't pick any table - return null to treat column as "missed" 1319 // This is the expected behavior for ambiguous star columns with NOT_PICKUP strategy 1320 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1321 System.out.println("[GUESS_COLUMN_STRATEGY] Column '" + columnName + 1322 "' is ambiguous across " + physicalTables.size() + " tables (NOT_PICKUP strategy - treating as missed)"); 1323 } 1324 return null; 1325 } 1326 1327 // Pick the table based on strategy 1328 TTable pickedTable; 1329 String evidence; 1330 if (strategy == gudusoft.gsqlparser.TBaseType.GUESS_COLUMN_STRATEGY_NEAREST) { 1331 // Pick the first table (nearest in FROM clause order) 1332 pickedTable = physicalTables.get(0); 1333 evidence = "guess_strategy_nearest"; 1334 } else if (strategy == gudusoft.gsqlparser.TBaseType.GUESS_COLUMN_STRATEGY_FARTHEST) { 1335 // Pick the last table (farthest in FROM clause order) 1336 pickedTable = physicalTables.get(physicalTables.size() - 1); 1337 evidence = "guess_strategy_farthest"; 1338 } else { 1339 // Unknown strategy - don't pick 1340 return null; 1341 } 1342 1343 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1344 System.out.println("[GUESS_COLUMN_STRATEGY] Column '" + columnName + 1345 "' picked from table '" + pickedTable.getName() + "' using " + evidence); 1346 } 1347 1348 // Create ColumnSource with the picked table 1349 return new ColumnSource( 1350 this, 1351 columnName, 1352 null, 1353 0.7, // Lower confidence since it's a guess 1354 evidence, 1355 pickedTable // Override table for getFinalTable() 1356 ); 1357 } 1358 1359 /** 1360 * Find a column in explicit subquery sources within the FROM clause. 1361 * This is used for ambiguous star cases where we try to find the column 1362 * in subqueries that have explicit columns before giving up. 1363 */ 1364 private ColumnSource findColumnInExplicitSources(String columnName) { 1365 if (subquery == null || subquery.tables == null) { 1366 return null; 1367 } 1368 1369 ColumnSource foundSource = null; 1370 List<TTable> physicalTables = new ArrayList<>(); 1371 1372 for (int i = 0; i < subquery.tables.size(); i++) { 1373 TTable table = subquery.tables.getTable(i); 1374 if (table == null) continue; 1375 1376 // For join tables, collect physical tables from within the join 1377 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1378 collectPhysicalTablesFromJoin(table, physicalTables); 1379 continue; 1380 } 1381 1382 // Check if this is a subquery with explicit columns 1383 if (table.getSubquery() != null) { 1384 TResultColumnList resultCols = table.getSubquery().getResultColumnList(); 1385 if (resultCols != null) { 1386 for (int j = 0; j < resultCols.size(); j++) { 1387 TResultColumn rc = resultCols.getResultColumn(j); 1388 String rcName = getResultColumnName(rc); 1389 if (rcName != null && nameMatcher.matches(rcName, columnName)) { 1390 // Found in this subquery - trace to its final table (use cached namespace) 1391 SubqueryNamespace nestedNs = getOrCreateNestedNamespace( 1392 table.getSubquery(), table.getAliasName()); 1393 if (nestedNs != null) { 1394 ColumnSource nestedSource = nestedNs.resolveColumn(columnName); 1395 if (nestedSource != null) { 1396 if (foundSource != null) { 1397 // Found in multiple sources - ambiguous 1398 return null; 1399 } 1400 foundSource = nestedSource; 1401 } 1402 } 1403 } 1404 } 1405 } 1406 } else if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1407 // Physical table - add to list 1408 physicalTables.add(table); 1409 } 1410 } 1411 1412 // If found in an explicit subquery source, return it 1413 if (foundSource != null) { 1414 return foundSource; 1415 } 1416 1417 // If column not found in any explicit source, and there's exactly one physical table, 1418 // infer from that table (use cached namespace) 1419 if (physicalTables.size() == 1) { 1420 // Create inferred column source from the single physical table 1421 TableNamespace tableNs = getOrCreateTableNamespace(physicalTables.get(0)); 1422 return tableNs != null ? tableNs.resolveColumn(columnName) : null; 1423 } 1424 1425 return null; 1426 } 1427 1428 /** 1429 * Trace a column through qualified references in the subquery. 1430 * This looks for qualified column references (like table1.x) in the subquery 1431 * that match the requested column name, and returns a ColumnSource from the 1432 * corresponding table. 1433 * 1434 * Uses getRelations() and TJoinExpr to properly traverse JOIN structures. 1435 */ 1436 private ColumnSource traceColumnThroughQualifiedReferences(String columnName) { 1437 if (subquery == null || columnName == null) { 1438 return null; 1439 } 1440 1441 // Debug logging 1442 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1443 System.out.println("TRACE: traceColumnThroughQualifiedReferences called for column: " + columnName); 1444 } 1445 1446 // Collect all physical tables using getRelations() - the proper way to get all tables 1447 List<TTable> physicalTables = new ArrayList<>(); 1448 java.util.ArrayList<TTable> relations = subquery.getRelations(); 1449 if (relations != null) { 1450 for (TTable table : relations) { 1451 if (table == null) continue; 1452 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1453 // For JOIN type, collect tables from the JoinExpr 1454 collectPhysicalTablesFromJoinExpr(table.getJoinExpr(), physicalTables); 1455 } else if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1456 physicalTables.add(table); 1457 } 1458 } 1459 } 1460 1461 if (physicalTables.isEmpty()) { 1462 return null; 1463 } 1464 1465 // Look for qualified column references in JOIN ON conditions using TJoinExpr 1466 TTable matchedTable = null; 1467 int matchCount = 0; 1468 1469 // Check WHERE clause 1470 if (subquery.getWhereClause() != null && subquery.getWhereClause().getCondition() != null) { 1471 TTable found = findTableFromQualifiedColumnInExpression( 1472 subquery.getWhereClause().getCondition(), columnName, physicalTables); 1473 if (found != null) { 1474 matchedTable = found; 1475 matchCount++; 1476 } 1477 } 1478 1479 // Check JOIN conditions using getRelations() and TJoinExpr 1480 if (relations != null) { 1481 for (TTable table : relations) { 1482 if (table != null && table.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1483 gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = table.getJoinExpr(); 1484 if (joinExpr != null) { 1485 TTable found = findTableFromQualifiedColumnInJoinExpr(joinExpr, columnName, physicalTables); 1486 if (found != null && (matchedTable == null || found != matchedTable)) { 1487 if (matchedTable != null && found != matchedTable) { 1488 matchCount++; 1489 } else { 1490 matchedTable = found; 1491 matchCount = 1; 1492 } 1493 } 1494 } 1495 } 1496 } 1497 } 1498 1499 // If found in exactly one table, return column source with SubqueryNamespace as source 1500 // The overrideTable ensures getFinalTable() returns the traced table 1501 if (matchCount == 1 && matchedTable != null) { 1502 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1503 System.out.println("TRACE: Found column " + columnName + " in table: " + matchedTable.getName()); 1504 } 1505 // Store the traced table mapping for reference 1506 if (tracedColumnTables == null) { 1507 tracedColumnTables = new java.util.LinkedHashMap<>(); 1508 } 1509 tracedColumnTables.put(columnName, matchedTable); 1510 1511 // Return a ColumnSource with this SubqueryNamespace as source (for priority) 1512 // and the traced table as overrideTable (for correct getFinalTable()) 1513 ColumnSource source = new ColumnSource( 1514 this, 1515 columnName, 1516 null, 1517 0.95, // High confidence - traced through qualified reference 1518 "traced_through_qualified_ref", 1519 matchedTable // Override table for getFinalTable() 1520 ); 1521 return source; 1522 } 1523 1524 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1525 System.out.println("TRACE: Column " + columnName + " NOT found in qualified references, matchCount=" + matchCount); 1526 } 1527 return null; 1528 } 1529 1530 /** 1531 * Collect physical tables from a TJoinExpr recursively. 1532 */ 1533 private void collectPhysicalTablesFromJoinExpr(gudusoft.gsqlparser.nodes.TJoinExpr joinExpr, List<TTable> physicalTables) { 1534 if (joinExpr == null) return; 1535 1536 // Process left table 1537 TTable leftTable = joinExpr.getLeftTable(); 1538 if (leftTable != null) { 1539 if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join && leftTable.getJoinExpr() != null) { 1540 collectPhysicalTablesFromJoinExpr(leftTable.getJoinExpr(), physicalTables); 1541 } else if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1542 physicalTables.add(leftTable); 1543 } 1544 } 1545 1546 // Process right table 1547 TTable rightTable = joinExpr.getRightTable(); 1548 if (rightTable != null) { 1549 if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.join && rightTable.getJoinExpr() != null) { 1550 collectPhysicalTablesFromJoinExpr(rightTable.getJoinExpr(), physicalTables); 1551 } else if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1552 physicalTables.add(rightTable); 1553 } 1554 } 1555 } 1556 1557 /** 1558 * Find table from qualified column references in a TJoinExpr and its nested joins. 1559 */ 1560 private TTable findTableFromQualifiedColumnInJoinExpr( 1561 gudusoft.gsqlparser.nodes.TJoinExpr joinExpr, 1562 String columnName, 1563 List<TTable> physicalTables) { 1564 if (joinExpr == null) return null; 1565 1566 // Check ON condition of this join 1567 if (joinExpr.getOnCondition() != null) { 1568 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1569 System.out.println("TRACE: Checking TJoinExpr ON condition: " + joinExpr.getOnCondition()); 1570 } 1571 TTable found = findTableFromQualifiedColumnInExpression( 1572 joinExpr.getOnCondition(), columnName, physicalTables); 1573 if (found != null) return found; 1574 } 1575 1576 // Recursively check nested joins on the left side 1577 TTable leftTable = joinExpr.getLeftTable(); 1578 if (leftTable != null && leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join 1579 && leftTable.getJoinExpr() != null) { 1580 TTable found = findTableFromQualifiedColumnInJoinExpr(leftTable.getJoinExpr(), columnName, physicalTables); 1581 if (found != null) return found; 1582 } 1583 1584 // Recursively check nested joins on the right side 1585 TTable rightTable = joinExpr.getRightTable(); 1586 if (rightTable != null && rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.join 1587 && rightTable.getJoinExpr() != null) { 1588 TTable found = findTableFromQualifiedColumnInJoinExpr(rightTable.getJoinExpr(), columnName, physicalTables); 1589 if (found != null) return found; 1590 } 1591 1592 return null; 1593 } 1594 1595 /** 1596 * Find table from qualified column references in an expression. 1597 * Looks for patterns like table1.column_name and returns the matching table. 1598 * Uses iterative DFS to avoid StackOverflowError for deeply nested expression chains. 1599 */ 1600 private TTable findTableFromQualifiedColumnInExpression( 1601 gudusoft.gsqlparser.nodes.TExpression expr, 1602 String columnName, 1603 List<TTable> physicalTables) { 1604 if (expr == null) return null; 1605 1606 Deque<gudusoft.gsqlparser.nodes.TExpression> stack = new ArrayDeque<>(); 1607 stack.push(expr); 1608 while (!stack.isEmpty()) { 1609 gudusoft.gsqlparser.nodes.TExpression current = stack.pop(); 1610 if (current == null) continue; 1611 1612 // Check if this expression is a qualified column reference 1613 if (current.getExpressionType() == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) { 1614 gudusoft.gsqlparser.nodes.TObjectName objName = current.getObjectOperand(); 1615 if (objName != null) { 1616 String colName = objName.getColumnNameOnly(); 1617 String tablePrefix = objName.getTableString(); 1618 1619 if (colName != null && nameMatcher.matches(colName, columnName) && 1620 tablePrefix != null && !tablePrefix.isEmpty()) { 1621 // Found a qualified reference to this column - find the matching table 1622 for (TTable table : physicalTables) { 1623 String tableName = table.getName(); 1624 String tableAlias = table.getAliasName(); 1625 1626 if ((tableName != null && nameMatcher.matches(tableName, tablePrefix)) || 1627 (tableAlias != null && nameMatcher.matches(tableAlias, tablePrefix))) { 1628 return table; 1629 } 1630 } 1631 } 1632 } 1633 } 1634 1635 // Push sub-expressions onto stack (right first so left is processed first) 1636 if (current.getRightOperand() != null) stack.push(current.getRightOperand()); 1637 if (current.getLeftOperand() != null) stack.push(current.getLeftOperand()); 1638 } 1639 1640 return null; 1641 } 1642 1643 /** 1644 * Find table from qualified column references in JOIN conditions. 1645 */ 1646 private TTable findTableFromQualifiedColumnInJoin( 1647 TTable joinTable, 1648 String columnName, 1649 List<TTable> physicalTables) { 1650 if (joinTable == null) return null; 1651 1652 gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = joinTable.getJoinExpr(); 1653 if (joinExpr == null) { 1654 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1655 System.out.println("TRACE: findTableFromQualifiedColumnInJoin - joinExpr is null"); 1656 } 1657 return null; 1658 } 1659 1660 // Check the ON condition of this join 1661 if (joinExpr.getOnCondition() != null) { 1662 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1663 System.out.println("TRACE: Checking ON condition for column " + columnName + ": " + joinExpr.getOnCondition()); 1664 } 1665 TTable found = findTableFromQualifiedColumnInExpression( 1666 joinExpr.getOnCondition(), columnName, physicalTables); 1667 if (found != null) return found; 1668 } else { 1669 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1670 System.out.println("TRACE: findTableFromQualifiedColumnInJoin - ON condition is null"); 1671 } 1672 } 1673 1674 // Recursively check nested joins on the left side 1675 if (joinExpr.getLeftTable() != null && 1676 joinExpr.getLeftTable().getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1677 TTable found = findTableFromQualifiedColumnInJoin( 1678 joinExpr.getLeftTable(), columnName, physicalTables); 1679 if (found != null) return found; 1680 } 1681 1682 // Recursively check nested joins on the right side 1683 if (joinExpr.getRightTable() != null && 1684 joinExpr.getRightTable().getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1685 TTable found = findTableFromQualifiedColumnInJoin( 1686 joinExpr.getRightTable(), columnName, physicalTables); 1687 if (found != null) return found; 1688 } 1689 1690 return null; 1691 } 1692 1693 /** 1694 * Collect all physical tables from a JOIN expression recursively. 1695 */ 1696 private void collectPhysicalTablesFromJoin(TTable joinTable, List<TTable> physicalTables) { 1697 if (joinTable == null) return; 1698 1699 gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = joinTable.getJoinExpr(); 1700 if (joinExpr == null) return; 1701 1702 // Process left side 1703 TTable leftTable = joinExpr.getLeftTable(); 1704 if (leftTable != null) { 1705 if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1706 collectPhysicalTablesFromJoin(leftTable, physicalTables); 1707 } else if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1708 physicalTables.add(leftTable); 1709 } else if (leftTable.getSubquery() != null) { 1710 // For subqueries within joins, we could trace through but for now just note it exists 1711 physicalTables.add(leftTable); 1712 } 1713 } 1714 1715 // Process right side 1716 TTable rightTable = joinExpr.getRightTable(); 1717 if (rightTable != null) { 1718 if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1719 collectPhysicalTablesFromJoin(rightTable, physicalTables); 1720 } else if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1721 physicalTables.add(rightTable); 1722 } else if (rightTable.getSubquery() != null) { 1723 physicalTables.add(rightTable); 1724 } 1725 } 1726 } 1727 1728 /** 1729 * Get the name of a result column (either alias or column name) 1730 */ 1731 private String getResultColumnName(TResultColumn rc) { 1732 if (rc == null) return null; 1733 1734 // Check for alias 1735 if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) { 1736 return rc.getAliasClause().getAliasName().toString(); 1737 } 1738 1739 // Check for star - not a named column 1740 String colStr = rc.toString().trim(); 1741 if (colStr.endsWith("*")) { 1742 return null; 1743 } 1744 1745 // Check for simple column reference 1746 if (rc.getExpr() != null) { 1747 gudusoft.gsqlparser.nodes.TExpression expr = rc.getExpr(); 1748 if (expr.getExpressionType() == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) { 1749 gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand(); 1750 if (objName != null) { 1751 return objName.getColumnNameOnly(); 1752 } 1753 } 1754 } 1755 1756 return null; 1757 } 1758 1759 @Override 1760 public String toString() { 1761 int totalColumns = (columnSources != null ? columnSources.size() : 0) + 1762 (inferredColumns != null ? inferredColumns.size() : 0); 1763 return "SubqueryNamespace(" + getDisplayName() + ", columns=" + totalColumns + 1764 ", inferred=" + (inferredColumns != null ? inferredColumns.size() : 0) + ")"; 1765 } 1766}