001package gudusoft.gsqlparser.resolver2.model;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.EExpressionType;
005import gudusoft.gsqlparser.nodes.TExpression;
006import gudusoft.gsqlparser.nodes.TObjectName;
007import gudusoft.gsqlparser.nodes.TParseTreeNode;
008import gudusoft.gsqlparser.nodes.TResultColumn;
009import gudusoft.gsqlparser.nodes.TTable;
010import gudusoft.gsqlparser.resolver2.inference.EvidenceType;
011import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
012import gudusoft.gsqlparser.resolver2.matcher.VendorNameMatcher;
013import gudusoft.gsqlparser.resolver2.namespace.AbstractNamespace;
014import gudusoft.gsqlparser.resolver2.namespace.INamespace;
015import gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace;
016import gudusoft.gsqlparser.resolver2.namespace.CTENamespace;
017import gudusoft.gsqlparser.resolver2.namespace.UnionNamespace;
018import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
019import gudusoft.gsqlparser.sqlenv.IdentifierService;
020import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
021
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.IdentityHashMap;
025import java.util.List;
026import java.util.Set;
027
028/**
029 * Represents the source of a column reference.
030 * Tracks where a column comes from, including intermediate transformations
031 * through subqueries and CTEs.
032 *
033 * Design principles:
034 * 1. Immutable - once created, cannot be modified
035 * 2. Recursive - can trace back through subquery/CTE layers
036 * 3. Confidence-scored - supports evidence-based inference
037 */
038public class ColumnSource {
039    /** The namespace where this column is exposed (e.g., subquery, table) */
040    private final INamespace sourceNamespace;
041
042    /** The name by which this column is exposed in the namespace */
043    private final String exposedName;
044
045    /** The AST node where this column is defined (TResultColumn, TTableColumn, etc.) */
046    private final TParseTreeNode definitionNode;
047
048    /** Location information for the definition */
049    private final SourceLocation definitionLocation;
050
051    /**
052     * Confidence score [0.0, 1.0]:
053     * - 1.0: Definite (from metadata or explicit definition)
054     * - 0.7-0.9: High confidence inference (strong evidence)
055     * - 0.5-0.7: Medium confidence inference (some evidence)
056     * - 0.0-0.5: Low confidence guess
057     */
058    private final double confidence;
059
060    /**
061     * Evidence that supports this resolution.
062     * Used for debugging and explaining inference decisions.
063     *
064     * @deprecated Use {@link #evidenceDetail} instead. This field is kept for backward
065     *             compatibility and will be derived from evidenceDetail if not explicitly set.
066     */
067    private final String evidence;
068
069    /**
070     * Structured evidence detail for this resolution.
071     * Provides type-safe evidence with confidence weight and source traceability.
072     * This is the preferred way to access resolution evidence.
073     *
074     * @see ResolutionEvidence
075     */
076    private final ResolutionEvidence evidenceDetail;
077
078    /**
079     * Override table for traced columns.
080     * When set, getFinalTable() returns this instead of namespace's table.
081     */
082    private final TTable overrideTable;
083
084    /**
085     * Candidate tables for ambiguous columns.
086     * When a column could come from multiple tables (e.g., SELECT * FROM t1, t2),
087     * this list contains all possible source tables so end users can access them.
088     */
089    private final List<TTable> candidateTables;
090
091    /**
092     * Field path for deep/record field access (e.g., struct.field.subfield).
093     *
094     * <p>When a column reference includes field access beyond the base column,
095     * this captures the field path. For example, in {@code customer.address.city},
096     * if base column is {@code customer}, fieldPath contains {@code ["address", "city"]}.</p>
097     *
098     * <p>This field is null or empty for regular column references without field access.</p>
099     *
100     * @see FieldPath
101     */
102    private final FieldPath fieldPath;
103
104    public ColumnSource(INamespace sourceNamespace,
105                       String exposedName,
106                       TParseTreeNode definitionNode,
107                       double confidence,
108                       String evidence) {
109        this(sourceNamespace, exposedName, definitionNode, confidence, evidence, null, null);
110    }
111
112    public ColumnSource(INamespace sourceNamespace,
113                       String exposedName,
114                       TParseTreeNode definitionNode,
115                       double confidence,
116                       String evidence,
117                       TTable overrideTable) {
118        this(sourceNamespace, exposedName, definitionNode, confidence, evidence, overrideTable, null);
119    }
120
121    public ColumnSource(INamespace sourceNamespace,
122                       String exposedName,
123                       TParseTreeNode definitionNode,
124                       double confidence,
125                       String evidence,
126                       TTable overrideTable,
127                       List<TTable> candidateTables) {
128        this(sourceNamespace, exposedName, definitionNode, confidence, evidence, overrideTable, candidateTables, null, null);
129    }
130
131    /**
132     * Full constructor with all fields including ResolutionEvidence.
133     */
134    public ColumnSource(INamespace sourceNamespace,
135                       String exposedName,
136                       TParseTreeNode definitionNode,
137                       double confidence,
138                       String evidence,
139                       TTable overrideTable,
140                       List<TTable> candidateTables,
141                       ResolutionEvidence evidenceDetail) {
142        this(sourceNamespace, exposedName, definitionNode, confidence, evidence, overrideTable, candidateTables, evidenceDetail, null);
143    }
144
145    /**
146     * Full constructor with all fields including ResolutionEvidence and FieldPath.
147     *
148     * @param sourceNamespace The namespace where this column is exposed
149     * @param exposedName The name by which this column is exposed
150     * @param definitionNode The AST node where this column is defined
151     * @param confidence Confidence score [0.0, 1.0]
152     * @param evidence Evidence string for this resolution
153     * @param overrideTable Override table for traced columns
154     * @param candidateTables Candidate tables for ambiguous columns
155     * @param evidenceDetail Structured evidence detail
156     * @param fieldPath Field path for deep/record field access
157     */
158    public ColumnSource(INamespace sourceNamespace,
159                       String exposedName,
160                       TParseTreeNode definitionNode,
161                       double confidence,
162                       String evidence,
163                       TTable overrideTable,
164                       List<TTable> candidateTables,
165                       ResolutionEvidence evidenceDetail,
166                       FieldPath fieldPath) {
167        this.sourceNamespace = sourceNamespace;
168        this.exposedName = exposedName;
169        this.definitionNode = definitionNode;
170        this.definitionLocation = definitionNode != null
171            ? new SourceLocation(definitionNode)
172            : null;
173        this.confidence = Math.max(0.0, Math.min(1.0, confidence));
174        this.evidence = evidence;
175        this.overrideTable = overrideTable;
176        this.candidateTables = candidateTables != null ? Collections.unmodifiableList(candidateTables) : null;
177        this.fieldPath = fieldPath;
178        // If evidenceDetail not provided, create from legacy evidence
179        if (evidenceDetail != null) {
180            this.evidenceDetail = evidenceDetail;
181        } else if (evidence != null) {
182            this.evidenceDetail = ResolutionEvidence.fromLegacyEvidence(evidence, confidence, definitionNode);
183        } else {
184            this.evidenceDetail = null;
185        }
186    }
187
188    /**
189     * Constructor with ResolutionEvidence (preferred for new code).
190     */
191    public ColumnSource(INamespace sourceNamespace,
192                       String exposedName,
193                       TParseTreeNode definitionNode,
194                       ResolutionEvidence evidenceDetail) {
195        this(sourceNamespace, exposedName, definitionNode,
196             evidenceDetail != null ? evidenceDetail.getWeight() : 1.0,
197             evidenceDetail != null ? evidenceDetail.toLegacyEvidence() : "metadata",
198             null, null, evidenceDetail);
199    }
200
201    /**
202     * Constructor with ResolutionEvidence and override table.
203     */
204    public ColumnSource(INamespace sourceNamespace,
205                       String exposedName,
206                       TParseTreeNode definitionNode,
207                       ResolutionEvidence evidenceDetail,
208                       TTable overrideTable) {
209        this(sourceNamespace, exposedName, definitionNode,
210             evidenceDetail != null ? evidenceDetail.getWeight() : 1.0,
211             evidenceDetail != null ? evidenceDetail.toLegacyEvidence() : "metadata",
212             overrideTable, null, evidenceDetail);
213    }
214
215    /**
216     * Constructor for definite matches (confidence = 1.0)
217     */
218    public ColumnSource(INamespace sourceNamespace,
219                       String exposedName,
220                       TParseTreeNode definitionNode) {
221        this(sourceNamespace, exposedName, definitionNode, 1.0, "metadata");
222    }
223
224    public INamespace getSourceNamespace() {
225        return sourceNamespace;
226    }
227
228    public String getExposedName() {
229        return exposedName;
230    }
231
232    public TParseTreeNode getDefinitionNode() {
233        return definitionNode;
234    }
235
236    public SourceLocation getDefinitionLocation() {
237        return definitionLocation;
238    }
239
240    public double getConfidence() {
241        return confidence;
242    }
243
244    public String getEvidence() {
245        return evidence;
246    }
247
248    /**
249     * Get the structured evidence detail for this resolution.
250     *
251     * <p>This is the preferred way to access resolution evidence as it provides:
252     * <ul>
253     *   <li>Type-safe evidence type (enum)</li>
254     *   <li>Confidence weight with clear semantics</li>
255     *   <li>Source location for traceability</li>
256     *   <li>Human-readable messages</li>
257     * </ul>
258     *
259     * @return The structured evidence detail, or null if not available
260     */
261    public ResolutionEvidence getEvidenceDetail() {
262        return evidenceDetail;
263    }
264
265    /**
266     * Get the evidence type from the structured evidence detail.
267     * Convenience method for common use cases.
268     *
269     * @return The evidence type, or null if no evidence detail
270     */
271    public EvidenceType getEvidenceType() {
272        return evidenceDetail != null ? evidenceDetail.getType() : null;
273    }
274
275    /**
276     * Check if this resolution has definite evidence (not inferred).
277     * Definite evidence comes from DDL, metadata, or explicit definitions.
278     *
279     * @return true if evidence is definite
280     */
281    public boolean hasDefiniteEvidence() {
282        if (evidenceDetail != null) {
283            return evidenceDetail.isDefinite();
284        }
285        // Fallback: check legacy evidence and confidence
286        if (confidence >= 1.0) {
287            return true;
288        }
289        if (evidence != null) {
290            String lower = evidence.toLowerCase(java.util.Locale.ROOT);
291            return lower.contains("metadata") || lower.contains("ddl") ||
292                   lower.contains("explicit") || lower.contains("insert_column");
293        }
294        return false;
295    }
296
297    /**
298     * Get the <b>final</b> physical table this column originates from after tracing
299     * through all subqueries and CTEs.
300     *
301     * <h3>Semantic Difference: getFinalTable() vs TObjectName.getSourceTable()</h3>
302     * <ul>
303     *   <li><b>getFinalTable()</b> (this method): The final physical table after
304     *       recursively tracing through all subqueries and CTEs. Use this for data lineage.</li>
305     *   <li><b>TObjectName.getSourceTable()</b>: The immediate source in the current scope.
306     *       For a column from a subquery, this points to the subquery's TTable itself.</li>
307     * </ul>
308     *
309     * <h3>Example</h3>
310     * <pre>{@code
311     * SELECT title FROM (SELECT * FROM books) sub
312     *
313     * For the 'title' column in outer SELECT:
314     * - TObjectName.getSourceTable() → TTable for subquery 'sub' (immediate source)
315     * - ColumnSource.getFinalTable() → TTable for 'books' (final physical table)
316     * }</pre>
317     *
318     * <p>For calculated columns in subqueries (expressions like {@code START_DT - x AS alias}),
319     * this returns null because such calculated columns don't originate from a physical
320     * table - they are derived values computed in the subquery.</p>
321     *
322     * <p>For aliased columns in subqueries (e.g., {@code SELECT t.id AS col1 FROM my_table t}),
323     * this traces through the alias to find the physical table, because the data still
324     * originates from the physical table even though the column has been renamed.</p>
325     *
326     * <p>Note: For CTEs, calculated columns ARE the CTE's own columns, so they trace
327     * to the CTE itself (handled by CTENamespace.getFinalTable()).</p>
328     *
329     * @return The physical table, or null if unable to determine or if calculated in subquery
330     * @see gudusoft.gsqlparser.nodes.TObjectName#getSourceTable()
331     */
332    public TTable getFinalTable() {
333        if (sourceNamespace == null && overrideTable == null) {
334            return null;
335        }
336
337        // For SubqueryNamespace: calculated columns should NOT trace to base table
338        // They are derived values that don't exist in the underlying physical table
339        // Example: SELECT *, expr AS alias FROM table - alias is calculated, not from table
340        //
341        // IMPORTANT: Check BEFORE overrideTable to prevent alias/calculated columns
342        // from being traced to base tables even when overrideTable is explicitly set
343        if (sourceNamespace instanceof SubqueryNamespace && isCalculatedColumn()) {
344            return null;
345        }
346
347        // For CTENamespace: calculated columns ARE the CTE's own columns
348        // They should trace to the CTE itself (referencing table), NOT to underlying base tables
349        // Example: WITH cte AS (SELECT SUM(x) AS total FROM t) SELECT total FROM cte
350        // The 'total' column traces to 'cte', not to 't'
351        if (sourceNamespace instanceof CTENamespace && isCalculatedColumn()) {
352            return ((CTENamespace) sourceNamespace).getReferencingTable();
353        }
354
355        // For SubqueryNamespace: column aliases - trace through to find the source table.
356        // The alias changes the column name but the data still originates from a physical table.
357        // Example: SELECT t.id AS col1 FROM my_table t - col1's data comes from my_table
358        if (sourceNamespace instanceof SubqueryNamespace && isColumnAlias()) {
359            return traceColumnAliasThroughSubquery((SubqueryNamespace) sourceNamespace);
360        }
361
362        // For CTENamespace: column aliases ARE the CTE's own columns
363        // They should trace to the CTE itself, NOT to underlying base tables
364        // Example: WITH cte AS (SELECT x AS y FROM t) SELECT y FROM cte
365        // The 'y' column traces to 'cte', not to 't'
366        if (sourceNamespace instanceof CTENamespace && isColumnAlias()) {
367            return ((CTENamespace) sourceNamespace).getReferencingTable();
368        }
369
370        // For CTENamespace: explicit column names ARE the CTE's own columns
371        // They should trace to the CTE itself, NOT to underlying base tables
372        // Example: WITH cte(c1, c2) AS (SELECT id, name FROM t) SELECT c1 FROM cte
373        // The 'c1' column traces to 'cte', not to 't' (because 'c1' doesn't exist in 't')
374        if (sourceNamespace instanceof CTENamespace && isCTEExplicitColumn()) {
375            return ((CTENamespace) sourceNamespace).getReferencingTable();
376        }
377
378        // For SubqueryNamespace: passthrough columns that reference calculated columns should NOT trace
379        // IMPORTANT: This check must come BEFORE isPassthroughToAlias() because
380        // isPassthroughToAlias() returns true for BOTH alias and calculated passthroughs.
381        // Example: SELECT kko_lfz_9 FROM (SELECT CASE...END AS kko_lfz_9 FROM t) subq
382        // The outer kko_lfz_9 references a calculated column in the subquery
383        if (sourceNamespace instanceof SubqueryNamespace && isPassthroughToCalculatedInSubquery()) {
384            return null;
385        }
386
387        // For SubqueryNamespace: passthrough columns that reference aliases - trace through
388        // to find the source table. The data still originates from a physical table.
389        // Example: SELECT stat_typ FROM (SELECT stat_typ = stellplatz_typ FROM t) AS b
390        // The stat_typ in outer query references b.stat_typ → traces to t
391        if (sourceNamespace instanceof SubqueryNamespace && isPassthroughToAlias()) {
392            return traceColumnAliasThroughSubquery((SubqueryNamespace) sourceNamespace);
393        }
394
395        // For CTENamespace: passthrough columns that reference calculated subquery columns should NOT trace
396        // Example: WITH DataCTE AS (SELECT subq.calc_col FROM (SELECT CASE...END AS calc_col FROM t) subq)
397        // The CTE column calc_col references a calculated column in the subquery
398        if (sourceNamespace instanceof CTENamespace && isPassthroughToCalculatedInCTE()) {
399            return null;
400        }
401
402        // For CTE explicit column + star pattern: c1/c2/c3 trace to the star, NOT through the star
403        // Example: WITH cte(c1, c2, c3) AS (SELECT * FROM Employees)
404        // Without metadata, c1 traces to Employees.* (the star), not Employees.c1 (which doesn't exist)
405        // The evidence "cte_explicit_column_via_star".equals(evidence) indicates this pattern
406        // A derived table's alias column list over a star whose expansion is not known is the same pattern:
407        // FROM (SELECT * FROM t3) d(a) names t3's first column a, and which column that is is not known (Mantis 4843)
408        if ("cte_explicit_column_via_star".equals(evidence) || EVIDENCE_ALIAS_COLUMN_VIA_STAR.equals(evidence)) {
409            return null;
410        }
411
412        // For inferred columns traced through SELECT * across a CTE/subquery chain:
413        // if the exposed name is defined deeper in the chain as a renamed alias of a
414        // simple column (e.g. {@code SELECT t.col AS bug2}), the alias name does NOT
415        // exist as a column in the underlying physical table. Falling through to the
416        // generic tracing would link the alias name to the base table - producing
417        // bogus entries in TTable.getLinkedColumns().
418        if (isInferredAliasThroughChain()) {
419            if (sourceNamespace instanceof CTENamespace) {
420                return ((CTENamespace) sourceNamespace).getReferencingTable();
421            }
422            return null;
423        }
424
425        // If an override table is set (e.g., for traced columns), use it
426        if (overrideTable != null) {
427            return overrideTable;
428        }
429
430        if (sourceNamespace == null) {
431            return null;
432        }
433
434        // For UnionNamespace: UNION columns don't belong to any specific physical table.
435        // They're a combination of multiple branches. UnionNamespace.getFinalTable() returns
436        // the first branch's table which is incorrect for tracking column origins.
437        if (sourceNamespace instanceof UnionNamespace) {
438            return null;
439        }
440
441        // For SubqueryNamespace without override table: if the subquery has multiple tables
442        // AND no qualified star to identify the source, we can't determine which table
443        // the column comes from. Returning the first table would be incorrect.
444        // Example: FROM CDS_H_PARTNER PAR, (SELECT kategorie ... FROM CDS_H_KUNDEN_OBJEKT) subq
445        // But if there's a qualified star like "ta.*", that identifies the source table.
446        //
447        // IMPORTANT: For Teradata, implicit lateral derived tables (auto-added tables when
448        // a column references an undeclared table in WHERE clause) should be excluded from
449        // the multiple-table count. These are syntactic sugar and shouldn't affect column
450        // resolution to the actual source table.
451        if (sourceNamespace instanceof SubqueryNamespace && overrideTable == null) {
452            SubqueryNamespace subNs = (SubqueryNamespace) sourceNamespace;
453            gudusoft.gsqlparser.stmt.TSelectSqlStatement subquery = subNs.getSubquery();
454            if (subquery != null && subquery.tables != null) {
455                // Count only real tables (excluding implicit lateral derived tables)
456                int realTableCount = countRealTables(subquery.tables);
457                if (realTableCount > 1) {
458                    // Check if there's a qualified star that can identify the source
459                    if (!hasQualifiedStar(subquery)) {
460                        return null;
461                    }
462                }
463            }
464        }
465
466        // For CTENamespace with multiple tables (e.g., JOIN): trace the specific column
467        // to its correct source table using the definitionNode
468        // Example: WITH cte AS (SELECT m.album_id, b.band_name FROM albums m JOIN bands b ...)
469        // When tracing 'band_name', we need to find it comes from 'b' (bands), not 'm' (albums)
470        if (sourceNamespace instanceof CTENamespace) {
471            CTENamespace cteNs = (CTENamespace) sourceNamespace;
472            TTable tracedTable = null;
473
474            if (definitionNode instanceof TResultColumn) {
475                // Direct case: definitionNode is a TResultColumn from the CTE's SELECT list
476                tracedTable = traceColumnThroughCTE(cteNs, (TResultColumn) definitionNode);
477            } else {
478                // Indirect case: The column might be traced through a star column
479                // Try to find the column by name in the CTE chain
480                tracedTable = traceColumnByNameThroughCTE(cteNs, exposedName);
481            }
482
483            if (tracedTable != null) {
484                return tracedTable;
485            }
486
487            // For CTEs with multiple tables, if tracing failed (unqualified column),
488            // return null instead of the first table to avoid incorrect lineage.
489            // Example: WITH cte AS (SELECT musicians.id, musician_name, music_bands.band_name
490            //          FROM musicians JOIN ... JOIN music_bands)
491            // The unqualified 'musician_name' cannot be traced to any specific table
492            // without metadata, so we should NOT guess and pick the first table.
493            TSelectSqlStatement cteSelect = cteNs.getSelectStatement();
494            if (cteSelect != null && cteSelect.tables != null) {
495                int tableCount = countRealTables(cteSelect.tables);
496                if (tableCount > 1) {
497                    // Cannot determine which table - don't guess
498                    return null;
499                }
500            }
501        }
502
503        return sourceNamespace.getFinalTable();
504    }
505
506    /**
507     * Get the original column name in the physical table when this column is an alias.
508     *
509     * <p>When a column is aliased in a subquery (e.g., {@code SELECT t.id AS col1}),
510     * the exposed name is {@code col1} but the original column in the physical table
511     * is {@code id}. This method returns {@code id} so callers can pair the correct
512     * column name with the physical table returned by {@link #getFinalTable()}.</p>
513     *
514     * <p>For multi-level aliases (e.g., {@code SELECT ADCS_MSISDN FROM (SELECT MSISDN AS ADCS_MSISDN FROM t) sub}),
515     * this recursively traces through all levels to find the original column name ({@code MSISDN}).</p>
516     *
517     * @return The original column name if this is an alias, or null if not an alias
518     *         or unable to determine
519     */
520    public String getFinalColumnName() {
521        return getFinalColumnNameInternal(0);
522    }
523
524    /**
525     * Internal recursive implementation of getFinalColumnName with depth limit.
526     */
527    private String getFinalColumnNameInternal(int depth) {
528        if (depth > 10) return null; // safety limit for deeply nested aliases
529
530        if (starMemberName != null) {
531            return starMemberName;
532        }
533
534        if (!isColumnAlias() && !isPassthroughToAlias()) {
535            return null;
536        }
537
538        if (definitionNode == null || !(definitionNode instanceof TResultColumn)) {
539            return null;
540        }
541
542        TResultColumn rc = (TResultColumn) definitionNode;
543        TExpression expr = rc.getExpr();
544        if (expr == null) {
545            return null;
546        }
547
548        TObjectName colRef = null;
549        if (expr.getExpressionType() == EExpressionType.simple_object_name_t) {
550            colRef = expr.getObjectOperand();
551        } else if (expr.getExpressionType() == EExpressionType.sqlserver_proprietary_column_alias_t) {
552            TExpression rightExpr = expr.getRightOperand();
553            if (rightExpr != null && rightExpr.getExpressionType() == EExpressionType.simple_object_name_t) {
554                colRef = rightExpr.getObjectOperand();
555            }
556        }
557
558        if (colRef != null) {
559            // Check if this inner column reference itself has a ColumnSource that's also an alias.
560            // If so, recursively trace to get the deepest original column name.
561            ColumnSource innerSource = colRef.getColumnSource();
562            if (innerSource != null && (innerSource.isColumnAlias() || innerSource.isPassthroughToAlias())) {
563                String deeperName = innerSource.getFinalColumnNameInternal(depth + 1);
564                if (deeperName != null) {
565                    return deeperName;
566                }
567            }
568            return colRef.getColumnNameOnly();
569        }
570        return null;
571    }
572
573    /**
574     * Trace a column by name through a CTE to find its correct source table.
575     * This handles the case when the column is traced through star columns
576     * and we don't have the direct TResultColumn definition.
577     *
578     * @param cteNs The CTENamespace to trace through
579     * @param columnName The name of the column to find
580     * @return The correct source table, or null if unable to determine
581     */
582    // ===== S2: vendor-aware identifier matching helpers =====
583    // Replace raw equalsIgnoreCase compares so quoted identifiers and
584    // vendor-specific case rules (BigQuery: tables sensitive, columns
585    // insensitive; Oracle/Postgres quoted: sensitive; etc.) are honored.
586    // The 2-arg INameMatcher.matches(...) defaults to dotColumn semantics
587    // inside VendorNameMatcher, so we route through VendorNameMatcher
588    // explicitly with the right ESQLDataObjectType when possible.
589
590    /**
591     * Slice S2: alias-equality check (subquery alias / table alias).
592     * Routes through dotTable on a VendorNameMatcher so BigQuery's
593     * table-sensitive rule is honored, falling back to the namespace's
594     * matcher for vendor-agnostic test scopes.
595     */
596    private boolean aliasMatches(String stored, String input) {
597        return matchesAs(stored, input, ESQLDataObjectType.dotTable);
598    }
599
600    /**
601     * Slice S2: bare table-name equality check.
602     * Same routing as {@link #aliasMatches} (dotTable). Kept as a
603     * separate name so call sites read self-documentingly.
604     */
605    private boolean tableMatches(String stored, String input) {
606        return matchesAs(stored, input, ESQLDataObjectType.dotTable);
607    }
608
609    /**
610     * Slice S2: column-name equality check.
611     * Routes through dotColumn (the VendorNameMatcher default), but
612     * called explicitly so future audits cannot mistake call sites
613     * for table compares.
614     */
615    private boolean columnMatches(String stored, String input) {
616        return matchesAs(stored, input, ESQLDataObjectType.dotColumn);
617    }
618
619    /**
620     * Look up the namespace's name matcher and route the compare through
621     * it with the supplied {@link ESQLDataObjectType}. Falls back to
622     * {@link String#equalsIgnoreCase} when the namespace is missing or is
623     * not an {@link AbstractNamespace} (synthetic / unit-test scopes), to
624     * preserve current vendor-agnostic behaviour for those callers.
625     */
626    private boolean matchesAs(String a, String b, ESQLDataObjectType objectType) {
627        if (a == null || b == null) {
628            return a == b;
629        }
630        INameMatcher matcher = sourceNamespace instanceof AbstractNamespace
631                ? ((AbstractNamespace) sourceNamespace).getNameMatcher()
632                : null;
633        if (matcher instanceof VendorNameMatcher) {
634            return ((VendorNameMatcher) matcher).matches(a, b, objectType);
635        }
636        if (matcher != null) {
637            return matcher.matches(a, b);
638        }
639        return a.equalsIgnoreCase(b);
640    }
641
642    private TTable traceColumnByNameThroughCTE(CTENamespace cteNs, String columnName) {
643        if (columnName == null || columnName.isEmpty()) {
644            return null;
645        }
646
647        // Get the CTE's SELECT statement
648        TSelectSqlStatement cteSelect = cteNs.getSelectStatement();
649        if (cteSelect == null) {
650            return null;
651        }
652
653        // First, check if this CTE has explicit columns matching the name
654        TTable result = findColumnInSelectList(cteSelect, columnName);
655        if (result != null) {
656            return result;
657        }
658
659        // If not found directly, check if this CTE uses SELECT * from another CTE
660        if (cteSelect.tables != null) {
661            for (int i = 0; i < cteSelect.tables.size(); i++) {
662                TTable table = cteSelect.tables.getTable(i);
663                if (table == null) continue;
664
665                // If it references another CTE, trace through it
666                if (table.isCTEName() && table.getCTE() != null) {
667                    gudusoft.gsqlparser.nodes.TCTE underlyingCte = table.getCTE();
668                    TSelectSqlStatement underlyingSelect = underlyingCte.getSubquery();
669                    if (underlyingSelect != null) {
670                        result = findColumnInSelectList(underlyingSelect, columnName);
671                        if (result != null) {
672                            return result;
673                        }
674                    }
675                }
676            }
677        }
678
679        return null;
680    }
681
682    /**
683     * Trace a column through a CTE to find its correct source table.
684     * This handles CTEs with JOINs where columns come from different tables,
685     * including CTEs with star columns that reference other CTEs.
686     *
687     * @param cteNs The CTENamespace
688     * @param resultColumn The TResultColumn from the CTE's SELECT list
689     * @return The correct source table, or null if unable to determine
690     */
691    private TTable traceColumnThroughCTE(CTENamespace cteNs, TResultColumn resultColumn) {
692        if (resultColumn == null || resultColumn.getExpr() == null) {
693            return null;
694        }
695
696        TExpression expr = resultColumn.getExpr();
697
698        // Check if the expression is a star column (e.g., SELECT * FROM other_cte)
699        // In this case, we need to trace through to the underlying CTE
700        if (expr.getExpressionType() == EExpressionType.simple_object_name_t) {
701            TObjectName colRef = expr.getObjectOperand();
702            if (colRef != null && "*".equals(colRef.getColumnNameOnly())) {
703                // This is a star column - trace through to find the actual column
704                return traceColumnThroughStarInCTE(cteNs, exposedName);
705            }
706        }
707
708        // Check if the expression is a simple column reference
709        if (expr.getExpressionType() != EExpressionType.simple_object_name_t) {
710            return null;
711        }
712
713        TObjectName colRef = expr.getObjectOperand();
714        if (colRef == null) {
715            return null;
716        }
717
718        // Check if the column has a table qualifier (e.g., "b.band_name")
719        String tableQualifier = colRef.getTableString();
720        if (tableQualifier == null || tableQualifier.isEmpty()) {
721            // No qualifier - can't determine which table
722            return null;
723        }
724
725        // Get the CTE's subquery to find the table with matching alias
726        TSelectSqlStatement cteSubquery = cteNs.getSelectStatement();
727        if (cteSubquery == null || cteSubquery.tables == null) {
728            return null;
729        }
730
731        // Search for the table with matching alias or name (S2: vendor-aware compares)
732        for (int i = 0; i < cteSubquery.tables.size(); i++) {
733            TTable table = cteSubquery.tables.getTable(i);
734            if (table == null) continue;
735
736            // Check alias match
737            String alias = table.getAliasName();
738            if (alias != null && aliasMatches(alias, tableQualifier)) {
739                // Found the table - now trace to its final physical table if needed
740                return traceToPhysicalTable(table);
741            }
742
743            // Check table name match (for unaliased tables)
744            String tableName = table.getTableName() != null ? table.getTableName().toString() : null;
745            if (tableName != null && tableMatches(tableName, tableQualifier)) {
746                return traceToPhysicalTable(table);
747            }
748        }
749
750        return null;
751    }
752
753    /**
754     * Trace a specific column through a CTE that uses SELECT *.
755     * This finds the underlying CTE that defines the column and traces it to the correct table.
756     *
757     * @param cteNs The CTE namespace with SELECT *
758     * @param columnName The name of the column to trace
759     * @return The correct source table, or null if unable to determine
760     */
761    private TTable traceColumnThroughStarInCTE(CTENamespace cteNs, String columnName) {
762        if (columnName == null || columnName.isEmpty()) {
763            return null;
764        }
765
766        TSelectSqlStatement cteSubquery = cteNs.getSelectStatement();
767        if (cteSubquery == null || cteSubquery.tables == null) {
768            return null;
769        }
770
771        // Find the underlying CTE or table that the star column references
772        for (int i = 0; i < cteSubquery.tables.size(); i++) {
773            TTable table = cteSubquery.tables.getTable(i);
774            if (table == null) continue;
775
776            // If it's a CTE reference, look for the column in that CTE
777            if (table.isCTEName() && table.getCTE() != null) {
778                gudusoft.gsqlparser.nodes.TCTE underlyingCte = table.getCTE();
779                TSelectSqlStatement underlyingSubquery = underlyingCte.getSubquery();
780                if (underlyingSubquery != null) {
781                    // Look for the column in the underlying CTE's SELECT list
782                    TTable tracedTable = findColumnInSelectList(underlyingSubquery, columnName);
783                    if (tracedTable != null) {
784                        return tracedTable;
785                    }
786                }
787            }
788        }
789
790        return null;
791    }
792
793    /**
794     * Find a column by name in a SELECT list and trace it to its source table.
795     *
796     * @param selectStmt The SELECT statement to search
797     * @param columnName The column name to find
798     * @return The source table for the column, or null if not found
799     */
800    private TTable findColumnInSelectList(TSelectSqlStatement selectStmt, String columnName) {
801        if (selectStmt == null || selectStmt.getResultColumnList() == null) {
802            return null;
803        }
804
805        gudusoft.gsqlparser.nodes.TResultColumnList resultList = selectStmt.getResultColumnList();
806        for (int i = 0; i < resultList.size(); i++) {
807            TResultColumn rc = resultList.getResultColumn(i);
808            if (rc == null) continue;
809
810            // Get the exposed name (alias or column name)
811            String exposedColName = rc.getAliasClause() != null
812                ? rc.getAliasClause().toString()
813                : (rc.getExpr() != null && rc.getExpr().getObjectOperand() != null
814                    ? rc.getExpr().getObjectOperand().getColumnNameOnly()
815                    : null);
816
817            if (exposedColName != null && columnMatches(exposedColName, columnName)) {
818                // Found the column - trace it to its source table (S2: vendor-aware compares)
819                TExpression expr = rc.getExpr();
820                if (expr != null && expr.getExpressionType() == EExpressionType.simple_object_name_t) {
821                    TObjectName colRef = expr.getObjectOperand();
822                    if (colRef != null) {
823                        String tableQualifier = colRef.getTableString();
824                        if (tableQualifier != null && !tableQualifier.isEmpty()) {
825                            // Find the table with this qualifier in the FROM clause
826                            if (selectStmt.tables != null) {
827                                for (int j = 0; j < selectStmt.tables.size(); j++) {
828                                    TTable table = selectStmt.tables.getTable(j);
829                                    if (table == null) continue;
830
831                                    String alias = table.getAliasName();
832                                    if (alias != null && aliasMatches(alias, tableQualifier)) {
833                                        return traceToPhysicalTable(table);
834                                    }
835
836                                    String tableName = table.getTableName() != null
837                                        ? table.getTableName().toString() : null;
838                                    if (tableName != null && tableMatches(tableName, tableQualifier)) {
839                                        return traceToPhysicalTable(table);
840                                    }
841                                }
842                            }
843                        }
844                    }
845                }
846            }
847        }
848
849        return null;
850    }
851
852    /**
853     * Trace a table to its underlying physical table.
854     * Handles CTEs, subqueries, and JOINs.
855     */
856    private TTable traceToPhysicalTable(TTable table) {
857        return traceToPhysicalTable(table,
858            Collections.newSetFromMap(new IdentityHashMap<TTable, Boolean>()));
859    }
860
861    private TTable traceToPhysicalTable(TTable table, Set<TTable> visited) {
862        if (table == null) {
863            return null;
864        }
865
866        if (!visited.add(table)) {
867            return null;
868        }
869
870        // If it's already a physical table, return it
871        if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !table.isCTEName()) {
872            return table;
873        }
874
875        // If it's a CTE reference, trace through the CTE
876        if (table.isCTEName() && table.getCTE() != null) {
877            // Use a simple approach - get the first physical table from the CTE
878            // This could be enhanced to trace specific columns through nested CTEs
879            gudusoft.gsqlparser.nodes.TCTE nestedCte = table.getCTE();
880            if (nestedCte.getSubquery() != null && nestedCte.getSubquery().tables != null) {
881                for (int i = 0; i < nestedCte.getSubquery().tables.size(); i++) {
882                    TTable nestedTable = nestedCte.getSubquery().tables.getTable(i);
883                    TTable physical = traceToPhysicalTable(nestedTable, visited);
884                    if (physical != null) {
885                        return physical;
886                    }
887                }
888            }
889        }
890
891        // If it's a subquery, trace through it
892        if (table.getSubquery() != null) {
893            SubqueryNamespace nestedNs = new SubqueryNamespace(
894                table.getSubquery(),
895                table.getAliasName(),
896                null  // nameMatcher not needed for simple tracing
897            );
898            return nestedNs.getFinalTable();
899        }
900
901        return null;
902    }
903
904    /**
905     * Trace a column alias through a subquery to find its source table.
906     * When a column is aliased (e.g., SELECT t.id AS col1 FROM my_table t),
907     * the alias changes the column name but the data still comes from the
908     * underlying table. This method traces through to find that table.
909     *
910     * <p>Used by {@link #getTracedFinalTable()} to provide alias-aware lineage
911     * tracing. Handles both qualified (t.id AS col1) and unqualified (id AS col1)
912     * column references, as well as SQL Server proprietary alias syntax.</p>
913     *
914     * @param subNs The SubqueryNamespace containing the aliased column
915     * @return The physical table the column traces to, or null if undetermined
916     */
917    private TTable traceColumnAliasThroughSubquery(SubqueryNamespace subNs) {
918        if (definitionNode == null || !(definitionNode instanceof TResultColumn)) {
919            return null;
920        }
921
922        TResultColumn rc = (TResultColumn) definitionNode;
923        TExpression expr = rc.getExpr();
924        if (expr == null) {
925            return null;
926        }
927
928        TObjectName colRef = null;
929
930        if (expr.getExpressionType() == EExpressionType.simple_object_name_t) {
931            // Standard: SELECT col AS alias
932            colRef = expr.getObjectOperand();
933        } else if (expr.getExpressionType() == EExpressionType.sqlserver_proprietary_column_alias_t) {
934            // SQL Server: SELECT alias = col
935            TExpression rightExpr = expr.getRightOperand();
936            if (rightExpr != null && rightExpr.getExpressionType() == EExpressionType.simple_object_name_t) {
937                colRef = rightExpr.getObjectOperand();
938            }
939        }
940
941        if (colRef == null) {
942            return null;
943        }
944
945        // Use the resolved sourceTable from the inner column reference
946        TTable sourceTable = colRef.getSourceTable();
947        if (sourceTable != null) {
948            return traceToPhysicalTable(sourceTable);
949        }
950
951        // Fallback: if no sourceTable resolved, try to find by table qualifier (S2: vendor-aware)
952        String tableQualifier = colRef.getTableString();
953        if (tableQualifier != null && !tableQualifier.isEmpty()) {
954            TSelectSqlStatement subquery = subNs.getSubquery();
955            if (subquery != null && subquery.tables != null) {
956                for (int i = 0; i < subquery.tables.size(); i++) {
957                    TTable table = subquery.tables.getTable(i);
958                    if (table == null) continue;
959
960                    String alias = table.getAliasName();
961                    if (alias != null && aliasMatches(alias, tableQualifier)) {
962                        return traceToPhysicalTable(table);
963                    }
964
965                    String tableName = table.getTableName() != null ? table.getTableName().toString() : null;
966                    if (tableName != null && tableMatches(tableName, tableQualifier)) {
967                        return traceToPhysicalTable(table);
968                    }
969                }
970            }
971        }
972
973        return null;
974    }
975
976    /**
977     * Get all physical tables that this column might originate from.
978     *
979     * <p>For columns from UNION queries, this returns tables from ALL branches,
980     * not just the first one. This is essential for proper lineage tracking
981     * where a column like {@code actor_id} in a UNION query should be linked
982     * to {@code actor.actor_id}, {@code actor2.actor_id}, {@code actor3.actor_id}.</p>
983     *
984     * <p>For regular single-table sources, this returns a single-element list
985     * with the same table as {@link #getFinalTable()}.</p>
986     *
987     * @return List of all physical tables, or empty list if unable to determine
988     */
989    public java.util.List<TTable> getAllFinalTables() {
990        // If this ColumnSource has explicit candidateTables set (e.g., from UNION inference),
991        // use those instead of delegating to namespace. This is critical for UNION queries
992        // where only branches with SELECT * should contribute candidate tables for inferred columns.
993        // An EMPTY list means "no matching tables" - return it as-is without delegating.
994        // A NULL means "not determined" - delegate to namespace.
995        if (candidateTables != null) {
996            return candidateTables;
997        }
998
999        // As getFinalTable: a star member the alias column list renamed is its table's, and a name the list gives a
1000        // column of a star whose columns are not known is no table's (Mantis 4843)
1001        if (starMemberName != null && overrideTable != null) {
1002            return java.util.Collections.singletonList(overrideTable);
1003        }
1004        if (EVIDENCE_ALIAS_COLUMN_VIA_STAR.equals(evidence)) {
1005            return java.util.Collections.emptyList();
1006        }
1007
1008        if (sourceNamespace == null) {
1009            if (overrideTable != null) {
1010                return java.util.Collections.singletonList(overrideTable);
1011            }
1012            return java.util.Collections.emptyList();
1013        }
1014
1015        // For calculated columns and aliases in SubqueryNamespace, don't trace
1016        if (sourceNamespace instanceof SubqueryNamespace) {
1017            if (isCalculatedColumn() || isColumnAlias()) {
1018                return java.util.Collections.emptyList();
1019            }
1020        }
1021
1022        // For CTENamespace calculated/alias/explicit columns, trace to CTE itself
1023        if (sourceNamespace instanceof CTENamespace) {
1024            if (isCalculatedColumn() || isColumnAlias() || isCTEExplicitColumn()) {
1025                TTable cteTable = ((CTENamespace) sourceNamespace).getReferencingTable();
1026                if (cteTable != null) {
1027                    return java.util.Collections.singletonList(cteTable);
1028                }
1029                return java.util.Collections.emptyList();
1030            }
1031        }
1032
1033        // Delegate to namespace - handles UNION queries via UnionNamespace.getAllFinalTables()
1034        return sourceNamespace.getAllFinalTables();
1035    }
1036
1037    /**
1038     * Check if this column is a passthrough reference to an underlying alias.
1039     *
1040     * <p>A passthrough column is a simple column reference in a subquery that
1041     * references another column from its FROM clause. If that underlying column
1042     * is an alias, then this passthrough should not trace to the base table.</p>
1043     *
1044     * <p>Example: In {@code SELECT stat_typ FROM (SELECT stat_typ = col FROM t) AS b},
1045     * the outer {@code stat_typ} is a passthrough to {@code b.stat_typ}, which is an alias.</p>
1046     *
1047     * @return true if this is a passthrough to an alias
1048     */
1049    private boolean isPassthroughToAlias() {
1050        if (definitionNode == null || !(definitionNode instanceof TResultColumn)) {
1051            return false;
1052        }
1053
1054        TResultColumn rc = (TResultColumn) definitionNode;
1055        TExpression expr = rc.getExpr();
1056        if (expr == null) {
1057            return false;
1058        }
1059
1060        // Only check simple column references (passthroughs)
1061        if (expr.getExpressionType() != EExpressionType.simple_object_name_t) {
1062            return false;
1063        }
1064
1065        // If this column itself has an alias that differs, it's already handled by isColumnAlias()
1066        if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) {
1067            return false;
1068        }
1069
1070        // Get the column name being referenced
1071        gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand();
1072        if (objName == null) {
1073            return false;
1074        }
1075        String columnName = objName.getColumnNameOnly();
1076        if (columnName == null || columnName.isEmpty()) {
1077            return false;
1078        }
1079
1080        // Resolve this column in the subquery's FROM scope to find the underlying ColumnSource
1081        if (sourceNamespace instanceof SubqueryNamespace) {
1082            SubqueryNamespace subNs = (SubqueryNamespace) sourceNamespace;
1083            ColumnSource underlyingSource = subNs.resolveColumnInFromScope(columnName);
1084            if (underlyingSource != null) {
1085                // Check if the underlying column is an alias or calculated
1086                if (underlyingSource.isColumnAlias() || underlyingSource.isCalculatedColumn()) {
1087                    return true;
1088                }
1089                // Recursively check if it's a passthrough to alias
1090                if (underlyingSource.isPassthroughToAlias()) {
1091                    return true;
1092                }
1093            }
1094        }
1095
1096        return false;
1097    }
1098
1099    /**
1100     * Check if this subquery column is a passthrough reference to a calculated column.
1101     *
1102     * <p>A subquery column is a passthrough to calculated if:</p>
1103     * <ol>
1104     *   <li>The column definition is a simple column reference (e.g., kko_lfz_9)</li>
1105     *   <li>The referenced column in the FROM scope is calculated (CASE, function, etc.)</li>
1106     * </ol>
1107     *
1108     * <p>Example:</p>
1109     * <pre>
1110     * SELECT kko_lfz_9 AS KKO_LFZ_9
1111     * FROM (SELECT CASE WHEN... END AS kko_lfz_9 FROM t) subq
1112     * </pre>
1113     * <p>Here, kko_lfz_9 in the outer query is a passthrough to a calculated column in subq.</p>
1114     *
1115     * <p>This differs from {@link #isPassthroughToAlias()} which skips columns with aliases.
1116     * Here we check even aliased passthroughs to see if they reference calculated columns.</p>
1117     *
1118     * @return true if this is a passthrough to a calculated column in a subquery
1119     */
1120    private boolean isPassthroughToCalculatedInSubquery() {
1121        if (definitionNode == null || !(definitionNode instanceof TResultColumn)) {
1122            return false;
1123        }
1124
1125        TResultColumn rc = (TResultColumn) definitionNode;
1126        TExpression expr = rc.getExpr();
1127        if (expr == null) {
1128            return false;
1129        }
1130
1131        // Only check simple column references (passthroughs)
1132        if (expr.getExpressionType() != EExpressionType.simple_object_name_t) {
1133            return false;
1134        }
1135
1136        // Get the column name being referenced
1137        gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand();
1138        if (objName == null) {
1139            return false;
1140        }
1141        String columnName = objName.getColumnNameOnly();
1142        if (columnName == null || columnName.isEmpty()) {
1143            return false;
1144        }
1145
1146        // Resolve this column in the subquery's FROM scope to find the underlying ColumnSource
1147        if (sourceNamespace instanceof SubqueryNamespace) {
1148            SubqueryNamespace subNs = (SubqueryNamespace) sourceNamespace;
1149            ColumnSource underlyingSource = subNs.resolveColumnInFromScope(columnName);
1150            if (underlyingSource != null) {
1151                // Check if the underlying column is calculated
1152                if (underlyingSource.isCalculatedColumn()) {
1153                    return true;
1154                }
1155                // Recursively check if it's a passthrough to calculated
1156                if (underlyingSource.isPassthroughToCalculatedInSubquery()) {
1157                    return true;
1158                }
1159            }
1160        }
1161
1162        return false;
1163    }
1164
1165    /**
1166     * Check if this CTE column is a passthrough reference to a calculated column in a subquery or nested CTE.
1167     *
1168     * <p>A CTE column is a passthrough to calculated if:</p>
1169     * <ol>
1170     *   <li>The column definition is a simple qualified column reference (e.g., subq.calc_col or cte.calc_col)</li>
1171     *   <li>The qualifier refers to a subquery or CTE in the CTE's body</li>
1172     *   <li>The referenced column in that subquery/CTE is calculated (CASE, function, etc.)</li>
1173     * </ol>
1174     *
1175     * <p>Example with subquery:</p>
1176     * <pre>
1177     * WITH DataCTE AS (
1178     *   SELECT ErrorCountsCTE.ErrorSeverityCategory  -- passthrough
1179     *   FROM (SELECT CASE...END AS ErrorSeverityCategory FROM t) ErrorCountsCTE
1180     * )
1181     * </pre>
1182     *
1183     * <p>Example with nested CTE:</p>
1184     * <pre>
1185     * WITH attendance_summary AS (
1186     *   SELECT date_trunc('month', attendance_date) as month FROM attendance
1187     * )
1188     * WITH outer_cte AS (
1189     *   SELECT a.month FROM attendance_summary a  -- passthrough to calculated in nested CTE
1190     * )
1191     * </pre>
1192     *
1193     * @return true if this is a passthrough to a calculated column in a CTE
1194     */
1195    private boolean isPassthroughToCalculatedInCTE() {
1196        if (definitionNode == null || !(definitionNode instanceof TResultColumn)) {
1197            return false;
1198        }
1199
1200        TResultColumn rc = (TResultColumn) definitionNode;
1201        TExpression expr = rc.getExpr();
1202        if (expr == null) {
1203            return false;
1204        }
1205
1206        // Only check simple qualified column references (passthroughs like subq.column)
1207        if (expr.getExpressionType() != EExpressionType.simple_object_name_t) {
1208            return false;
1209        }
1210
1211        // Get the column reference
1212        gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand();
1213        if (objName == null) {
1214            return false;
1215        }
1216
1217        // Must have a table qualifier (e.g., "ErrorCountsCTE" in "ErrorCountsCTE.ErrorSeverityCategory")
1218        String tableQualifier = objName.getTableString();
1219        if (tableQualifier == null || tableQualifier.isEmpty()) {
1220            return false;
1221        }
1222
1223        String columnName = objName.getColumnNameOnly();
1224        if (columnName == null || columnName.isEmpty()) {
1225            return false;
1226        }
1227
1228        // Get the CTE's subquery to find the referenced subquery alias
1229        if (!(sourceNamespace instanceof CTENamespace)) {
1230            return false;
1231        }
1232
1233        CTENamespace cteNs = (CTENamespace) sourceNamespace;
1234        gudusoft.gsqlparser.nodes.TCTE cte = cteNs.getCTE();
1235        if (cte == null || cte.getSubquery() == null) {
1236            return false;
1237        }
1238
1239        // Find the subquery/table with this alias in the CTE's body
1240        gudusoft.gsqlparser.stmt.TSelectSqlStatement cteBody = cte.getSubquery();
1241        TTable referencedTable = findTableByAlias(cteBody, tableQualifier);
1242        if (referencedTable == null) {
1243            return false;
1244        }
1245
1246        // Case 1: Referenced table is a subquery
1247        if (referencedTable.getSubquery() != null) {
1248            gudusoft.gsqlparser.stmt.TSelectSqlStatement subquery = referencedTable.getSubquery();
1249            // the derived table's alias column list renames its columns, (SELECT upper(z) AS z FROM t1) d(y):
1250            // y is the column at the alias's position (Mantis 4814, Codex review)
1251            int position = aliasColumnPosition(referencedTable, columnName);
1252            if (position >= 0) {
1253                return isCalculatedColumnAt(subquery, position);
1254            }
1255            return isCalculatedColumnInSelect(subquery, columnName);
1256        }
1257
1258        // Case 2: Referenced table is a CTE reference
1259        if (referencedTable.isCTEName() && referencedTable.getCTE() != null) {
1260            gudusoft.gsqlparser.nodes.TCTE referencedCTE = referencedTable.getCTE();
1261            if (referencedCTE.getSubquery() != null) {
1262                return isCalculatedColumnInSelect(referencedCTE.getSubquery(), columnName);
1263            }
1264        }
1265
1266        return false;
1267    }
1268
1269    /**
1270     * Find a table in a SELECT statement by its alias.
1271     */
1272    private TTable findTableByAlias(gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String alias) {
1273        if (select == null || select.tables == null || alias == null) {
1274            return null;
1275        }
1276
1277        for (int i = 0; i < select.tables.size(); i++) {
1278            TTable table = select.tables.getTable(i);
1279            if (table != null) {
1280                String tableAlias = table.getAliasName();
1281                // S2: vendor-aware alias / table-name compare (dotTable)
1282                if (tableAlias != null && aliasMatches(tableAlias, alias)) {
1283                    return table;
1284                }
1285                // Also check table name for non-aliased references
1286                if (tableAlias == null && table.getTableName() != null) {
1287                    String tableName = table.getTableName().toString();
1288                    if (tableName != null && tableMatches(tableName, alias)) {
1289                        return table;
1290                    }
1291                }
1292            }
1293        }
1294        return null;
1295    }
1296
1297    /**
1298     * Check if a column in a SELECT statement is calculated (not a simple column reference).
1299     */
1300    /** The position of {@code columnName} in the derived table's alias column list, or -1 (Mantis 4814). */
1301    private int aliasColumnPosition(TTable table, String columnName) {
1302        if (table == null || table.getAliasClause() == null || table.getAliasClause().getColumns() == null) {
1303            return -1;
1304        }
1305        gudusoft.gsqlparser.nodes.TObjectNameList columns = table.getAliasClause().getColumns();
1306        for (int i = 0; i < columns.size(); i++) {
1307            if (columns.getObjectName(i) != null && columnMatches(columns.getObjectName(i).getColumnNameOnly(), columnName)) {
1308                return i;
1309            }
1310        }
1311        return -1;
1312    }
1313
1314    /** Whether the select list's column at {@code position} is an expression rather than a column reference. */
1315    private boolean isCalculatedColumnAt(gudusoft.gsqlparser.stmt.TSelectSqlStatement select, int position) {
1316        if (select == null || select.getResultColumnList() == null || position >= select.getResultColumnList().size()) {
1317            return false;
1318        }
1319        TResultColumn rc = select.getResultColumnList().getResultColumn(position);
1320        TExpression expr = rc == null ? null : rc.getExpr();
1321        return expr != null && expr.getExpressionType() != EExpressionType.simple_object_name_t;
1322    }
1323
1324    private boolean isCalculatedColumnInSelect(gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String columnName) {
1325        if (select == null || select.getResultColumnList() == null || columnName == null) {
1326            return false;
1327        }
1328
1329        for (int i = 0; i < select.getResultColumnList().size(); i++) {
1330            TResultColumn rc = select.getResultColumnList().getResultColumn(i);
1331            if (rc == null) continue;
1332
1333            // Get the column name for this result column
1334            String rcName = null;
1335            if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) {
1336                rcName = rc.getAliasClause().getAliasName().toString();
1337            } else if (rc.getExpr() != null &&
1338                       rc.getExpr().getExpressionType() == EExpressionType.simple_object_name_t &&
1339                       rc.getExpr().getObjectOperand() != null) {
1340                rcName = rc.getExpr().getObjectOperand().getColumnNameOnly();
1341            }
1342
1343            if (rcName != null && columnMatches(rcName, columnName)) {
1344                // S2: vendor-aware column-name compare. Found the column - check if it's calculated.
1345                TExpression expr = rc.getExpr();
1346                if (expr != null && expr.getExpressionType() != EExpressionType.simple_object_name_t) {
1347                    // Non-simple expression = calculated
1348                    return true;
1349                }
1350            }
1351        }
1352        return false;
1353    }
1354
1355    /**
1356     * Check if this column source represents a calculated expression.
1357     *
1358     * <p>A column is calculated if its definition is a TResultColumn with
1359     * a non-simple expression (not a direct column reference or star).</p>
1360     *
1361     * <p>For inferred columns (via star expansion), we trace back to the
1362     * source CTE/subquery to check if the original column is calculated.</p>
1363     *
1364     * @return true if this is a calculated column
1365     */
1366    /** True when the exposed name comes from the derived table's alias column list, {@code d(y)} (Mantis 4814). */
1367    private boolean renamedByAliasList;
1368
1369    public void markRenamedByAliasList() {
1370        this.renamedByAliasList = true;
1371    }
1372
1373    public boolean isRenamedByAliasList() {
1374        return renamedByAliasList;
1375    }
1376
1377    /**
1378     * Evidence of a derived table's alias column name over a star whose expansion is not known (Mantis 4843):
1379     * the name is the derived table's column, and no column of the star's table is known to be behind it.
1380     */
1381    public static final String EVIDENCE_ALIAS_COLUMN_VIA_STAR = "derived_table_alias_column_via_star";
1382
1383    /**
1384     * The column of the star's table an alias column list renamed (Mantis 4843): in FROM (SELECT * FROM t3) d(a, b)
1385     * over t3(c1, c2, c3), a's member is c1. Reported by {@link #getFinalColumnName()}, so a is treated as an alias.
1386     */
1387    private String starMemberName;
1388
1389    public void setStarMemberName(String starMemberName) {
1390        this.starMemberName = starMemberName;
1391    }
1392
1393    /** A copy keeps the alias-list mark: x.p.address.city over x(p) is still a renamed column (Mantis 4814). */
1394    private ColumnSource keepAliasListMark(ColumnSource copy) {
1395        copy.renamedByAliasList = this.renamedByAliasList;
1396        copy.starMemberName = this.starMemberName;
1397        return copy;
1398    }
1399
1400    public boolean isCalculatedColumn() {
1401        if (definitionNode == null) {
1402            // For inferred columns through star expansion, check if the underlying
1403            // column in the source CTE/subquery is calculated
1404            return isInferredFromCalculatedColumn();
1405        }
1406
1407        if (!(definitionNode instanceof TResultColumn)) {
1408            return false;
1409        }
1410
1411        TResultColumn rc = (TResultColumn) definitionNode;
1412        TExpression expr = rc.getExpr();
1413        if (expr == null) {
1414            return false;
1415        }
1416
1417        EExpressionType exprType = expr.getExpressionType();
1418
1419        // Simple column reference - NOT calculated (passthrough)
1420        if (exprType == EExpressionType.simple_object_name_t) {
1421            return false;
1422        }
1423
1424        // Star column - NOT calculated (passthrough)
1425        String colText = rc.toString();
1426        if (colText != null && colText.endsWith("*")) {
1427            return false;
1428        }
1429
1430        // SQL Server proprietary column alias (col = expr)
1431        if (exprType == EExpressionType.sqlserver_proprietary_column_alias_t) {
1432            if (expr.getRightOperand() != null &&
1433                expr.getRightOperand().getExpressionType() == EExpressionType.simple_object_name_t) {
1434                return false;
1435            }
1436        }
1437
1438        // Any other expression type is calculated
1439        return true;
1440    }
1441
1442    /**
1443     * Check if this is an inferred column (via star expansion) that originates from
1444     * a calculated column in the source CTE/subquery.
1445     *
1446     * <p>When a column is resolved through star expansion (e.g., SELECT * FROM CTE),
1447     * the definitionNode is null. We need to trace back to the source namespace
1448     * to check if the original column is calculated.</p>
1449     *
1450     * @return true if this inferred column traces back to a calculated column
1451     */
1452    private boolean isInferredFromCalculatedColumn() {
1453        // Only check for inferred columns (evidence contains "auto_inferred")
1454        if (evidence == null || !evidence.contains("auto_inferred")) {
1455            return false;
1456        }
1457
1458        // Need the source namespace and column name to trace
1459        if (sourceNamespace == null || exposedName == null) {
1460            return false;
1461        }
1462
1463        // For CTE namespace, check if the column is calculated in the CTE's SELECT list
1464        if (sourceNamespace instanceof CTENamespace) {
1465            CTENamespace cteNs = (CTENamespace) sourceNamespace;
1466            gudusoft.gsqlparser.nodes.TCTE cte = cteNs.getCTE();
1467            if (cte != null && cte.getSubquery() != null) {
1468                // First check the CTE's direct SELECT list
1469                if (isCalculatedColumnInSelect(cte.getSubquery(), exposedName)) {
1470                    return true;
1471                }
1472
1473                // If the CTE has a star column, trace through to referenced CTEs
1474                if (cteNs.hasStarColumn()) {
1475                    return isCalculatedInCTEChain(cte.getSubquery(), exposedName);
1476                }
1477            }
1478        }
1479
1480        // For Subquery namespace, check if the column is calculated in the subquery's SELECT list
1481        if (sourceNamespace instanceof SubqueryNamespace) {
1482            SubqueryNamespace subNs = (SubqueryNamespace) sourceNamespace;
1483            gudusoft.gsqlparser.stmt.TSelectSqlStatement subquery = subNs.getSubquery();
1484            if (subquery != null) {
1485                // First check the subquery's direct SELECT list
1486                if (isCalculatedColumnInSelect(subquery, exposedName)) {
1487                    return true;
1488                }
1489
1490                // If the subquery has a star column, trace through to source tables
1491                if (subNs.hasStarColumn()) {
1492                    return isCalculatedInSubqueryChain(subquery, exposedName);
1493                }
1494            }
1495        }
1496
1497        return false;
1498    }
1499
1500    /**
1501     * Check if a column is calculated by tracing through CTE references or
1502     * inline subqueries in the FROM clause. This handles cases like
1503     * Stage4 -> Stage3 -> Stage2 where the column is calculated at some
1504     * intermediate level, regardless of whether each level is a named CTE or
1505     * an inline derived table.
1506     *
1507     * <p>Example covered by the subquery branch:</p>
1508     * <pre>
1509     * WITH tbl AS (
1510     *   SELECT t.* FROM (SELECT CASE...END AS bug1 FROM base) t
1511     * )
1512     * SELECT bug1 FROM tbl
1513     * </pre>
1514     * <p>The CTE body's FROM clause is a subquery (not another CTE), but
1515     * bug1 is still calculated in the inner subquery and must not be traced
1516     * to {@code base}.</p>
1517     */
1518    private boolean isCalculatedInCTEChain(gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String columnName) {
1519        return isCalculatedInCTEChain(select, columnName,
1520                new HashSet<gudusoft.gsqlparser.stmt.TSelectSqlStatement>());
1521    }
1522
1523    private boolean isCalculatedInCTEChain(
1524            gudusoft.gsqlparser.stmt.TSelectSqlStatement select,
1525            String columnName,
1526            java.util.Set<gudusoft.gsqlparser.stmt.TSelectSqlStatement> visited) {
1527        if (select == null || select.tables == null) {
1528            return false;
1529        }
1530        // Guard against recursive CTEs (anchor body references itself via the
1531        // CTE name) and self-cycling subquery DAGs.
1532        if (!visited.add(select)) {
1533            return false;
1534        }
1535
1536        // Walk every table in the FROM clause - both CTE references and inline
1537        // subqueries can shadow underlying physical columns with calculated
1538        // expressions.
1539        for (int i = 0; i < select.tables.size(); i++) {
1540            TTable table = select.tables.getTable(i);
1541            if (table == null) continue;
1542
1543            if (table.isCTEName() && table.getCTE() != null) {
1544                gudusoft.gsqlparser.nodes.TCTE referencedCTE = table.getCTE();
1545                if (referencedCTE.getSubquery() != null) {
1546                    if (isCalculatedColumnInSelect(referencedCTE.getSubquery(), columnName)) {
1547                        return true;
1548                    }
1549                    if (isCalculatedInCTEChain(referencedCTE.getSubquery(), columnName, visited)) {
1550                        return true;
1551                    }
1552                }
1553            } else if (table.getSubquery() != null) {
1554                gudusoft.gsqlparser.stmt.TSelectSqlStatement subquery = table.getSubquery();
1555                if (isCalculatedColumnInSelect(subquery, columnName)) {
1556                    return true;
1557                }
1558                if (isCalculatedInCTEChain(subquery, columnName, visited)) {
1559                    return true;
1560                }
1561            }
1562        }
1563        return false;
1564    }
1565
1566    /**
1567     * Check if a column is calculated by tracing through subquery references.
1568     */
1569    private boolean isCalculatedInSubqueryChain(gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String columnName) {
1570        return isCalculatedInSubqueryChain(select, columnName,
1571                new HashSet<gudusoft.gsqlparser.stmt.TSelectSqlStatement>());
1572    }
1573
1574    private boolean isCalculatedInSubqueryChain(
1575            gudusoft.gsqlparser.stmt.TSelectSqlStatement select,
1576            String columnName,
1577            java.util.Set<gudusoft.gsqlparser.stmt.TSelectSqlStatement> visited) {
1578        if (select == null || select.tables == null) {
1579            return false;
1580        }
1581        if (!visited.add(select)) {
1582            return false;
1583        }
1584
1585        // Look for subquery tables in the FROM clause
1586        for (int i = 0; i < select.tables.size(); i++) {
1587            TTable table = select.tables.getTable(i);
1588            if (table != null && table.getSubquery() != null) {
1589                gudusoft.gsqlparser.stmt.TSelectSqlStatement subquery = table.getSubquery();
1590                if (isCalculatedColumnInSelect(subquery, columnName)) {
1591                    return true;
1592                }
1593                if (isCalculatedInSubqueryChain(subquery, columnName, visited)) {
1594                    return true;
1595                }
1596            }
1597            // Also check CTE references within subqueries
1598            if (table != null && table.isCTEName() && table.getCTE() != null) {
1599                gudusoft.gsqlparser.nodes.TCTE referencedCTE = table.getCTE();
1600                if (referencedCTE.getSubquery() != null) {
1601                    if (isCalculatedColumnInSelect(referencedCTE.getSubquery(), columnName)) {
1602                        return true;
1603                    }
1604                    if (isCalculatedInCTEChain(referencedCTE.getSubquery(), columnName, visited)) {
1605                        return true;
1606                    }
1607                }
1608            }
1609        }
1610        return false;
1611    }
1612
1613    /**
1614     * Check if this inferred column traces through SELECT * across a CTE or
1615     * subquery chain to a renamed alias of a simple column.
1616     *
1617     * <p>Used by {@link #getFinalTable()} to avoid linking renamed alias names
1618     * to the underlying physical table. Unlike {@link #isColumnAlias()}, this
1619     * works for inferred columns whose {@code definitionNode} is null because
1620     * they were exposed through {@code SELECT *}.</p>
1621     *
1622     * <p>Example:</p>
1623     * <pre>
1624     * WITH tbl AS (
1625     *   SELECT t.* FROM (SELECT max_hxdate AS bug2 FROM base) t
1626     * )
1627     * SELECT bug2 FROM tbl
1628     * </pre>
1629     * <p>{@code bug2} is exposed by the inner subquery as an alias of
1630     * {@code max_hxdate}. The base table has no column named {@code bug2},
1631     * so tracing further must stop here.</p>
1632     */
1633    private boolean isInferredAliasThroughChain() {
1634        if (evidence == null || !evidence.contains("auto_inferred")) {
1635            return false;
1636        }
1637        if (sourceNamespace == null || exposedName == null) {
1638            return false;
1639        }
1640
1641        gudusoft.gsqlparser.stmt.TSelectSqlStatement select = null;
1642        if (sourceNamespace instanceof CTENamespace) {
1643            CTENamespace cteNs = (CTENamespace) sourceNamespace;
1644            if (cteNs.getCTE() != null) {
1645                select = cteNs.getCTE().getSubquery();
1646            }
1647        } else if (sourceNamespace instanceof SubqueryNamespace) {
1648            select = ((SubqueryNamespace) sourceNamespace).getSubquery();
1649        }
1650        if (select == null) {
1651            return false;
1652        }
1653
1654        // Direct match in current SELECT list.
1655        if (isRenamedAliasInSelect(select, exposedName)) {
1656            return true;
1657        }
1658        // Walk through underlying CTE/subquery tables exposed via star.
1659        return isRenamedAliasInChain(select, exposedName, new HashSet<gudusoft.gsqlparser.stmt.TSelectSqlStatement>());
1660    }
1661
1662    /**
1663     * Check if a SELECT list contains a result column where a simple column
1664     * reference is renamed via an alias whose name matches {@code columnName}
1665     * but differs from the underlying column's own name.
1666     */
1667    private boolean isRenamedAliasInSelect(
1668            gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String columnName) {
1669        if (select == null || select.getResultColumnList() == null || columnName == null) {
1670            return false;
1671        }
1672        for (int i = 0; i < select.getResultColumnList().size(); i++) {
1673            TResultColumn rc = select.getResultColumnList().getResultColumn(i);
1674            if (rc == null || rc.getAliasClause() == null
1675                    || rc.getAliasClause().getAliasName() == null) {
1676                continue;
1677            }
1678            String aliasName = rc.getAliasClause().getAliasName().toString();
1679            if (aliasName == null || !columnMatches(aliasName, columnName)) {
1680                continue;
1681            }
1682            TExpression expr = rc.getExpr();
1683            if (expr == null
1684                    || expr.getExpressionType() != EExpressionType.simple_object_name_t) {
1685                // Non-simple expressions are handled as "calculated" elsewhere.
1686                continue;
1687            }
1688            gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand();
1689            if (objName == null) continue;
1690            String origName = objName.getColumnNameOnly();
1691            if (origName != null && !columnMatches(origName, aliasName)) {
1692                return true;
1693            }
1694        }
1695        return false;
1696    }
1697
1698    /**
1699     * Recursive walk through the CTE references and subquery tables in
1700     * {@code select}'s FROM clause that can expose {@code columnName} via
1701     * star expansion, looking for {@link #isRenamedAliasInSelect} matches.
1702     *
1703     * <p>The walk is restricted to star-source tables to avoid false
1704     * positives in queries like {@code SELECT a.* FROM a JOIN (SELECT x AS id
1705     * FROM b_base) b}: when resolving an inferred {@code id} from {@code a.*}
1706     * we must not match the alias inside {@code b}, which never contributed
1707     * to {@code a.*}'s expansion.</p>
1708     */
1709    private boolean isRenamedAliasInChain(
1710            gudusoft.gsqlparser.stmt.TSelectSqlStatement select,
1711            String columnName,
1712            java.util.Set<gudusoft.gsqlparser.stmt.TSelectSqlStatement> visited) {
1713        if (select == null || select.tables == null || columnName == null) {
1714            return false;
1715        }
1716        if (!visited.add(select)) {
1717            return false;
1718        }
1719
1720        java.util.List<TTable> sources = findStarExpansionSources(select);
1721        if (sources == null) {
1722            // Unqualified star is present - it can expose a column from any
1723            // FROM-clause table, so walk them all.
1724            sources = new java.util.ArrayList<>();
1725            for (int i = 0; i < select.tables.size(); i++) {
1726                TTable t = select.tables.getTable(i);
1727                if (t != null) sources.add(t);
1728            }
1729        }
1730
1731        for (TTable table : sources) {
1732            gudusoft.gsqlparser.stmt.TSelectSqlStatement nested = null;
1733            if (table.isCTEName() && table.getCTE() != null) {
1734                nested = table.getCTE().getSubquery();
1735            } else if (table.getSubquery() != null) {
1736                nested = table.getSubquery();
1737            }
1738            if (nested == null) continue;
1739
1740            if (isRenamedAliasInSelect(nested, columnName)) {
1741                return true;
1742            }
1743            if (isRenamedAliasInChain(nested, columnName, visited)) {
1744                return true;
1745            }
1746        }
1747        return false;
1748    }
1749
1750    /**
1751     * Collect the FROM-clause tables that can expose any inferred column
1752     * through a star expansion in {@code select}'s SELECT list.
1753     *
1754     * <p>Return value semantics:</p>
1755     * <ul>
1756     *   <li>{@code null} - an unqualified {@code *} is present, so every
1757     *       FROM-clause table is a potential source.</li>
1758     *   <li>Non-null, possibly empty - only the resolved qualified-star
1759     *       sources (e.g. the {@code t} in {@code t.*}).</li>
1760     * </ul>
1761     */
1762    private java.util.List<TTable> findStarExpansionSources(
1763            gudusoft.gsqlparser.stmt.TSelectSqlStatement select) {
1764        if (select == null || select.tables == null) {
1765            return java.util.Collections.emptyList();
1766        }
1767        gudusoft.gsqlparser.nodes.TResultColumnList rcs = select.getResultColumnList();
1768        if (rcs == null) {
1769            return java.util.Collections.emptyList();
1770        }
1771        java.util.LinkedHashSet<TTable> sources = new java.util.LinkedHashSet<>();
1772        for (int i = 0; i < rcs.size(); i++) {
1773            TResultColumn rc = rcs.getResultColumn(i);
1774            if (rc == null) continue;
1775            String text = rc.toString();
1776            if (text == null) continue;
1777            text = text.trim();
1778            if (!text.endsWith("*")) continue;
1779            if (text.equals("*")) {
1780                // Unqualified star - any FROM-clause table is a potential source.
1781                return null;
1782            }
1783            int dot = text.lastIndexOf('.');
1784            if (dot <= 0) continue;
1785            String prefix = text.substring(0, dot).trim();
1786            TTable matched = findStarPrefixTable(select, prefix);
1787            if (matched != null) {
1788                sources.add(matched);
1789            }
1790        }
1791        return new java.util.ArrayList<>(sources);
1792    }
1793
1794    /**
1795     * Find a FROM-clause table whose alias (or table name, when unaliased)
1796     * matches the prefix of a qualified star like {@code prefix.*}.
1797     */
1798    private TTable findStarPrefixTable(
1799            gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String prefix) {
1800        if (select == null || select.tables == null || prefix == null || prefix.isEmpty()) {
1801            return null;
1802        }
1803        for (int i = 0; i < select.tables.size(); i++) {
1804            TTable table = select.tables.getTable(i);
1805            if (table == null) continue;
1806            String alias = table.getAliasName();
1807            if (alias != null && aliasMatches(alias, prefix)) {
1808                return table;
1809            }
1810            if (alias == null && table.getTableName() != null) {
1811                String name = table.getTableName().toString();
1812                if (name != null && tableMatches(name, prefix)) {
1813                    return table;
1814                }
1815            }
1816        }
1817        return null;
1818    }
1819
1820    /**
1821     * Check if this column source represents a column alias (renamed column).
1822     *
1823     * <p>A column is an alias if it's a simple column reference in a subquery
1824     * that has been given a different name via AS or NAMED. For example:</p>
1825     * <ul>
1826     *   <li>{@code SELECT col AS alias FROM table} - alias is different from col</li>
1827     *   <li>{@code SELECT col (NAMED alias) FROM table} - Teradata NAMED syntax</li>
1828     *   <li>{@code SELECT alias = col FROM table} - SQL Server proprietary syntax</li>
1829     * </ul>
1830     *
1831     * <p>Column aliases are traced through in {@link #getFinalTable()} to find the
1832     * physical table the data originates from, since the alias only renames the column
1833     * but the data still comes from the physical table.</p>
1834     *
1835     * @return true if this is a column alias with a different name than the original
1836     */
1837    public boolean isColumnAlias() {
1838        if (definitionNode == null) {
1839            return false;
1840        }
1841
1842        if (!(definitionNode instanceof TResultColumn)) {
1843            return false;
1844        }
1845
1846        if (renamedByAliasList) {
1847            // FROM (SELECT :salary, z FROM t3) d(salary, y): the exposed name is the alias list's, so the column is
1848            // an alias of its definition, traced like `z AS y` is (Mantis 4814): a column definition reaches its
1849            // table, a variable or an expression reaches none
1850            return true;
1851        }
1852
1853        TResultColumn rc = (TResultColumn) definitionNode;
1854        TExpression expr = rc.getExpr();
1855        if (expr == null) {
1856            return false;
1857        }
1858
1859        EExpressionType exprType = expr.getExpressionType();
1860
1861        // Handle SQL Server proprietary alias syntax: alias = column
1862        // Example: stat_typ = stellplatz_typ
1863        if (exprType == EExpressionType.sqlserver_proprietary_column_alias_t) {
1864            TExpression rightExpr = expr.getRightOperand();
1865            TExpression leftExpr = expr.getLeftOperand();
1866            // Only if right side is a simple column reference
1867            if (rightExpr != null && leftExpr != null &&
1868                rightExpr.getExpressionType() == EExpressionType.simple_object_name_t) {
1869                gudusoft.gsqlparser.nodes.TObjectName rightObjName = rightExpr.getObjectOperand();
1870                gudusoft.gsqlparser.nodes.TObjectName leftObjName = leftExpr.getObjectOperand();
1871                if (rightObjName != null && leftObjName != null) {
1872                    String origName = rightObjName.getColumnNameOnly();
1873                    String aliasName = leftObjName.getColumnNameOnly();
1874                    // If alias name differs from original column name, it's an alias
1875                    if (origName != null && aliasName != null &&
1876                        !columnMatches(origName, aliasName)) {
1877                        return true;
1878                    }
1879                }
1880            }
1881            return false;
1882        }
1883
1884        // Standard alias syntax: column AS alias
1885        // Only applies to simple column references
1886        if (exprType != EExpressionType.simple_object_name_t) {
1887            return false;
1888        }
1889
1890        // Check if there's an alias that differs from the column name
1891        if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) {
1892            String aliasName = rc.getAliasClause().getAliasName().toString();
1893            if (aliasName != null && !aliasName.isEmpty()) {
1894                gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand();
1895                if (objName != null) {
1896                    String origName = objName.getColumnNameOnly();
1897                    // If alias name differs from original name, it's an alias
1898                    if (origName != null && !columnMatches(origName, aliasName)) {
1899                        return true;
1900                    }
1901                }
1902            }
1903        }
1904
1905        return false;
1906    }
1907
1908    /**
1909     * Check if this column is a CTE explicit column with a different name than the underlying column.
1910     *
1911     * <p>A CTE explicit column is one defined in the CTE's column list that maps to a
1912     * different column name in the CTE's SELECT list. For example:</p>
1913     * <pre>
1914     * WITH cte(c1, c2) AS (SELECT id, name FROM users)
1915     * SELECT c1 FROM cte  -- c1 maps to 'id', names differ
1916     * </pre>
1917     *
1918     * <p>CTE explicit columns should NOT trace to base tables because the explicit
1919     * column name (c1) doesn't exist as an actual column in the base table (users).</p>
1920     *
1921     * @return true if this is a CTE explicit column with a different name
1922     */
1923    public boolean isCTEExplicitColumn() {
1924        // Must be from a CTENamespace
1925        if (!(sourceNamespace instanceof CTENamespace)) {
1926            return false;
1927        }
1928
1929        // Check evidence for explicit column marker
1930        if (!"cte_explicit_column".equals(evidence)) {
1931            return false;
1932        }
1933
1934        // Get the underlying column name from the definition node
1935        if (definitionNode == null || !(definitionNode instanceof TResultColumn)) {
1936            return false;
1937        }
1938
1939        TResultColumn rc = (TResultColumn) definitionNode;
1940        TExpression expr = rc.getExpr();
1941        if (expr == null) {
1942            return false;
1943        }
1944
1945        // Get the column name from the SELECT list item
1946        String underlyingName = null;
1947
1948        // Check for alias first
1949        if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) {
1950            underlyingName = rc.getAliasClause().getAliasName().toString();
1951        }
1952        // Then check for simple column reference
1953        else if (expr.getExpressionType() == EExpressionType.simple_object_name_t &&
1954                 expr.getObjectOperand() != null) {
1955            underlyingName = expr.getObjectOperand().getColumnNameOnly();
1956        }
1957
1958        // If we can't determine the underlying name, assume it's different
1959        // (calculated expressions, etc. are definitely different from explicit column names)
1960        if (underlyingName == null) {
1961            return true;
1962        }
1963
1964        // If the exposed name differs from the underlying column name, it's an explicit column rename
1965        return !columnMatches(exposedName, underlyingName);
1966    }
1967
1968    /**
1969     * Get the override table, if set.
1970     */
1971    public TTable getOverrideTable() {
1972        return overrideTable;
1973    }
1974
1975    /**
1976     * Get the candidate tables for ambiguous columns.
1977     *
1978     * <p>When a column could come from multiple tables (e.g., SELECT * FROM t1, t2),
1979     * this returns all possible source tables. End users can iterate through this
1980     * list to understand all potential sources for the column.</p>
1981     *
1982     * @return List of candidate tables, or empty list if not ambiguous
1983     */
1984    public List<TTable> getCandidateTables() {
1985        return candidateTables != null ? candidateTables : Collections.emptyList();
1986    }
1987
1988    /**
1989     * Check if this column has multiple candidate tables (is ambiguous).
1990     *
1991     * @return true if there are multiple candidate tables
1992     */
1993    public boolean isAmbiguous() {
1994        return candidateTables != null && candidateTables.size() > 1;
1995    }
1996
1997    /**
1998     * Get the field path for deep/record field access.
1999     *
2000     * <p>When a column reference includes field access beyond the base column,
2001     * this returns the field path. For example, in {@code customer.address.city},
2002     * if base column is {@code customer}, this returns a FieldPath with
2003     * segments {@code ["address", "city"]}.</p>
2004     *
2005     * @return The field path, or null if no field access
2006     */
2007    public FieldPath getFieldPath() {
2008        return fieldPath;
2009    }
2010
2011    /**
2012     * Check if this column source has a field path (deep/record field access).
2013     *
2014     * @return true if a non-empty field path exists
2015     */
2016    public boolean hasFieldPath() {
2017        return fieldPath != null && !fieldPath.isEmpty();
2018    }
2019
2020    /**
2021     * Check if this is a struct field access (has evidence "struct_field_access").
2022     *
2023     * <p>This is a convenience method for checking if this column source represents
2024     * a struct/record field dereference operation.</p>
2025     *
2026     * @return true if this is a struct field access
2027     */
2028    public boolean isStructFieldAccess() {
2029        return "struct_field_access".equals(evidence);
2030    }
2031
2032    /**
2033     * Checks if this is a definite resolution (confidence = 1.0)
2034     */
2035    public boolean isDefinite() {
2036        return confidence >= 1.0;
2037    }
2038
2039    /**
2040     * Checks if this is an inferred resolution (confidence < 1.0)
2041     */
2042    public boolean isInferred() {
2043        return confidence < 1.0;
2044    }
2045
2046    @Override
2047    public String toString() {
2048        StringBuilder sb = new StringBuilder();
2049        sb.append(exposedName);
2050        if (sourceNamespace != null) {
2051            sb.append(" from ").append(sourceNamespace.getDisplayName());
2052        }
2053        if (confidence < 1.0) {
2054            sb.append(String.format(" (confidence: %.2f)", confidence));
2055        }
2056        return sb.toString();
2057    }
2058
2059    /**
2060     * Creates a copy with updated confidence and evidence.
2061     * Used when merging or updating inference results.
2062     *
2063     * @deprecated Use {@link #withEvidence(ResolutionEvidence)} instead
2064     */
2065    public ColumnSource withConfidence(double newConfidence, String newEvidence) {
2066        return keepAliasListMark(new ColumnSource(
2067            this.sourceNamespace,
2068            this.exposedName,
2069            this.definitionNode,
2070            newConfidence,
2071            newEvidence,
2072            this.overrideTable,
2073            this.candidateTables != null ? new java.util.ArrayList<>(this.candidateTables) : null,
2074            null, // will create from legacy evidence
2075            this.fieldPath
2076        ));
2077    }
2078
2079    /**
2080     * Creates a copy with updated ResolutionEvidence.
2081     * This is the preferred method for updating evidence in new code.
2082     *
2083     * @param newEvidence The new evidence detail
2084     * @return A new ColumnSource with updated evidence
2085     */
2086    public ColumnSource withEvidence(ResolutionEvidence newEvidence) {
2087        return keepAliasListMark(new ColumnSource(
2088            this.sourceNamespace,
2089            this.exposedName,
2090            this.definitionNode,
2091            newEvidence != null ? newEvidence.getWeight() : this.confidence,
2092            newEvidence != null ? newEvidence.toLegacyEvidence() : this.evidence,
2093            this.overrideTable,
2094            this.candidateTables != null ? new java.util.ArrayList<>(this.candidateTables) : null,
2095            newEvidence,
2096            this.fieldPath
2097        ));
2098    }
2099
2100    /**
2101     * Creates a copy with candidate tables.
2102     * Used when a column could come from multiple tables.
2103     */
2104    public ColumnSource withCandidateTables(List<TTable> candidates) {
2105        return keepAliasListMark(new ColumnSource(
2106            this.sourceNamespace,
2107            this.exposedName,
2108            this.definitionNode,
2109            this.confidence,
2110            this.evidence,
2111            this.overrideTable,
2112            candidates != null ? new java.util.ArrayList<>(candidates) : null,
2113            this.evidenceDetail,
2114            this.fieldPath
2115        ));
2116    }
2117
2118    /**
2119     * Creates a copy with a field path for deep/record field access.
2120     *
2121     * <p>This method is used when resolving struct/record field access patterns
2122     * like {@code customer.address.city}. The base column is preserved as the
2123     * exposedName, and the field path captures the remaining segments.</p>
2124     *
2125     * @param newFieldPath The field path segments (beyond the base column)
2126     * @return A new ColumnSource with the field path set
2127     */
2128    public ColumnSource withFieldPath(FieldPath newFieldPath) {
2129        return keepAliasListMark(new ColumnSource(
2130            this.sourceNamespace,
2131            this.exposedName,
2132            this.definitionNode,
2133            this.confidence,
2134            this.evidence,
2135            this.overrideTable,
2136            this.candidateTables != null ? new java.util.ArrayList<>(this.candidateTables) : null,
2137            this.evidenceDetail,
2138            newFieldPath
2139        ));
2140    }
2141
2142    /**
2143     * Creates a copy with a field path from a list of segments.
2144     *
2145     * <p>Convenience method for creating a ColumnSource with a field path
2146     * from a list of string segments.</p>
2147     *
2148     * @param segments The field path segments
2149     * @return A new ColumnSource with the field path set
2150     */
2151    public ColumnSource withFieldPath(List<String> segments) {
2152        return withFieldPath(FieldPath.of(segments));
2153    }
2154
2155    /**
2156     * Creates a copy with field path and updated evidence.
2157     *
2158     * <p>This method is used when resolving struct field access, combining
2159     * both the field path and the struct_field_access evidence marker.</p>
2160     *
2161     * @param newFieldPath The field path segments
2162     * @param newEvidence The evidence string (e.g., "struct_field_access")
2163     * @return A new ColumnSource with field path and evidence updated
2164     */
2165    public ColumnSource withFieldPath(FieldPath newFieldPath, String newEvidence) {
2166        return keepAliasListMark(new ColumnSource(
2167            this.sourceNamespace,
2168            this.exposedName,
2169            this.definitionNode,
2170            this.confidence,
2171            newEvidence,
2172            this.overrideTable,
2173            this.candidateTables != null ? new java.util.ArrayList<>(this.candidateTables) : null,
2174            null, // will create from legacy evidence
2175            newFieldPath
2176        ));
2177    }
2178
2179    /**
2180     * Count the number of "real" tables in a table list, excluding implicit lateral derived tables.
2181     *
2182     * <p>Teradata supports implicit lateral derived tables, which are auto-added when a column
2183     * references an undeclared table in the WHERE clause. These should not be counted when
2184     * determining if a subquery has multiple tables for column resolution purposes.</p>
2185     *
2186     * @param tables The table list to count
2187     * @return The number of real (non-implicit) tables
2188     */
2189    private static int countRealTables(gudusoft.gsqlparser.nodes.TTableList tables) {
2190        if (tables == null) {
2191            return 0;
2192        }
2193        int count = 0;
2194        for (int i = 0; i < tables.size(); i++) {
2195            TTable table = tables.getTable(i);
2196            if (table != null && table.getEffectType() != gudusoft.gsqlparser.ETableEffectType.tetImplicitLateralDerivedTable) {
2197                count++;
2198            }
2199        }
2200        return count;
2201    }
2202
2203    /**
2204     * Check if a SELECT statement has a qualified star column (e.g., ta.*, tb.*).
2205     * Qualified stars identify which table columns come from in multi-table subqueries.
2206     */
2207    private static boolean hasQualifiedStar(gudusoft.gsqlparser.stmt.TSelectSqlStatement select) {
2208        if (select == null || select.getResultColumnList() == null) {
2209            return false;
2210        }
2211        gudusoft.gsqlparser.nodes.TResultColumnList resultCols = select.getResultColumnList();
2212        for (int i = 0; i < resultCols.size(); i++) {
2213            TResultColumn rc = resultCols.getResultColumn(i);
2214            if (rc != null) {
2215                String colStr = rc.toString().trim();
2216                // Qualified star has format "alias.*" or "table.*"
2217                if (colStr.endsWith("*") && colStr.contains(".")) {
2218                    return true;
2219                }
2220            }
2221        }
2222        return false;
2223    }
2224
2225    /**
2226     * Check if a column exists in a table's DDL definition.
2227     *
2228     * <p>This method checks the table's column definitions (from CREATE TABLE statements
2229     * parsed in the same script) to verify if the column name is defined.</p>
2230     *
2231     * @param table The table to check
2232     * @param columnName The column name to look for
2233     * @return true if the column exists in the table's DDL, false if not found or no DDL available
2234     */
2235    public static boolean isColumnInTableDdl(TTable table, String columnName) {
2236        if (table == null || columnName == null || columnName.isEmpty()) {
2237            return false;
2238        }
2239
2240        // Check if the table has column definitions (from CREATE TABLE DDL)
2241        gudusoft.gsqlparser.nodes.TColumnDefinitionList columnDefs = table.getColumnDefinitions();
2242        if (columnDefs != null && columnDefs.size() > 0) {
2243            // S2: route compare through IdentifierService so quoted-sensitive
2244            // dialects (Oracle quoted, BigQuery tables) and BigQuery's
2245            // case-insensitive columns are handled per-vendor.
2246            EDbVendor vendor = table.dbvendor != null ? table.dbvendor : EDbVendor.dbvgeneric;
2247            for (int i = 0; i < columnDefs.size(); i++) {
2248                gudusoft.gsqlparser.nodes.TColumnDefinition colDef = columnDefs.getColumn(i);
2249                if (colDef != null && colDef.getColumnName() != null) {
2250                    String defColName = colDef.getColumnName().toString();
2251                    if (defColName != null
2252                            && IdentifierService.areEqualStatic(vendor, ESQLDataObjectType.dotColumn, defColName, columnName)) {
2253                        return true;
2254                    }
2255                }
2256            }
2257            // DDL exists but column not found
2258            return false;
2259        }
2260
2261        // No DDL available - return false (cannot verify)
2262        return false;
2263    }
2264
2265    /**
2266     * Check if a table has DDL metadata available (from CREATE TABLE in same script).
2267     *
2268     * @param table The table to check
2269     * @return true if DDL metadata is available for this table
2270     */
2271    public static boolean hasTableDdl(TTable table) {
2272        if (table == null) {
2273            return false;
2274        }
2275        gudusoft.gsqlparser.nodes.TColumnDefinitionList columnDefs = table.getColumnDefinitions();
2276        return columnDefs != null && columnDefs.size() > 0;
2277    }
2278
2279    /**
2280     * Check DDL verification status for a candidate table.
2281     *
2282     * <p>Returns a tri-state result:</p>
2283     * <ul>
2284     *   <li>1 = Column exists in table's DDL</li>
2285     *   <li>0 = Column NOT found in table's DDL (DDL available but column missing)</li>
2286     *   <li>-1 = Cannot verify (no DDL available for this table)</li>
2287     * </ul>
2288     *
2289     * @param table The candidate table to check
2290     * @param columnName The column name to verify
2291     * @return DDL verification status: 1 (exists), 0 (not found), -1 (no DDL)
2292     */
2293    public static int getDdlVerificationStatus(TTable table, String columnName) {
2294        if (table == null || columnName == null) {
2295            return -1;
2296        }
2297
2298        gudusoft.gsqlparser.nodes.TColumnDefinitionList columnDefs = table.getColumnDefinitions();
2299        if (columnDefs == null || columnDefs.size() == 0) {
2300            return -1; // No DDL available
2301        }
2302
2303        // DDL available - check if column exists (S2: vendor-aware compare)
2304        EDbVendor vendor = table.dbvendor != null ? table.dbvendor : EDbVendor.dbvgeneric;
2305        for (int i = 0; i < columnDefs.size(); i++) {
2306            gudusoft.gsqlparser.nodes.TColumnDefinition colDef = columnDefs.getColumn(i);
2307            if (colDef != null && colDef.getColumnName() != null) {
2308                String defColName = colDef.getColumnName().toString();
2309                if (defColName != null
2310                        && IdentifierService.areEqualStatic(vendor, ESQLDataObjectType.dotColumn, defColName, columnName)) {
2311                    return 1; // Column exists in DDL
2312                }
2313            }
2314        }
2315
2316        return 0; // DDL exists but column not found
2317    }
2318
2319    /**
2320     * Get DDL verification status for all candidate tables.
2321     *
2322     * <p>Returns a map from each candidate table to its DDL verification status:</p>
2323     * <ul>
2324     *   <li>1 = Column exists in table's DDL</li>
2325     *   <li>0 = Column NOT found in table's DDL</li>
2326     *   <li>-1 = Cannot verify (no DDL available)</li>
2327     * </ul>
2328     *
2329     * @return Map of candidate tables to their DDL verification status, or empty map if no candidates
2330     */
2331    public java.util.Map<TTable, Integer> getCandidateTableDdlStatus() {
2332        java.util.Map<TTable, Integer> result = new java.util.LinkedHashMap<>();
2333        if (candidateTables == null || candidateTables.isEmpty() || exposedName == null) {
2334            return result;
2335        }
2336
2337        for (TTable candidate : candidateTables) {
2338            int status = getDdlVerificationStatus(candidate, exposedName);
2339            result.put(candidate, status);
2340        }
2341        return result;
2342    }
2343}