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