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