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