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