001package gudusoft.gsqlparser.resolver2.namespace; 002 003import gudusoft.gsqlparser.nodes.TCTE; 004import gudusoft.gsqlparser.nodes.TObjectName; 005import gudusoft.gsqlparser.nodes.TResultColumn; 006import gudusoft.gsqlparser.nodes.TResultColumnList; 007import gudusoft.gsqlparser.nodes.TTable; 008import gudusoft.gsqlparser.resolver2.ColumnLevel; 009import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 010import gudusoft.gsqlparser.resolver2.model.ColumnSource; 011import gudusoft.gsqlparser.stmt.TSelectSqlStatement; 012 013import java.util.ArrayList; 014import java.util.Collections; 015import java.util.HashSet; 016import java.util.LinkedHashMap; 017import java.util.List; 018import java.util.Map; 019import java.util.Set; 020 021/** 022 * Namespace representing a Common Table Expression (CTE). 023 * Similar to SubqueryNamespace but handles CTE-specific features: 024 * - Explicit column list: WITH cte(c1, c2) AS (SELECT ...) 025 * - Recursive CTEs 026 * - Multiple references within same query 027 * - UNION subqueries: columns are pushed through to all UNION branches 028 * 029 * Example: 030 * WITH my_cte(id, name) AS ( 031 * SELECT user_id, user_name FROM users 032 * ) 033 * SELECT id, name FROM my_cte; 034 */ 035public class CTENamespace extends AbstractNamespace { 036 037 private final TCTE cte; 038 private final String cteName; 039 private final TSelectSqlStatement selectStatement; 040 041 /** CTE column list (explicit column names) */ 042 private final List<String> explicitColumns; 043 044 /** Whether this CTE is recursive */ 045 private final boolean recursive; 046 047 /** UnionNamespace if this CTE's subquery is a UNION */ 048 private UnionNamespace unionNamespace; 049 050 /** Inferred columns from star push-down */ 051 private Map<String, ColumnSource> inferredColumns; 052 053 /** Track inferred column names */ 054 private Set<String> inferredColumnNames; 055 056 /** 057 * The TTable that references this CTE in a FROM clause. 058 * Used as fallback for getFinalTable() when there's no underlying physical table. 059 * For example: WITH cte AS (SELECT 1 AS col) SELECT col FROM cte 060 * The referencing TTable is the 'cte' in the FROM clause. 061 */ 062 private TTable referencingTable; 063 064 /** 065 * Namespaces for FROM clause tables that support dynamic inference. 066 * Used to propagate inferred columns through deeply nested structures. 067 * Lazily initialized when needed. 068 * 069 * Example: WITH cte AS (SELECT * FROM (SELECT * FROM t1 UNION ALL SELECT * FROM t2) sub) 070 * The 'sub' subquery namespace is stored here for propagation. 071 */ 072 private List<INamespace> fromClauseNamespaces; 073 074 public CTENamespace(TCTE cte, 075 String cteName, 076 TSelectSqlStatement selectStatement, 077 INameMatcher nameMatcher) { 078 super(cte, nameMatcher); 079 this.cte = cte; 080 this.cteName = cteName; 081 this.selectStatement = selectStatement; 082 this.explicitColumns = extractExplicitColumns(cte); 083 this.recursive = isRecursiveCTE(cte); 084 085 // If the CTE's subquery is a UNION, create a UnionNamespace to handle it 086 if (selectStatement != null && selectStatement.isCombinedQuery()) { 087 this.unionNamespace = new UnionNamespace(selectStatement, cteName + "_union", nameMatcher); 088 } 089 } 090 091 public CTENamespace(TCTE cte, String cteName, TSelectSqlStatement selectStatement) { 092 this(cte, cteName, selectStatement, null); 093 } 094 095 @Override 096 public String getDisplayName() { 097 return cteName; 098 } 099 100 /** 101 * Get the TTable that references this CTE in a FROM clause. 102 */ 103 public TTable getReferencingTable() { 104 return referencingTable; 105 } 106 107 /** 108 * {@inheritDoc} 109 * For CTENamespace, returns the TTable that references this CTE in the query. 110 * This is the immediate source table for columns resolved through this CTE. 111 */ 112 @Override 113 public TTable getSourceTable() { 114 return referencingTable; 115 } 116 117 /** 118 * Set the TTable that references this CTE in a FROM clause. 119 * Called by ScopeBuilder when a CTE is referenced. 120 */ 121 public void setReferencingTable(TTable table) { 122 this.referencingTable = table; 123 } 124 125 @Override 126 public TTable getFinalTable() { 127 // Trace through the CTE's subquery to find the underlying physical table 128 // This is similar to SubqueryNamespace.getFinalTable() but handles CTE chains 129 130 // If this CTE has a UNION subquery, delegate to UnionNamespace 131 if (unionNamespace != null) { 132 TTable unionTable = unionNamespace.getFinalTable(); 133 if (unionTable != null) { 134 return unionTable; 135 } 136 // Fallback to referencing table if UNION has no physical tables 137 return referencingTable; 138 } 139 140 // If no tables in the CTE's SELECT, return the referencing table 141 // This handles CTEs like: WITH cte AS (SELECT 1 AS col) 142 if (selectStatement == null || selectStatement.tables == null || selectStatement.tables.size() == 0) { 143 return referencingTable; 144 } 145 146 // Check for qualified star column (e.g., CTE_NAME.*) first 147 TTable qualifiedStarTable = findTableFromQualifiedStar(); 148 if (qualifiedStarTable != null) { 149 return qualifiedStarTable; 150 } 151 152 // For single-table CTEs, trace to the underlying table 153 TTable firstTable = selectStatement.tables.getTable(0); 154 if (firstTable == null) { 155 return null; 156 } 157 158 // If it's a physical table (not a CTE reference), return it 159 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !firstTable.isCTEName()) { 160 return firstTable; 161 } 162 163 // If it's a CTE reference, trace through the CTE chain 164 if (firstTable.isCTEName() && firstTable.getCTE() != null) { 165 return traceTableThroughCTE(firstTable.getCTE()); 166 } 167 168 // If it's a subquery, trace through it 169 if (firstTable.getSubquery() != null) { 170 SubqueryNamespace nestedNs = new SubqueryNamespace( 171 firstTable.getSubquery(), 172 firstTable.getAliasName(), 173 nameMatcher 174 ); 175 nestedNs.validate(); 176 TTable subTable = nestedNs.getFinalTable(); 177 if (subTable != null) { 178 return subTable; 179 } 180 } 181 182 // If it's a join, get the first base table 183 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 184 TTable joinTable = findFirstPhysicalTableFromJoin(firstTable); 185 if (joinTable != null) { 186 return joinTable; 187 } 188 } 189 190 // Fallback: return the referencing TTable (the CTE reference in FROM clause) 191 // This is used when the CTE doesn't have underlying physical tables, 192 // e.g., WITH cte AS (SELECT 1 AS col) - the columns are literals, not from tables 193 return referencingTable; 194 } 195 196 /** 197 * Find the table referenced by a qualified star column in this CTE's SELECT list. 198 * Example: SELECT other_cte.* FROM other_cte -> traces to other_cte's underlying table 199 */ 200 private TTable findTableFromQualifiedStar() { 201 if (selectStatement == null || selectStatement.getResultColumnList() == null) { 202 return null; 203 } 204 205 TResultColumnList selectList = selectStatement.getResultColumnList(); 206 for (int i = 0; i < selectList.size(); i++) { 207 TResultColumn resultCol = selectList.getResultColumn(i); 208 if (resultCol == null) continue; 209 210 String colStr = resultCol.toString().trim(); 211 // Check if it's a qualified star (contains . before *) 212 if (colStr.endsWith("*") && colStr.contains(".")) { 213 int dotIndex = colStr.lastIndexOf('.'); 214 if (dotIndex > 0) { 215 String tablePrefix = colStr.substring(0, dotIndex).trim(); 216 // Find the table with this alias or name 217 TTable matchingTable = findTableByAliasOrName(tablePrefix); 218 if (matchingTable != null) { 219 // If the matching table is a CTE reference, trace through it 220 if (matchingTable.isCTEName() && matchingTable.getCTE() != null) { 221 return traceTableThroughCTE(matchingTable.getCTE()); 222 } 223 // If it's a subquery, trace through it 224 if (matchingTable.getSubquery() != null) { 225 SubqueryNamespace nestedNs = new SubqueryNamespace( 226 matchingTable.getSubquery(), 227 matchingTable.getAliasName(), 228 nameMatcher 229 ); 230 nestedNs.validate(); 231 return nestedNs.getFinalTable(); 232 } 233 // If it's a physical table, return it 234 if (matchingTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !matchingTable.isCTEName()) { 235 return matchingTable; 236 } 237 } 238 } 239 } 240 } 241 return null; 242 } 243 244 /** 245 * Find a table in the FROM clause by alias or name. 246 */ 247 private TTable findTableByAliasOrName(String nameOrAlias) { 248 if (selectStatement == null || selectStatement.tables == null) { 249 return null; 250 } 251 252 for (int i = 0; i < selectStatement.tables.size(); i++) { 253 TTable table = selectStatement.tables.getTable(i); 254 if (table == null) continue; 255 256 // Check alias 257 String alias = table.getAliasName(); 258 if (alias != null && nameMatcher.matches(alias, nameOrAlias)) { 259 return table; 260 } 261 262 // Check table name 263 if (table.getTableName() != null && nameMatcher.matches(table.getTableName().toString(), nameOrAlias)) { 264 return table; 265 } 266 } 267 return null; 268 } 269 270 /** 271 * Trace through a CTE to find its underlying physical table. 272 * This handles CTE chains like: CTE1 -> CTE2 -> CTE3 -> physical_table 273 */ 274 private TTable traceTableThroughCTE(TCTE cteNode) { 275 if (cteNode == null || cteNode.getSubquery() == null) { 276 return null; 277 } 278 279 TSelectSqlStatement cteSubquery = cteNode.getSubquery(); 280 281 // Handle UNION in the CTE 282 if (cteSubquery.isCombinedQuery()) { 283 // For UNION, trace the left branch 284 TSelectSqlStatement leftStmt = cteSubquery.getLeftStmt(); 285 if (leftStmt != null && leftStmt.tables != null && leftStmt.tables.size() > 0) { 286 cteSubquery = leftStmt; 287 } 288 } 289 290 if (cteSubquery.tables == null || cteSubquery.tables.size() == 0) { 291 return null; 292 } 293 294 TTable firstTable = cteSubquery.tables.getTable(0); 295 if (firstTable == null) { 296 return null; 297 } 298 299 // If it's a physical table (not CTE), we found it 300 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !firstTable.isCTEName()) { 301 return firstTable; 302 } 303 304 // If it's another CTE reference, continue tracing 305 if (firstTable.isCTEName() && firstTable.getCTE() != null) { 306 return traceTableThroughCTE(firstTable.getCTE()); 307 } 308 309 // If it's a subquery, trace through it 310 if (firstTable.getSubquery() != null) { 311 SubqueryNamespace nestedNs = new SubqueryNamespace( 312 firstTable.getSubquery(), 313 firstTable.getAliasName(), 314 nameMatcher 315 ); 316 nestedNs.validate(); 317 return nestedNs.getFinalTable(); 318 } 319 320 // If it's a join, get the first base table 321 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 322 return findFirstPhysicalTableFromJoin(firstTable); 323 } 324 325 return null; 326 } 327 328 /** 329 * Find the first physical table from a JOIN expression. 330 */ 331 private TTable findFirstPhysicalTableFromJoin(TTable joinTable) { 332 if (joinTable == null || joinTable.getJoinExpr() == null) { 333 return null; 334 } 335 336 gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = joinTable.getJoinExpr(); 337 338 // Check left side first 339 TTable leftTable = joinExpr.getLeftTable(); 340 if (leftTable != null) { 341 if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !leftTable.isCTEName()) { 342 return leftTable; 343 } 344 if (leftTable.isCTEName() && leftTable.getCTE() != null) { 345 TTable traced = traceTableThroughCTE(leftTable.getCTE()); 346 if (traced != null) return traced; 347 } 348 if (leftTable.getSubquery() != null) { 349 SubqueryNamespace nestedNs = new SubqueryNamespace( 350 leftTable.getSubquery(), 351 leftTable.getAliasName(), 352 nameMatcher 353 ); 354 nestedNs.validate(); 355 return nestedNs.getFinalTable(); 356 } 357 if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 358 return findFirstPhysicalTableFromJoin(leftTable); 359 } 360 } 361 362 // Check right side 363 TTable rightTable = joinExpr.getRightTable(); 364 if (rightTable != null) { 365 if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !rightTable.isCTEName()) { 366 return rightTable; 367 } 368 if (rightTable.isCTEName() && rightTable.getCTE() != null) { 369 return traceTableThroughCTE(rightTable.getCTE()); 370 } 371 } 372 373 return null; 374 } 375 376 @Override 377 public List<TTable> getAllFinalTables() { 378 // If this CTE has a UNION subquery, delegate to the UnionNamespace 379 if (unionNamespace != null) { 380 return unionNamespace.getAllFinalTables(); 381 } 382 383 // Check if this CTE references another CTE (which might be a UNION) 384 if (selectStatement != null && selectStatement.tables != null && selectStatement.tables.size() > 0) { 385 TTable firstTable = selectStatement.tables.getTable(0); 386 if (firstTable != null && firstTable.isCTEName() && firstTable.getCTE() != null) { 387 TCTE referencedCTE = firstTable.getCTE(); 388 if (referencedCTE.getSubquery() != null) { 389 // Create a namespace for the referenced CTE to get its tables 390 CTENamespace referencedNs = new CTENamespace( 391 referencedCTE, 392 referencedCTE.getTableName() != null ? referencedCTE.getTableName().toString() : "cte", 393 referencedCTE.getSubquery(), 394 nameMatcher 395 ); 396 referencedNs.validate(); 397 // This will trace through the CTE chain to get all tables 398 // including from UNION branches 399 return referencedNs.getAllFinalTables(); 400 } 401 } 402 } 403 404 // For non-UNION, non-CTE-reference CTEs, return the single final table 405 TTable finalTable = getFinalTable(); 406 if (finalTable != null) { 407 return Collections.singletonList(finalTable); 408 } 409 410 return Collections.emptyList(); 411 } 412 413 @Override 414 protected void doValidate() { 415 columnSources = new LinkedHashMap<>(); 416 417 if (selectStatement == null || selectStatement.getResultColumnList() == null) { 418 return; 419 } 420 421 TResultColumnList selectList = selectStatement.getResultColumnList(); 422 423 // If CTE has explicit column list, use it 424 if (!explicitColumns.isEmpty()) { 425 validateWithExplicitColumns(selectList); 426 } else { 427 // No explicit columns, derive from SELECT list 428 validateWithImplicitColumns(selectList); 429 } 430 } 431 432 /** 433 * Validate CTE with explicit column list. 434 * Example: WITH cte(c1, c2, c3) AS (SELECT a, b, c FROM t) 435 * 436 * Two cases are handled: 437 * 438 * 1. **Position-based (Snowflake pattern)**: CTE explicit column list + SELECT * 439 * Example: WITH cte(c1, c2, c3) AS (SELECT * FROM Employees) 440 * - c1/c2/c3 are positional aliases for star expansion 441 * - Without metadata: c1 -> Employees.*, c2 -> Employees.*, c3 -> Employees.* 442 * - With metadata: c1 -> Employees.<col_1>, c2 -> Employees.<col_2>, etc. 443 * 444 * 2. **Direct mapping**: CTE explicit column list + named columns 445 * Example: WITH cte(c1, c2) AS (SELECT id, name FROM t) 446 * - c1 -> t.id, c2 -> t.name (1:1 positional mapping) 447 * 448 * @see <a href="star_column_pushdown.md#cte-explicit-column-list--select--snowflake-case"> 449 * Documentation: CTE Explicit Column List + SELECT *</a> 450 */ 451 private void validateWithExplicitColumns(TResultColumnList selectList) { 452 // Check if the SELECT list contains only star column(s) - the position-based pattern 453 StarColumnInfo starInfo = analyzeStarColumns(selectList); 454 455 if (starInfo.isSingleStar()) { 456 // Position-based case: CTE(c1,c2,c3) AS (SELECT * FROM t) 457 // The column names c1/c2/c3 are positional aliases, not real column names 458 handleExplicitColumnsWithStar(starInfo.getStarColumn(), starInfo.getStarQualifier()); 459 } else { 460 // Direct mapping case: CTE(c1,c2) AS (SELECT id, name FROM t) 461 // Each explicit column maps to corresponding SELECT list item by position 462 handleExplicitColumnsWithDirectMapping(selectList); 463 } 464 } 465 466 /** 467 * Handle CTE explicit columns when SELECT list is a star. 468 * This is the position-based (Snowflake) pattern. 469 * 470 * @param starColumn the star column (* or table.*) 471 * @param starQualifier the table qualifier if qualified star (e.g., "src" for "src.*"), or null 472 */ 473 private void handleExplicitColumnsWithStar(TResultColumn starColumn, String starQualifier) { 474 // Try ordinal mapping if metadata is available 475 List<String> ordinalColumns = tryOrdinalMapping(starQualifier); 476 477 if (ordinalColumns != null && ordinalColumns.size() >= explicitColumns.size()) { 478 // Metadata available - use ordinal mapping: c1 -> Employees.<col_1> 479 for (int i = 0; i < explicitColumns.size(); i++) { 480 String cteColName = explicitColumns.get(i); 481 String baseColName = ordinalColumns.get(i); 482 483 ColumnSource source = new ColumnSource( 484 this, 485 cteColName, 486 starColumn, // Reference to star column 487 1.0, // High confidence - ordinal mapping from metadata 488 "cte_explicit_column_ordinal:" + baseColName 489 ); 490 columnSources.put(cteColName, source); 491 } 492 } else { 493 // No metadata - fallback to star reference: c1 -> Employees.* 494 for (String colName : explicitColumns) { 495 ColumnSource source = new ColumnSource( 496 this, 497 colName, 498 starColumn, // Reference to star column 499 0.8, // Lower confidence - ordinal mapping unknown 500 "cte_explicit_column_via_star" 501 ); 502 columnSources.put(colName, source); 503 } 504 } 505 } 506 507 /** 508 * Handle CTE explicit columns with direct positional mapping to SELECT list. 509 * 510 * @param selectList the SELECT list to map from 511 */ 512 private void handleExplicitColumnsWithDirectMapping(TResultColumnList selectList) { 513 int columnCount = Math.min(explicitColumns.size(), selectList.size()); 514 515 for (int i = 0; i < columnCount; i++) { 516 String colName = explicitColumns.get(i); 517 TResultColumn resultCol = selectList.getResultColumn(i); 518 519 ColumnSource source = new ColumnSource( 520 this, 521 colName, 522 resultCol, 523 1.0, // Definite - direct positional mapping 524 "cte_explicit_column" 525 ); 526 527 columnSources.put(colName, source); 528 } 529 } 530 531 /** 532 * Try to get ordered column names from metadata for ordinal mapping. 533 * 534 * @param starQualifier the table qualifier (e.g., "src"), or null for unqualified star 535 * @return ordered list of column names from metadata, or null if not available 536 */ 537 private List<String> tryOrdinalMapping(String starQualifier) { 538 // TODO: When metadata (TSQLEnv/DDL) is available, return ordered column list 539 // For now, return null to use the fallback (star reference) 540 // 541 // Future implementation: 542 // 1. Find the source table namespace by starQualifier 543 // 2. Get its column sources (which use LinkedHashMap for insertion order) 544 // 3. Return the column names in order 545 return null; 546 } 547 548 /** 549 * Analyze star columns in the SELECT list. 550 * Determines if the SELECT is a single star column pattern. 551 */ 552 private StarColumnInfo analyzeStarColumns(TResultColumnList selectList) { 553 if (selectList == null || selectList.size() == 0) { 554 return new StarColumnInfo(); 555 } 556 557 // Check for single star column pattern 558 if (selectList.size() == 1) { 559 TResultColumn rc = selectList.getResultColumn(0); 560 if (isStarColumn(rc)) { 561 String qualifier = getStarQualifier(rc); 562 return new StarColumnInfo(rc, qualifier); 563 } 564 } 565 566 return new StarColumnInfo(); 567 } 568 569 /** 570 * Check if a result column is a star column (* or table.*) 571 */ 572 private boolean isStarColumn(TResultColumn rc) { 573 if (rc == null) { 574 return false; 575 } 576 String str = rc.toString(); 577 return str != null && (str.equals("*") || str.endsWith(".*")); 578 } 579 580 /** 581 * Get the qualifier from a qualified star (src.* returns "src") 582 */ 583 private String getStarQualifier(TResultColumn rc) { 584 if (rc == null) { 585 return null; 586 } 587 String str = rc.toString(); 588 if (str != null && str.endsWith(".*") && str.length() > 2) { 589 return str.substring(0, str.length() - 2); 590 } 591 return null; 592 } 593 594 /** 595 * Helper class to hold star column analysis results. 596 */ 597 private static class StarColumnInfo { 598 private final TResultColumn starColumn; 599 private final String starQualifier; 600 601 StarColumnInfo() { 602 this.starColumn = null; 603 this.starQualifier = null; 604 } 605 606 StarColumnInfo(TResultColumn starColumn, String starQualifier) { 607 this.starColumn = starColumn; 608 this.starQualifier = starQualifier; 609 } 610 611 boolean isSingleStar() { 612 return starColumn != null; 613 } 614 615 TResultColumn getStarColumn() { 616 return starColumn; 617 } 618 619 String getStarQualifier() { 620 return starQualifier; 621 } 622 } 623 624 /** 625 * Validate CTE without explicit column list. 626 * Example: WITH cte AS (SELECT id, name FROM users) 627 */ 628 private void validateWithImplicitColumns(TResultColumnList selectList) { 629 for (int i = 0; i < selectList.size(); i++) { 630 TResultColumn resultCol = selectList.getResultColumn(i); 631 632 // Determine column name 633 String colName = getColumnName(resultCol); 634 if (colName == null) { 635 colName = "col_" + (i + 1); 636 } 637 638 // Create column source 639 ColumnSource source = new ColumnSource( 640 this, 641 colName, 642 resultCol, 643 1.0, // Definite - from SELECT list 644 "cte_implicit_column" 645 ); 646 647 columnSources.put(colName, source); 648 } 649 } 650 651 /** 652 * Extract column name from TResultColumn 653 */ 654 private String getColumnName(TResultColumn resultCol) { 655 // Check for alias 656 if (resultCol.getAliasClause() != null && 657 resultCol.getAliasClause().getAliasName() != null) { 658 return resultCol.getAliasClause().getAliasName().toString(); 659 } 660 661 // Check for simple column reference 662 if (resultCol.getExpr() != null) { 663 gudusoft.gsqlparser.nodes.TExpression expr = resultCol.getExpr(); 664 if (expr.getExpressionType() == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) { 665 TObjectName objName = expr.getObjectOperand(); 666 if (objName != null) { 667 return objName.getColumnNameOnly(); 668 } 669 } 670 } 671 672 return null; 673 } 674 675 /** 676 * Extract explicit column list from CTE 677 */ 678 private List<String> extractExplicitColumns(TCTE cte) { 679 List<String> columns = new ArrayList<>(); 680 681 if (cte != null && cte.getColumnList() != null) { 682 for (int i = 0; i < cte.getColumnList().size(); i++) { 683 TObjectName colName = cte.getColumnList().getObjectName(i); 684 if (colName != null) { 685 columns.add(colName.toString()); 686 } 687 } 688 } 689 690 return columns; 691 } 692 693 /** 694 * Check if this is a recursive CTE 695 */ 696 private boolean isRecursiveCTE(TCTE cte) { 697 if (cte == null) { 698 return false; 699 } 700 return cte.isRecursive(); 701 } 702 703 public TCTE getCTE() { 704 return cte; 705 } 706 707 @Override 708 public TSelectSqlStatement getSelectStatement() { 709 return selectStatement; 710 } 711 712 @Override 713 public boolean hasStarColumn() { 714 // If this CTE has a UNION subquery, delegate to the UnionNamespace 715 if (unionNamespace != null) { 716 return unionNamespace.hasStarColumn(); 717 } 718 719 if (selectStatement == null || selectStatement.getResultColumnList() == null) { 720 return false; 721 } 722 723 TResultColumnList selectList = selectStatement.getResultColumnList(); 724 for (int i = 0; i < selectList.size(); i++) { 725 TResultColumn resultCol = selectList.getResultColumn(i); 726 if (resultCol != null && resultCol.toString().endsWith("*")) { 727 return true; 728 } 729 } 730 return false; 731 } 732 733 @Override 734 public boolean supportsDynamicInference() { 735 return hasStarColumn(); 736 } 737 738 @Override 739 public boolean addInferredColumn(String columnName, double confidence, String evidence) { 740 if (columnName == null || columnName.isEmpty()) { 741 return false; 742 } 743 744 // Initialize maps if needed 745 if (inferredColumns == null) { 746 inferredColumns = new LinkedHashMap<>(); 747 } 748 if (inferredColumnNames == null) { 749 inferredColumnNames = new HashSet<>(); 750 } 751 752 // Check if already exists in explicit columns 753 if (columnSources != null && columnSources.containsKey(columnName)) { 754 return false; 755 } 756 757 // Check if already inferred 758 if (inferredColumns.containsKey(columnName)) { 759 return false; 760 } 761 762 // Collect candidate tables - get ALL final tables from the CTE chain 763 // This handles both UNION CTEs and CTEs that reference other CTEs 764 java.util.List<TTable> candidateTables = new java.util.ArrayList<>(); 765 766 // Get all final tables from this CTE's namespace (handles UNION and CTE chains) 767 java.util.List<TTable> allTables = this.getAllFinalTables(); 768 for (TTable table : allTables) { 769 if (table != null && !candidateTables.contains(table)) { 770 candidateTables.add(table); 771 } 772 } 773 774 // Create inferred column source WITH candidate tables if applicable 775 ColumnSource source = new ColumnSource( 776 this, 777 columnName, 778 null, 779 confidence, 780 evidence, 781 null, // overrideTable 782 (candidateTables != null && !candidateTables.isEmpty()) ? candidateTables : null 783 ); 784 785 inferredColumns.put(columnName, source); 786 inferredColumnNames.add(columnName); 787 788 // Propagate to nested namespaces if this CTE has SELECT * from subqueries/unions 789 propagateToNestedNamespaces(columnName, confidence, evidence); 790 791 // NOTE: Propagation to referenced CTEs (CTE chains like cte2 -> cte1) is handled 792 // by NamespaceEnhancer.propagateThroughCTEChains() which has access to the actual 793 // namespace instances from the scope tree. We don't do it here because creating 794 // new CTENamespace instances would not affect the actual instances used for resolution. 795 796 return true; 797 } 798 799 /** 800 * Propagate an inferred column to nested namespaces. 801 * 802 * This is a unified algorithm that handles: 803 * 1. Direct UNION subqueries (CTE body is a UNION) 804 * 2. SELECT * FROM (UNION) patterns 805 * 3. SELECT * FROM (subquery) patterns 806 * 4. Deeply nested structures with JOINs 807 * 808 * The propagation is recursive - each namespace that receives the column 809 * will further propagate to its own nested namespaces. 810 * 811 * @param columnName The column name to propagate 812 * @param confidence Confidence score 813 * @param evidence Evidence string for debugging 814 */ 815 private void propagateToNestedNamespaces(String columnName, double confidence, String evidence) { 816 // Case 1: Direct UNION subquery (CTE body is a UNION) 817 if (unionNamespace != null) { 818 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 819 System.out.println("[CTENamespace] Propagating '" + columnName + "' to direct unionNamespace in " + cteName); 820 } 821 unionNamespace.addInferredColumn(columnName, confidence, evidence + "_cte_union_propagate"); 822 return; 823 } 824 825 // Case 2: CTE has SELECT * from nested structures (subqueries, unions in FROM clause) 826 // Only propagate if the CTE's SELECT list contains a star column 827 if (!hasStarColumn()) { 828 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 829 System.out.println("[CTENamespace] No star column in " + cteName + ", skipping FROM clause propagation"); 830 } 831 return; 832 } 833 834 // Get or create namespaces for FROM clause tables 835 List<INamespace> fromNamespaces = getOrCreateFromClauseNamespaces(); 836 if (fromNamespaces.isEmpty()) { 837 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 838 System.out.println("[CTENamespace] No FROM clause namespaces with dynamic inference in " + cteName); 839 } 840 return; 841 } 842 843 // Propagate to each FROM clause namespace 844 for (INamespace ns : fromNamespaces) { 845 if (ns.supportsDynamicInference()) { 846 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 847 System.out.println("[CTENamespace] Propagating '" + columnName + "' to FROM clause namespace " + 848 ns.getDisplayName() + " in " + cteName); 849 } 850 // The nested namespace's addInferredColumn will recursively propagate further 851 ns.addInferredColumn(columnName, confidence, evidence + "_cte_from_propagate"); 852 } 853 } 854 } 855 856 @Override 857 public Set<String> getInferredColumns() { 858 if (inferredColumnNames == null) { 859 return Collections.emptySet(); 860 } 861 return Collections.unmodifiableSet(inferredColumnNames); 862 } 863 864 @Override 865 public ColumnLevel hasColumn(String columnName) { 866 ensureValidated(); 867 868 // Check in explicit columns 869 if (columnSources != null) { 870 for (String existingCol : columnSources.keySet()) { 871 if (nameMatcher.matches(existingCol, columnName)) { 872 return ColumnLevel.EXISTS; 873 } 874 } 875 } 876 877 // Check in inferred columns 878 if (inferredColumns != null && inferredColumns.containsKey(columnName)) { 879 return ColumnLevel.EXISTS; 880 } 881 882 // If has star column, unknown columns MAYBE exist 883 if (hasStarColumn()) { 884 return ColumnLevel.MAYBE; 885 } 886 887 // If the CTE has explicit column definitions like "cte(c1, c2, c3)", then ONLY 888 // those columns exist - don't return MAYBE for other columns. 889 // This prevents ambiguous resolution when a CTE with explicit columns is joined 890 // with another table. 891 if (!explicitColumns.isEmpty()) { 892 return ColumnLevel.NOT_EXISTS; 893 } 894 895 // For CTEs without explicit columns AND without star columns, check if underlying 896 // tables might have the column. This handles cases like referencing columns from 897 // the CTE's base tables that aren't explicitly selected in the CTE's SELECT list. 898 if (selectStatement != null && selectStatement.tables != null) { 899 for (int i = 0; i < selectStatement.tables.size(); i++) { 900 TTable table = selectStatement.tables.getTable(i); 901 if (table != null && table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 902 // The CTE has a base table - column might exist there 903 return ColumnLevel.MAYBE; 904 } 905 } 906 } 907 908 return ColumnLevel.NOT_EXISTS; 909 } 910 911 @Override 912 public ColumnSource resolveColumn(String columnName) { 913 ensureValidated(); 914 915 // First check explicit columns 916 ColumnSource source = super.resolveColumn(columnName); 917 if (source != null) { 918 return source; 919 } 920 921 // Then check inferred columns 922 if (inferredColumns != null) { 923 for (Map.Entry<String, ColumnSource> entry : inferredColumns.entrySet()) { 924 if (nameMatcher.matches(entry.getKey(), columnName)) { 925 return entry.getValue(); 926 } 927 } 928 } 929 930 // If has star column, auto-infer this column 931 if (hasStarColumn()) { 932 boolean added = addInferredColumn(columnName, 0.8, "auto_inferred_from_reference"); 933 if (added && inferredColumns != null) { 934 return inferredColumns.get(columnName); 935 } 936 } 937 938 // For CTEs without star columns, check if underlying base tables might have the column. 939 // This handles references to columns that aren't explicitly selected in the CTE's SELECT list. 940 if (selectStatement != null && selectStatement.tables != null) { 941 for (int i = 0; i < selectStatement.tables.size(); i++) { 942 TTable table = selectStatement.tables.getTable(i); 943 if (table != null && table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 944 // Create an inferred column source that traces to the base table 945 boolean added = addInferredColumn(columnName, 0.6, "inferred_from_cte_base_table"); 946 if (added && inferredColumns != null) { 947 return inferredColumns.get(columnName); 948 } 949 break; 950 } 951 } 952 } 953 954 return null; 955 } 956 957 /** 958 * Get the UnionNamespace if this CTE's subquery is a UNION. 959 */ 960 public UnionNamespace getUnionNamespace() { 961 return unionNamespace; 962 } 963 964 /** 965 * Get or create namespaces for FROM clause tables that support dynamic inference. 966 * This handles cases like: WITH cte AS (SELECT * FROM (UNION) sub) 967 * where the CTE body is not directly a UNION but contains a subquery with UNION. 968 * 969 * The namespaces are lazily created and cached for reuse. 970 * 971 * @return List of namespaces that support dynamic inference (may be empty) 972 */ 973 private List<INamespace> getOrCreateFromClauseNamespaces() { 974 if (fromClauseNamespaces != null) { 975 return fromClauseNamespaces; 976 } 977 978 fromClauseNamespaces = new ArrayList<>(); 979 980 if (selectStatement == null || selectStatement.tables == null) { 981 return fromClauseNamespaces; 982 } 983 984 // Iterate through FROM clause tables and create namespaces for those that 985 // could have star columns (subqueries, unions, CTE references) 986 for (int i = 0; i < selectStatement.tables.size(); i++) { 987 TTable table = selectStatement.tables.getTable(i); 988 if (table == null) continue; 989 990 INamespace ns = createNamespaceForTable(table); 991 if (ns != null && ns.supportsDynamicInference()) { 992 fromClauseNamespaces.add(ns); 993 } 994 } 995 996 return fromClauseNamespaces; 997 } 998 999 /** 1000 * Create an appropriate namespace for a table in the FROM clause. 1001 * Handles subqueries (including UNION), CTE references, and joins recursively. 1002 * 1003 * @param table The table from the FROM clause 1004 * @return INamespace for the table, or null if not applicable 1005 */ 1006 private INamespace createNamespaceForTable(TTable table) { 1007 if (table == null) return null; 1008 1009 // Handle subquery tables 1010 if (table.getSubquery() != null) { 1011 TSelectSqlStatement subquery = table.getSubquery(); 1012 String alias = table.getAliasName(); 1013 1014 // Check if subquery is a UNION/INTERSECT/EXCEPT 1015 if (subquery.isCombinedQuery()) { 1016 UnionNamespace unionNs = new UnionNamespace(subquery, alias, nameMatcher); 1017 return unionNs; 1018 } else { 1019 // Regular subquery - create SubqueryNamespace 1020 SubqueryNamespace subNs = new SubqueryNamespace(subquery, alias, nameMatcher); 1021 subNs.validate(); 1022 return subNs; 1023 } 1024 } 1025 1026 // Handle CTE references - these are handled by NamespaceEnhancer.propagateThroughCTEChains() 1027 // We don't create new CTENamespace here because we need the actual instances from scope tree 1028 1029 // Handle JOIN tables - recursively collect from join expressions 1030 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1031 return createNamespaceForJoin(table); 1032 } 1033 1034 return null; 1035 } 1036 1037 /** 1038 * Create namespaces for tables within a JOIN expression. 1039 * Returns a composite namespace that wraps all namespaces from the join. 1040 * 1041 * @param joinTable The JOIN table 1042 * @return INamespace that wraps join namespaces, or null 1043 */ 1044 private INamespace createNamespaceForJoin(TTable joinTable) { 1045 if (joinTable == null || joinTable.getJoinExpr() == null) { 1046 return null; 1047 } 1048 1049 gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = joinTable.getJoinExpr(); 1050 1051 // Collect namespaces from both sides of the join 1052 List<INamespace> joinNamespaces = new ArrayList<>(); 1053 1054 // Left side 1055 TTable leftTable = joinExpr.getLeftTable(); 1056 if (leftTable != null) { 1057 INamespace leftNs = createNamespaceForTable(leftTable); 1058 if (leftNs != null && leftNs.supportsDynamicInference()) { 1059 joinNamespaces.add(leftNs); 1060 } 1061 } 1062 1063 // Right side 1064 TTable rightTable = joinExpr.getRightTable(); 1065 if (rightTable != null) { 1066 INamespace rightNs = createNamespaceForTable(rightTable); 1067 if (rightNs != null && rightNs.supportsDynamicInference()) { 1068 joinNamespaces.add(rightNs); 1069 } 1070 } 1071 1072 // If we found namespaces, add them to fromClauseNamespaces directly 1073 // (we don't create a composite namespace, just add the individual ones) 1074 if (!joinNamespaces.isEmpty()) { 1075 fromClauseNamespaces.addAll(joinNamespaces); 1076 } 1077 1078 return null; // Individual namespaces added directly to fromClauseNamespaces 1079 } 1080 1081 public List<String> getExplicitColumns() { 1082 return new ArrayList<>(explicitColumns); 1083 } 1084 1085 public boolean isRecursive() { 1086 return recursive; 1087 } 1088 1089 @Override 1090 public String toString() { 1091 return String.format("CTENamespace(%s, columns=%d, recursive=%s)", 1092 cteName, 1093 columnSources != null ? columnSources.size() : explicitColumns.size(), 1094 recursive 1095 ); 1096 } 1097}