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