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