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