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