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 /** 126 * Whether this (definition) namespace has already been bound to an outer 127 * FROM-clause reference. See {@link #createReference()} and Mantis #4545. 128 */ 129 private boolean boundToOuterReference = false; 130 131 public boolean isBoundToOuterReference() { 132 return boundToOuterReference; 133 } 134 135 public void setBoundToOuterReference(boolean bound) { 136 this.boundToOuterReference = bound; 137 } 138 139 /** 140 * Create an independent per-reference copy of this CTE namespace. 141 * 142 * <p>The single {@code CTENamespace} registered in the {@code CTEScope} 143 * represents the CTE <em>definition</em>. When the same CTE is referenced 144 * more than once in a FROM clause with different aliases 145 * (e.g. {@code FROM cte a, cte b}), each reference needs its own 146 * {@link #referencingTable} so that {@link #getSourceTable()} reports the 147 * correct alias. Reusing the single definition instance makes every 148 * reference report the <em>last</em> reference's table (Mantis #4545).</p> 149 * 150 * <p>The returned namespace is a fresh, unvalidated instance built from the 151 * same AST ({@code cte}/{@code selectStatement}); its column sources are 152 * rebuilt against itself when {@link #validate()} runs, so resolved columns 153 * trace back to this reference rather than the shared definition.</p> 154 * 155 * @return a new {@code CTENamespace} for an additional FROM-clause reference 156 */ 157 public CTENamespace createReference() { 158 return new CTENamespace(cte, cteName, selectStatement, nameMatcher); 159 } 160 161 @Override 162 public TTable getFinalTable() { 163 // Trace through the CTE's subquery to find the underlying physical table 164 // This is similar to SubqueryNamespace.getFinalTable() but handles CTE chains 165 166 // If this CTE has a UNION subquery, delegate to UnionNamespace 167 if (unionNamespace != null) { 168 TTable unionTable = unionNamespace.getFinalTable(); 169 if (unionTable != null) { 170 return unionTable; 171 } 172 // Fallback to referencing table if UNION has no physical tables 173 return referencingTable; 174 } 175 176 // If no tables in the CTE's SELECT, return the referencing table 177 // This handles CTEs like: WITH cte AS (SELECT 1 AS col) 178 if (selectStatement == null || selectStatement.tables == null || selectStatement.tables.size() == 0) { 179 return referencingTable; 180 } 181 182 // Check for qualified star column (e.g., CTE_NAME.*) first 183 TTable qualifiedStarTable = findTableFromQualifiedStar(); 184 if (qualifiedStarTable != null) { 185 return qualifiedStarTable; 186 } 187 188 // For single-table CTEs, trace to the underlying table 189 TTable firstTable = selectStatement.tables.getTable(0); 190 if (firstTable == null) { 191 return null; 192 } 193 194 // If it's a physical table (not a CTE reference), return it 195 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !firstTable.isCTEName()) { 196 return firstTable; 197 } 198 199 // If it's a CTE reference, trace through the CTE chain 200 if (firstTable.isCTEName() && firstTable.getCTE() != null) { 201 return traceTableThroughCTE(firstTable.getCTE()); 202 } 203 204 // If it's a subquery, trace through it 205 if (firstTable.getSubquery() != null) { 206 SubqueryNamespace nestedNs = new SubqueryNamespace( 207 firstTable.getSubquery(), 208 firstTable.getAliasName(), 209 nameMatcher 210 ); 211 nestedNs.validate(); 212 TTable subTable = nestedNs.getFinalTable(); 213 if (subTable != null) { 214 return subTable; 215 } 216 } 217 218 // If it's a join, get the first base table 219 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 220 TTable joinTable = findFirstPhysicalTableFromJoin(firstTable); 221 if (joinTable != null) { 222 return joinTable; 223 } 224 } 225 226 // Fallback: return the referencing TTable (the CTE reference in FROM clause) 227 // This is used when the CTE doesn't have underlying physical tables, 228 // e.g., WITH cte AS (SELECT 1 AS col) - the columns are literals, not from tables 229 return referencingTable; 230 } 231 232 /** 233 * Find the table referenced by a qualified star column in this CTE's SELECT list. 234 * Example: SELECT other_cte.* FROM other_cte -> traces to other_cte's underlying table 235 */ 236 private TTable findTableFromQualifiedStar() { 237 if (selectStatement == null || selectStatement.getResultColumnList() == null) { 238 return null; 239 } 240 241 TResultColumnList selectList = selectStatement.getResultColumnList(); 242 for (int i = 0; i < selectList.size(); i++) { 243 TResultColumn resultCol = selectList.getResultColumn(i); 244 if (resultCol == null) continue; 245 246 String colStr = resultCol.toString().trim(); 247 // Check if it's a qualified star (contains . before *) 248 if (colStr.endsWith("*") && colStr.contains(".")) { 249 int dotIndex = colStr.lastIndexOf('.'); 250 if (dotIndex > 0) { 251 String tablePrefix = colStr.substring(0, dotIndex).trim(); 252 // Find the table with this alias or name 253 TTable matchingTable = findTableByAliasOrName(tablePrefix); 254 if (matchingTable != null) { 255 // If the matching table is a CTE reference, trace through it 256 if (matchingTable.isCTEName() && matchingTable.getCTE() != null) { 257 return traceTableThroughCTE(matchingTable.getCTE()); 258 } 259 // If it's a subquery, trace through it 260 if (matchingTable.getSubquery() != null) { 261 SubqueryNamespace nestedNs = new SubqueryNamespace( 262 matchingTable.getSubquery(), 263 matchingTable.getAliasName(), 264 nameMatcher 265 ); 266 nestedNs.validate(); 267 return nestedNs.getFinalTable(); 268 } 269 // If it's a physical table, return it 270 if (matchingTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !matchingTable.isCTEName()) { 271 return matchingTable; 272 } 273 } 274 } 275 } 276 } 277 return null; 278 } 279 280 /** 281 * Find a table in the FROM clause by alias or name. 282 */ 283 private TTable findTableByAliasOrName(String nameOrAlias) { 284 if (selectStatement == null || selectStatement.tables == null) { 285 return null; 286 } 287 288 for (int i = 0; i < selectStatement.tables.size(); i++) { 289 TTable table = selectStatement.tables.getTable(i); 290 if (table == null) continue; 291 292 // Check alias 293 String alias = table.getAliasName(); 294 if (alias != null && nameMatcher.matches(alias, nameOrAlias)) { 295 return table; 296 } 297 298 // Check table name 299 if (table.getTableName() != null && nameMatcher.matches(table.getTableName().toString(), nameOrAlias)) { 300 return table; 301 } 302 } 303 return null; 304 } 305 306 /** 307 * Trace through a CTE to find its underlying physical table. 308 * This handles CTE chains like: CTE1 -> CTE2 -> CTE3 -> physical_table 309 */ 310 private TTable traceTableThroughCTE(TCTE cteNode) { 311 return traceTableThroughCTE(cteNode, new HashSet<TCTE>()); 312 } 313 314 private TTable traceTableThroughCTE(TCTE cteNode, java.util.Set<TCTE> visited) { 315 if (cteNode == null || cteNode.getSubquery() == null) { 316 return null; 317 } 318 319 // Detect circular CTE references 320 if (!visited.add(cteNode)) { 321 return null; 322 } 323 324 TSelectSqlStatement cteSubquery = cteNode.getSubquery(); 325 326 // Handle UNION in the CTE 327 if (cteSubquery.isCombinedQuery()) { 328 // For UNION, trace the left branch 329 TSelectSqlStatement leftStmt = cteSubquery.getLeftStmt(); 330 if (leftStmt != null && leftStmt.tables != null && leftStmt.tables.size() > 0) { 331 cteSubquery = leftStmt; 332 } 333 } 334 335 if (cteSubquery.tables == null || cteSubquery.tables.size() == 0) { 336 return null; 337 } 338 339 TTable firstTable = cteSubquery.tables.getTable(0); 340 if (firstTable == null) { 341 return null; 342 } 343 344 // If it's a physical table (not CTE), we found it 345 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !firstTable.isCTEName()) { 346 return firstTable; 347 } 348 349 // If it's another CTE reference, continue tracing 350 if (firstTable.isCTEName() && firstTable.getCTE() != null) { 351 return traceTableThroughCTE(firstTable.getCTE(), visited); 352 } 353 354 // If it's a subquery, trace through it 355 if (firstTable.getSubquery() != null) { 356 SubqueryNamespace nestedNs = new SubqueryNamespace( 357 firstTable.getSubquery(), 358 firstTable.getAliasName(), 359 nameMatcher 360 ); 361 nestedNs.validate(); 362 return nestedNs.getFinalTable(); 363 } 364 365 // If it's a join, get the first base table 366 if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 367 return findFirstPhysicalTableFromJoin(firstTable); 368 } 369 370 return null; 371 } 372 373 /** 374 * Find the first physical table from a JOIN expression. 375 */ 376 private TTable findFirstPhysicalTableFromJoin(TTable joinTable) { 377 if (joinTable == null || joinTable.getJoinExpr() == null) { 378 return null; 379 } 380 381 gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = joinTable.getJoinExpr(); 382 383 // Check left side first 384 TTable leftTable = joinExpr.getLeftTable(); 385 if (leftTable != null) { 386 if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !leftTable.isCTEName()) { 387 return leftTable; 388 } 389 if (leftTable.isCTEName() && leftTable.getCTE() != null) { 390 TTable traced = traceTableThroughCTE(leftTable.getCTE()); 391 if (traced != null) return traced; 392 } 393 if (leftTable.getSubquery() != null) { 394 SubqueryNamespace nestedNs = new SubqueryNamespace( 395 leftTable.getSubquery(), 396 leftTable.getAliasName(), 397 nameMatcher 398 ); 399 nestedNs.validate(); 400 return nestedNs.getFinalTable(); 401 } 402 if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 403 return findFirstPhysicalTableFromJoin(leftTable); 404 } 405 } 406 407 // Check right side 408 TTable rightTable = joinExpr.getRightTable(); 409 if (rightTable != null) { 410 if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !rightTable.isCTEName()) { 411 return rightTable; 412 } 413 if (rightTable.isCTEName() && rightTable.getCTE() != null) { 414 return traceTableThroughCTE(rightTable.getCTE()); 415 } 416 } 417 418 return null; 419 } 420 421 @Override 422 public List<TTable> getAllFinalTables() { 423 // If this CTE has a UNION subquery, delegate to the UnionNamespace 424 if (unionNamespace != null) { 425 return unionNamespace.getAllFinalTables(); 426 } 427 428 // Check if this CTE references another CTE (which might be a UNION) 429 if (selectStatement != null && selectStatement.tables != null && selectStatement.tables.size() > 0) { 430 TTable firstTable = selectStatement.tables.getTable(0); 431 if (firstTable != null && firstTable.isCTEName() && firstTable.getCTE() != null) { 432 TCTE referencedCTE = firstTable.getCTE(); 433 if (referencedCTE.getSubquery() != null) { 434 // Create a namespace for the referenced CTE to get its tables 435 CTENamespace referencedNs = new CTENamespace( 436 referencedCTE, 437 referencedCTE.getTableName() != null ? referencedCTE.getTableName().toString() : "cte", 438 referencedCTE.getSubquery(), 439 nameMatcher 440 ); 441 referencedNs.validate(); 442 // This will trace through the CTE chain to get all tables 443 // including from UNION branches 444 return referencedNs.getAllFinalTables(); 445 } 446 } 447 } 448 449 // For non-UNION, non-CTE-reference CTEs, return the single final table 450 TTable finalTable = getFinalTable(); 451 if (finalTable != null) { 452 return Collections.singletonList(finalTable); 453 } 454 455 return Collections.emptyList(); 456 } 457 458 @Override 459 protected void doValidate() { 460 columnSources = new LinkedHashMap<>(); 461 462 if (selectStatement == null || selectStatement.getResultColumnList() == null) { 463 return; 464 } 465 466 TResultColumnList selectList = selectStatement.getResultColumnList(); 467 468 // If CTE has explicit column list, use it 469 if (!explicitColumns.isEmpty()) { 470 validateWithExplicitColumns(selectList); 471 } else { 472 // No explicit columns, derive from SELECT list 473 validateWithImplicitColumns(selectList); 474 } 475 } 476 477 /** 478 * Validate CTE with explicit column list. 479 * Example: WITH cte(c1, c2, c3) AS (SELECT a, b, c FROM t) 480 * 481 * Two cases are handled: 482 * 483 * 1. **Position-based (Snowflake pattern)**: CTE explicit column list + SELECT * 484 * Example: WITH cte(c1, c2, c3) AS (SELECT * FROM Employees) 485 * - c1/c2/c3 are positional aliases for star expansion 486 * - Without metadata: c1 -> Employees.*, c2 -> Employees.*, c3 -> Employees.* 487 * - With metadata: c1 -> Employees.<col_1>, c2 -> Employees.<col_2>, etc. 488 * 489 * 2. **Direct mapping**: CTE explicit column list + named columns 490 * Example: WITH cte(c1, c2) AS (SELECT id, name FROM t) 491 * - c1 -> t.id, c2 -> t.name (1:1 positional mapping) 492 * 493 * @see <a href="star_column_pushdown.md#cte-explicit-column-list--select--snowflake-case"> 494 * Documentation: CTE Explicit Column List + SELECT *</a> 495 */ 496 private void validateWithExplicitColumns(TResultColumnList selectList) { 497 // Check if the SELECT list contains only star column(s) - the position-based pattern 498 StarColumnInfo starInfo = analyzeStarColumns(selectList); 499 500 if (starInfo.isSingleStar()) { 501 // Position-based case: CTE(c1,c2,c3) AS (SELECT * FROM t) 502 // The column names c1/c2/c3 are positional aliases, not real column names 503 handleExplicitColumnsWithStar(starInfo.getStarColumn(), starInfo.getStarQualifier()); 504 } else { 505 // Direct mapping case: CTE(c1,c2) AS (SELECT id, name FROM t) 506 // Each explicit column maps to corresponding SELECT list item by position 507 handleExplicitColumnsWithDirectMapping(selectList); 508 } 509 } 510 511 /** 512 * Handle CTE explicit columns when SELECT list is a star. 513 * This is the position-based (Snowflake) pattern. 514 * 515 * @param starColumn the star column (* or table.*) 516 * @param starQualifier the table qualifier if qualified star (e.g., "src" for "src.*"), or null 517 */ 518 private void handleExplicitColumnsWithStar(TResultColumn starColumn, String starQualifier) { 519 // Try ordinal mapping if metadata is available 520 List<String> ordinalColumns = tryOrdinalMapping(starQualifier); 521 522 if (ordinalColumns != null && ordinalColumns.size() >= explicitColumns.size()) { 523 // Metadata available - use ordinal mapping: c1 -> Employees.<col_1> 524 for (int i = 0; i < explicitColumns.size(); i++) { 525 String cteColName = explicitColumns.get(i); 526 String baseColName = ordinalColumns.get(i); 527 528 ColumnSource source = new ColumnSource( 529 this, 530 cteColName, 531 starColumn, // Reference to star column 532 1.0, // High confidence - ordinal mapping from metadata 533 "cte_explicit_column_ordinal:" + baseColName 534 ); 535 columnSources.put(cteColName, source); 536 } 537 } else { 538 // No metadata - fallback to star reference: c1 -> Employees.* 539 for (String colName : explicitColumns) { 540 ColumnSource source = new ColumnSource( 541 this, 542 colName, 543 starColumn, // Reference to star column 544 0.8, // Lower confidence - ordinal mapping unknown 545 "cte_explicit_column_via_star" 546 ); 547 columnSources.put(colName, source); 548 } 549 } 550 } 551 552 /** 553 * Handle CTE explicit columns with direct positional mapping to SELECT list. 554 * 555 * @param selectList the SELECT list to map from 556 */ 557 private void handleExplicitColumnsWithDirectMapping(TResultColumnList selectList) { 558 int columnCount = Math.min(explicitColumns.size(), selectList.size()); 559 560 for (int i = 0; i < columnCount; i++) { 561 String colName = explicitColumns.get(i); 562 TResultColumn resultCol = selectList.getResultColumn(i); 563 564 ColumnSource source = new ColumnSource( 565 this, 566 colName, 567 resultCol, 568 1.0, // Definite - direct positional mapping 569 "cte_explicit_column" 570 ); 571 572 columnSources.put(colName, source); 573 } 574 } 575 576 /** 577 * Try to get ordered column names from metadata for ordinal mapping. 578 * 579 * @param starQualifier the table qualifier (e.g., "src"), or null for unqualified star 580 * @return ordered list of column names from metadata, or null if not available 581 */ 582 private List<String> tryOrdinalMapping(String starQualifier) { 583 // TODO: When metadata (TSQLEnv/DDL) is available, return ordered column list 584 // For now, return null to use the fallback (star reference) 585 // 586 // Future implementation: 587 // 1. Find the source table namespace by starQualifier 588 // 2. Get its column sources (which use LinkedHashMap for insertion order) 589 // 3. Return the column names in order 590 return null; 591 } 592 593 /** 594 * Analyze star columns in the SELECT list. 595 * Determines if the SELECT is a single star column pattern. 596 */ 597 private StarColumnInfo analyzeStarColumns(TResultColumnList selectList) { 598 if (selectList == null || selectList.size() == 0) { 599 return new StarColumnInfo(); 600 } 601 602 // Check for single star column pattern 603 if (selectList.size() == 1) { 604 TResultColumn rc = selectList.getResultColumn(0); 605 if (isStarColumn(rc)) { 606 String qualifier = getStarQualifier(rc); 607 return new StarColumnInfo(rc, qualifier); 608 } 609 } 610 611 return new StarColumnInfo(); 612 } 613 614 /** 615 * Check if a result column is a star column (* or table.*) 616 */ 617 private boolean isStarColumn(TResultColumn rc) { 618 if (rc == null) { 619 return false; 620 } 621 String str = rc.toString(); 622 return str != null && (str.equals("*") || str.endsWith(".*")); 623 } 624 625 /** 626 * Get the qualifier from a qualified star (src.* returns "src") 627 */ 628 private String getStarQualifier(TResultColumn rc) { 629 if (rc == null) { 630 return null; 631 } 632 String str = rc.toString(); 633 if (str != null && str.endsWith(".*") && str.length() > 2) { 634 return str.substring(0, str.length() - 2); 635 } 636 return null; 637 } 638 639 /** 640 * Helper class to hold star column analysis results. 641 */ 642 private static class StarColumnInfo { 643 private final TResultColumn starColumn; 644 private final String starQualifier; 645 646 StarColumnInfo() { 647 this.starColumn = null; 648 this.starQualifier = null; 649 } 650 651 StarColumnInfo(TResultColumn starColumn, String starQualifier) { 652 this.starColumn = starColumn; 653 this.starQualifier = starQualifier; 654 } 655 656 boolean isSingleStar() { 657 return starColumn != null; 658 } 659 660 TResultColumn getStarColumn() { 661 return starColumn; 662 } 663 664 String getStarQualifier() { 665 return starQualifier; 666 } 667 } 668 669 /** 670 * Validate CTE without explicit column list. 671 * Example: WITH cte AS (SELECT id, name FROM users) 672 */ 673 private void validateWithImplicitColumns(TResultColumnList selectList) { 674 for (int i = 0; i < selectList.size(); i++) { 675 TResultColumn resultCol = selectList.getResultColumn(i); 676 677 // Determine column name 678 String colName = getColumnName(resultCol); 679 if (colName == null) { 680 colName = "col_" + (i + 1); 681 } 682 683 // Create column source 684 ColumnSource source = new ColumnSource( 685 this, 686 colName, 687 resultCol, 688 1.0, // Definite - from SELECT list 689 "cte_implicit_column" 690 ); 691 692 columnSources.put(colName, source); 693 } 694 } 695 696 /** 697 * Extract column name from TResultColumn 698 */ 699 private String getColumnName(TResultColumn resultCol) { 700 // Check for alias 701 if (resultCol.getAliasClause() != null && 702 resultCol.getAliasClause().getAliasName() != null) { 703 return resultCol.getAliasClause().getAliasName().toString(); 704 } 705 706 // Check for simple column reference 707 if (resultCol.getExpr() != null) { 708 gudusoft.gsqlparser.nodes.TExpression expr = resultCol.getExpr(); 709 if (expr.getExpressionType() == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) { 710 TObjectName objName = expr.getObjectOperand(); 711 if (objName != null) { 712 return objName.getColumnNameOnly(); 713 } 714 } 715 } 716 717 return null; 718 } 719 720 /** 721 * Extract explicit column list from CTE 722 */ 723 private List<String> extractExplicitColumns(TCTE cte) { 724 List<String> columns = new ArrayList<>(); 725 726 if (cte != null && cte.getColumnList() != null) { 727 for (int i = 0; i < cte.getColumnList().size(); i++) { 728 TObjectName colName = cte.getColumnList().getObjectName(i); 729 if (colName != null) { 730 columns.add(colName.toString()); 731 } 732 } 733 } 734 735 return columns; 736 } 737 738 /** 739 * Check if this is a recursive CTE 740 */ 741 private boolean isRecursiveCTE(TCTE cte) { 742 if (cte == null) { 743 return false; 744 } 745 return cte.isRecursive(); 746 } 747 748 public TCTE getCTE() { 749 return cte; 750 } 751 752 @Override 753 public TSelectSqlStatement getSelectStatement() { 754 return selectStatement; 755 } 756 757 @Override 758 public boolean hasStarColumn() { 759 // If this CTE has a UNION subquery, delegate to the UnionNamespace 760 if (unionNamespace != null) { 761 return unionNamespace.hasStarColumn(); 762 } 763 764 if (selectStatement == null || selectStatement.getResultColumnList() == null) { 765 return false; 766 } 767 768 TResultColumnList selectList = selectStatement.getResultColumnList(); 769 for (int i = 0; i < selectList.size(); i++) { 770 TResultColumn resultCol = selectList.getResultColumn(i); 771 if (resultCol != null && resultCol.toString().endsWith("*")) { 772 return true; 773 } 774 } 775 return false; 776 } 777 778 @Override 779 public boolean supportsDynamicInference() { 780 return hasStarColumn(); 781 } 782 783 /** 784 * Slice S4 (plan §5.5): a CTE's derived schema is authoritative once the 785 * CTE has validated and produced at least one named column source. CTEs 786 * with explicit column lists are always FOUND once validated; CTEs that 787 * simply propagate {@code SELECT *} produce derived columns from the 788 * underlying namespace and only become FOUND when those columns are 789 * known. Recursive CTEs and unresolved cyclic references stay 790 * METADATA_UNAVAILABLE — S9 will refine this contract. 791 */ 792 @Override 793 public MetadataState getMetadataState() { 794 ensureValidated(); 795 if (columnSources != null && !columnSources.isEmpty()) { 796 return MetadataState.FOUND; 797 } 798 return MetadataState.METADATA_UNAVAILABLE; 799 } 800 801 @Override 802 public boolean addInferredColumn(String columnName, double confidence, String evidence) { 803 if (columnName == null || columnName.isEmpty()) { 804 return false; 805 } 806 807 // Initialize maps if needed 808 if (inferredColumns == null) { 809 inferredColumns = new LinkedHashMap<>(); 810 } 811 if (inferredColumnNames == null) { 812 inferredColumnNames = new HashSet<>(); 813 } 814 815 // Slice S1: dedupe via matcher-aware helper so per-vendor identifier 816 // rules govern collision detection. Without this, BigQuery / MySQL 817 // (case-insensitive columns) accept "MyCol" and "MYCOL" as two entries 818 // and downstream lookups become non-deterministic. Codex round 2: 819 // the storage key is the raw (exposedName) form so two matcher-distinct 820 // identifiers that happen to normalize equally (e.g. Postgres quoted 821 // "mycol" vs unquoted MYCOL) keep separate entries. 822 if (containsColumnByMatcher(columnSources, columnName)) { 823 return false; 824 } 825 if (containsColumnByMatcher(inferredColumns, columnName)) { 826 return false; 827 } 828 829 // Collect candidate tables - get ALL final tables from the CTE chain 830 // This handles both UNION CTEs and CTEs that reference other CTEs 831 java.util.List<TTable> candidateTables = new java.util.ArrayList<>(); 832 833 // Get all final tables from this CTE's namespace (handles UNION and CTE chains) 834 java.util.List<TTable> allTables = this.getAllFinalTables(); 835 for (TTable table : allTables) { 836 if (table != null && !candidateTables.contains(table)) { 837 candidateTables.add(table); 838 } 839 } 840 841 // Create inferred column source WITH candidate tables if applicable 842 ColumnSource source = new ColumnSource( 843 this, 844 columnName, 845 null, 846 confidence, 847 evidence, 848 null, // overrideTable 849 (candidateTables != null && !candidateTables.isEmpty()) ? candidateTables : null 850 ); 851 852 inferredColumns.put(columnName, source); 853 inferredColumnNames.add(columnName); 854 855 // Propagate to nested namespaces if this CTE has SELECT * from subqueries/unions 856 propagateToNestedNamespaces(columnName, confidence, evidence); 857 858 // NOTE: Propagation to referenced CTEs (CTE chains like cte2 -> cte1) is handled 859 // by NamespaceEnhancer.propagateThroughCTEChains() which has access to the actual 860 // namespace instances from the scope tree. We don't do it here because creating 861 // new CTENamespace instances would not affect the actual instances used for resolution. 862 863 return true; 864 } 865 866 /** 867 * Propagate an inferred column to nested namespaces. 868 * 869 * This is a unified algorithm that handles: 870 * 1. Direct UNION subqueries (CTE body is a UNION) 871 * 2. SELECT * FROM (UNION) patterns 872 * 3. SELECT * FROM (subquery) patterns 873 * 4. Deeply nested structures with JOINs 874 * 875 * The propagation is recursive - each namespace that receives the column 876 * will further propagate to its own nested namespaces. 877 * 878 * @param columnName The column name to propagate 879 * @param confidence Confidence score 880 * @param evidence Evidence string for debugging 881 */ 882 private void propagateToNestedNamespaces(String columnName, double confidence, String evidence) { 883 // Case 1: Direct UNION subquery (CTE body is a UNION) 884 if (unionNamespace != null) { 885 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 886 System.out.println("[CTENamespace] Propagating '" + columnName + "' to direct unionNamespace in " + cteName); 887 } 888 unionNamespace.addInferredColumn(columnName, confidence, evidence + "_cte_union_propagate"); 889 return; 890 } 891 892 // Case 2: CTE has SELECT * from nested structures (subqueries, unions in FROM clause) 893 // Only propagate if the CTE's SELECT list contains a star column 894 if (!hasStarColumn()) { 895 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 896 System.out.println("[CTENamespace] No star column in " + cteName + ", skipping FROM clause propagation"); 897 } 898 return; 899 } 900 901 // Get or create namespaces for FROM clause tables 902 List<INamespace> fromNamespaces = getOrCreateFromClauseNamespaces(); 903 if (fromNamespaces.isEmpty()) { 904 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 905 System.out.println("[CTENamespace] No FROM clause namespaces with dynamic inference in " + cteName); 906 } 907 return; 908 } 909 910 // Propagate to each FROM clause namespace 911 for (INamespace ns : fromNamespaces) { 912 if (ns.supportsDynamicInference()) { 913 if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 914 System.out.println("[CTENamespace] Propagating '" + columnName + "' to FROM clause namespace " + 915 ns.getDisplayName() + " in " + cteName); 916 } 917 // The nested namespace's addInferredColumn will recursively propagate further 918 ns.addInferredColumn(columnName, confidence, evidence + "_cte_from_propagate"); 919 } 920 } 921 } 922 923 @Override 924 public Set<String> getInferredColumns() { 925 if (inferredColumnNames == null) { 926 return Collections.emptySet(); 927 } 928 return Collections.unmodifiableSet(inferredColumnNames); 929 } 930 931 /** 932 * Binding-diagnostic view of the CTE output schema. 933 * 934 * <p>This deliberately differs from {@link #hasColumn(String)}, which has 935 * legacy compatibility fallbacks that may infer hidden base-table columns 936 * for lineage. Binding diagnostics need the SQL-visible CTE projection: 937 * explicit CTE column names, named projection columns, and star/pushdown 938 * inferred output columns are visible; hidden base-table inferences are 939 * not.</p> 940 */ 941 public ColumnLevel hasAuthoritativeOutputColumn(String columnName) { 942 ensureValidated(); 943 944 if (columnName == null || columnName.isEmpty()) { 945 return ColumnLevel.MAYBE; 946 } 947 948 if (containsColumnByMatcher(columnSources, columnName)) { 949 return ColumnLevel.EXISTS; 950 } 951 952 if (!explicitColumns.isEmpty()) { 953 return ColumnLevel.NOT_EXISTS; 954 } 955 956 if (containsAuthoritativeInferredColumn(columnName)) { 957 return ColumnLevel.EXISTS; 958 } 959 960 if (hasStarColumn()) { 961 return ColumnLevel.MAYBE; 962 } 963 964 if (columnSources != null && !columnSources.isEmpty()) { 965 return ColumnLevel.NOT_EXISTS; 966 } 967 968 return ColumnLevel.MAYBE; 969 } 970 971 /** 972 * Diagnostic-only sibling of the matcher-aware contains check that 973 * additionally filters out the legacy {@code inferred_from_cte_base_table} 974 * entries (which represent hidden base-table columns, not the CTE's 975 * SQL-visible projection). Matches must still respect per-vendor identifier 976 * rules so quoted Oracle / Postgres identifiers don't get folded. 977 */ 978 private boolean containsAuthoritativeInferredColumn(String columnName) { 979 if (inferredColumns == null || inferredColumns.isEmpty()) { 980 return false; 981 } 982 for (ColumnSource candidate : inferredColumns.values()) { 983 // Codex round 1: compare against the exposedName (original case, 984 // quotes preserved) instead of the normalized key, so quoted 985 // vs unquoted distinctions are honored on Oracle / Postgres / 986 // Snowflake / Hive / Teradata. 987 String exposed = candidate != null ? candidate.getExposedName() : null; 988 if (exposed == null || !nameMatcher.matches(exposed, columnName)) { 989 continue; 990 } 991 String evidence = candidate.getEvidence(); 992 if ("inferred_from_cte_base_table".equals(evidence)) { 993 continue; 994 } 995 return true; 996 } 997 return false; 998 } 999 1000 @Override 1001 public ColumnLevel hasColumn(String columnName) { 1002 ensureValidated(); 1003 1004 // Check in explicit columns 1005 if (containsColumnByMatcher(columnSources, columnName)) { 1006 return ColumnLevel.EXISTS; 1007 } 1008 1009 // Check in inferred columns. The map is raw-keyed (= ColumnSource. 1010 // exposedName); the matcher-aware helper applies per-dialect rules 1011 // including SQL Server COLLATION_BASED. Round 2 reverted from 1012 // normalized keys because two matcher-distinct identifiers can 1013 // normalize to the same key (Postgres "mycol" vs MYCOL). 1014 if (containsColumnByMatcher(inferredColumns, columnName)) { 1015 return ColumnLevel.EXISTS; 1016 } 1017 1018 // If has star column, unknown columns MAYBE exist 1019 if (hasStarColumn()) { 1020 return ColumnLevel.MAYBE; 1021 } 1022 1023 // If the CTE has explicit column definitions like "cte(c1, c2, c3)", then ONLY 1024 // those columns exist - don't return MAYBE for other columns. 1025 // This prevents ambiguous resolution when a CTE with explicit columns is joined 1026 // with another table. 1027 if (!explicitColumns.isEmpty()) { 1028 return ColumnLevel.NOT_EXISTS; 1029 } 1030 1031 // For CTEs without explicit columns AND without star columns, check if underlying 1032 // tables might have the column. This handles cases like referencing columns from 1033 // the CTE's base tables that aren't explicitly selected in the CTE's SELECT list. 1034 if (selectStatement != null && selectStatement.tables != null) { 1035 for (int i = 0; i < selectStatement.tables.size(); i++) { 1036 TTable table = selectStatement.tables.getTable(i); 1037 if (table != null && table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1038 // The CTE has a base table - column might exist there 1039 return ColumnLevel.MAYBE; 1040 } 1041 } 1042 } 1043 1044 return ColumnLevel.NOT_EXISTS; 1045 } 1046 1047 @Override 1048 public ColumnSource resolveColumn(String columnName) { 1049 ensureValidated(); 1050 1051 // First check explicit columns 1052 ColumnSource source = super.resolveColumn(columnName); 1053 if (source != null) { 1054 return source; 1055 } 1056 1057 // Then check inferred columns. Slice S1 + codex round 2: the map is 1058 // raw-keyed (= ColumnSource.exposedName), so the exact-match probe 1059 // is O(1) for the same identifier queried again. For case-only- 1060 // different references, the matcher loop walks values via 1061 // getExposedName() so quote state is preserved on quoted-sensitive 1062 // dialects (Oracle / Postgres / Snowflake / Hive / Teradata). 1063 if (inferredColumns != null) { 1064 ColumnSource exact = inferredColumns.get(columnName); 1065 if (exact != null) { 1066 return exact; 1067 } 1068 for (ColumnSource entry : inferredColumns.values()) { 1069 String exposed = entry != null ? entry.getExposedName() : null; 1070 if (exposed != null && nameMatcher.matches(exposed, columnName)) { 1071 return entry; 1072 } 1073 } 1074 } 1075 1076 // If has star column, auto-infer this column 1077 if (hasStarColumn()) { 1078 boolean added = addInferredColumn(columnName, 0.8, "auto_inferred_from_reference"); 1079 if (added && inferredColumns != null) { 1080 return inferredColumns.get(columnName); 1081 } 1082 } 1083 1084 // For CTEs without star columns, check if underlying base tables might have the column. 1085 // This handles references to columns that aren't explicitly selected in the CTE's SELECT list. 1086 if (selectStatement != null && selectStatement.tables != null) { 1087 for (int i = 0; i < selectStatement.tables.size(); i++) { 1088 TTable table = selectStatement.tables.getTable(i); 1089 if (table != null && table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) { 1090 // Create an inferred column source that traces to the base table 1091 boolean added = addInferredColumn(columnName, 0.6, "inferred_from_cte_base_table"); 1092 if (added && inferredColumns != null) { 1093 return inferredColumns.get(columnName); 1094 } 1095 break; 1096 } 1097 } 1098 } 1099 1100 return null; 1101 } 1102 1103 /** 1104 * Get the UnionNamespace if this CTE's subquery is a UNION. 1105 */ 1106 public UnionNamespace getUnionNamespace() { 1107 return unionNamespace; 1108 } 1109 1110 /** 1111 * Get or create namespaces for FROM clause tables that support dynamic inference. 1112 * This handles cases like: WITH cte AS (SELECT * FROM (UNION) sub) 1113 * where the CTE body is not directly a UNION but contains a subquery with UNION. 1114 * 1115 * The namespaces are lazily created and cached for reuse. 1116 * 1117 * @return List of namespaces that support dynamic inference (may be empty) 1118 */ 1119 private List<INamespace> getOrCreateFromClauseNamespaces() { 1120 if (fromClauseNamespaces != null) { 1121 return fromClauseNamespaces; 1122 } 1123 1124 fromClauseNamespaces = new ArrayList<>(); 1125 1126 if (selectStatement == null || selectStatement.tables == null) { 1127 return fromClauseNamespaces; 1128 } 1129 1130 // Iterate through FROM clause tables and create namespaces for those that 1131 // could have star columns (subqueries, unions, CTE references) 1132 for (int i = 0; i < selectStatement.tables.size(); i++) { 1133 TTable table = selectStatement.tables.getTable(i); 1134 if (table == null) continue; 1135 1136 INamespace ns = createNamespaceForTable(table); 1137 if (ns != null && ns.supportsDynamicInference()) { 1138 fromClauseNamespaces.add(ns); 1139 } 1140 } 1141 1142 return fromClauseNamespaces; 1143 } 1144 1145 /** 1146 * Create an appropriate namespace for a table in the FROM clause. 1147 * Handles subqueries (including UNION), CTE references, and joins recursively. 1148 * 1149 * @param table The table from the FROM clause 1150 * @return INamespace for the table, or null if not applicable 1151 */ 1152 private INamespace createNamespaceForTable(TTable table) { 1153 if (table == null) return null; 1154 1155 // Handle subquery tables 1156 if (table.getSubquery() != null) { 1157 TSelectSqlStatement subquery = table.getSubquery(); 1158 String alias = table.getAliasName(); 1159 1160 // Check if subquery is a UNION/INTERSECT/EXCEPT 1161 if (subquery.isCombinedQuery()) { 1162 UnionNamespace unionNs = new UnionNamespace(subquery, alias, nameMatcher); 1163 return unionNs; 1164 } else { 1165 // Regular subquery - create SubqueryNamespace 1166 SubqueryNamespace subNs = new SubqueryNamespace(subquery, alias, nameMatcher); 1167 subNs.validate(); 1168 return subNs; 1169 } 1170 } 1171 1172 // Handle CTE references - these are handled by NamespaceEnhancer.propagateThroughCTEChains() 1173 // We don't create new CTENamespace here because we need the actual instances from scope tree 1174 1175 // Handle JOIN tables - recursively collect from join expressions 1176 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) { 1177 return createNamespaceForJoin(table); 1178 } 1179 1180 return null; 1181 } 1182 1183 /** 1184 * Create namespaces for tables within a JOIN expression. 1185 * Returns a composite namespace that wraps all namespaces from the join. 1186 * 1187 * @param joinTable The JOIN table 1188 * @return INamespace that wraps join namespaces, or null 1189 */ 1190 private INamespace createNamespaceForJoin(TTable joinTable) { 1191 if (joinTable == null || joinTable.getJoinExpr() == null) { 1192 return null; 1193 } 1194 1195 gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = joinTable.getJoinExpr(); 1196 1197 // Collect namespaces from both sides of the join 1198 List<INamespace> joinNamespaces = new ArrayList<>(); 1199 1200 // Left side 1201 TTable leftTable = joinExpr.getLeftTable(); 1202 if (leftTable != null) { 1203 INamespace leftNs = createNamespaceForTable(leftTable); 1204 if (leftNs != null && leftNs.supportsDynamicInference()) { 1205 joinNamespaces.add(leftNs); 1206 } 1207 } 1208 1209 // Right side 1210 TTable rightTable = joinExpr.getRightTable(); 1211 if (rightTable != null) { 1212 INamespace rightNs = createNamespaceForTable(rightTable); 1213 if (rightNs != null && rightNs.supportsDynamicInference()) { 1214 joinNamespaces.add(rightNs); 1215 } 1216 } 1217 1218 // If we found namespaces, add them to fromClauseNamespaces directly 1219 // (we don't create a composite namespace, just add the individual ones) 1220 if (!joinNamespaces.isEmpty()) { 1221 fromClauseNamespaces.addAll(joinNamespaces); 1222 } 1223 1224 return null; // Individual namespaces added directly to fromClauseNamespaces 1225 } 1226 1227 public List<String> getExplicitColumns() { 1228 return new ArrayList<>(explicitColumns); 1229 } 1230 1231 public boolean isRecursive() { 1232 return recursive; 1233 } 1234 1235 @Override 1236 public String toString() { 1237 return String.format("CTENamespace(%s, columns=%d, recursive=%s)", 1238 cteName, 1239 columnSources != null ? columnSources.size() : explicitColumns.size(), 1240 recursive 1241 ); 1242 } 1243}