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