Skip to main content

polyglot_sql/
lineage.rs

1//! Column Lineage Tracking
2//!
3//! This module provides functionality to track column lineage through SQL queries,
4//! building a graph of how columns flow from source tables to the result set.
5//! Supports UNION/INTERSECT/EXCEPT, CTEs, derived tables, subqueries, and star expansion.
6//!
7
8use crate::dialects::DialectType;
9use crate::error::{ColumnResolutionReason, ColumnResolutionTarget};
10use crate::expressions::{DataType, Expression, Identifier, JoinKind, NamedWindow, Select, With};
11#[cfg(feature = "generate")]
12use crate::generator::Generator;
13use crate::optimizer::annotate_types::annotate_types;
14use crate::optimizer::qualify_schema_aware_expression;
15use crate::schema::{normalize_name, Schema};
16use crate::scope::{
17    build_scope, find_all_in_scope, Scope, ScopeType, SourceInfo as ScopeSourceInfo, SourceKind,
18};
19use crate::{Error, Result};
20use serde::{Deserialize, Serialize};
21use std::collections::{HashMap, HashSet};
22
23/// The ordered output description of a query.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct QueryOutput {
27    /// Output entries in projection order.
28    pub columns: Vec<OutputColumn>,
29    /// Whether every entry has a stable zero-based ordinal.
30    pub ordinal_complete: bool,
31}
32
33/// One entry in a query's ordered output description.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(tag = "kind", rename_all = "snake_case")]
36pub enum OutputColumn {
37    /// A single output column with a known name.
38    Named {
39        name: String,
40        /// The zero-based output ordinal, when knowable.
41        ordinal: Option<usize>,
42    },
43    /// A single output column whose database-provided name is not reliable.
44    Unnamed {
45        /// The zero-based output ordinal, when knowable.
46        ordinal: Option<usize>,
47    },
48    /// An unresolved wildcard that contributes an unknown number of columns.
49    Wildcard {
50        /// Optional table or source qualifier from `table.*`.
51        qualifier: Option<String>,
52        /// The first possible output ordinal, when no earlier wildcard exists.
53        #[serde(rename = "startOrdinal")]
54        start_ordinal: Option<usize>,
55    },
56}
57
58/// The set operator that owns an immediate lineage branch.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum SetOperator {
62    Union,
63    Intersect,
64    Except,
65}
66
67/// Metadata describing a lineage node's position in an immediate set operation.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct SetBranch {
71    /// The set operator that owns this branch.
72    pub operator: SetOperator,
73    /// The zero-based branch ordinal within the immediate binary operation.
74    pub ordinal: usize,
75    /// Whether the immediate set operation uses `ALL` semantics.
76    pub all: bool,
77}
78
79/// A node in the column lineage graph
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct LineageNode {
82    /// Name of this lineage step (e.g., "table.column")
83    pub name: String,
84    /// The expression at this node
85    pub expression: Expression,
86    /// The source expression (the full query context)
87    pub source: Expression,
88    /// Downstream nodes that depend on this one
89    pub downstream: Vec<LineageNode>,
90    /// Optional source name (e.g., for derived tables)
91    pub source_name: String,
92    /// Semantic source kind for downstream consumers.
93    pub source_kind: SourceKind,
94    /// User-written source alias when different from canonical source name.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub source_alias: Option<String>,
97    /// Immediate set-operation branch metadata, when this node is a branch root.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub set_branch: Option<SetBranch>,
100    /// Optional reference node name (e.g., for CTEs)
101    pub reference_node_name: String,
102}
103
104impl LineageNode {
105    /// Create a new lineage node
106    pub fn new(name: impl Into<String>, expression: Expression, source: Expression) -> Self {
107        Self {
108            name: name.into(),
109            expression,
110            source,
111            downstream: Vec::new(),
112            source_name: String::new(),
113            source_kind: SourceKind::Unknown,
114            source_alias: None,
115            set_branch: None,
116            reference_node_name: String::new(),
117        }
118    }
119
120    /// Iterate over all nodes in the lineage graph using DFS
121    pub fn walk(&self) -> LineageWalker<'_> {
122        LineageWalker { stack: vec![self] }
123    }
124
125    /// Get all downstream column names
126    pub fn downstream_names(&self) -> Vec<String> {
127        self.downstream.iter().map(|n| n.name.clone()).collect()
128    }
129}
130
131fn source_kind_for_scope_context(
132    scope: &Scope,
133    source_name: &str,
134    reference_node_name: &str,
135) -> SourceKind {
136    source_kind_for_scope_context_with_type(
137        scope,
138        scope.scope_type,
139        source_name,
140        reference_node_name,
141    )
142}
143
144fn source_kind_for_scope_context_with_type(
145    scope: &Scope,
146    scope_type: ScopeType,
147    source_name: &str,
148    reference_node_name: &str,
149) -> SourceKind {
150    if source_name.is_empty() && reference_node_name.is_empty() {
151        return SourceKind::Root;
152    }
153    if let Some(source_info) = scope.sources.get(source_name) {
154        return source_info.kind;
155    }
156    if scope.cte_sources.contains_key(source_name) {
157        return SourceKind::Cte;
158    }
159    match scope_type {
160        ScopeType::Cte => SourceKind::Cte,
161        ScopeType::DerivedTable => SourceKind::DerivedTable,
162        ScopeType::Udtf => SourceKind::Virtual,
163        _ => SourceKind::Unknown,
164    }
165}
166
167fn apply_scope_context(
168    node: &mut LineageNode,
169    scope: &Scope,
170    source_name: &str,
171    reference_node_name: &str,
172) {
173    node.source_name = source_name.to_string();
174    node.reference_node_name = reference_node_name.to_string();
175    node.source_kind = source_kind_for_scope_context(scope, source_name, reference_node_name);
176}
177
178fn apply_scope_context_with_type(
179    node: &mut LineageNode,
180    scope: &Scope,
181    scope_type: ScopeType,
182    source_name: &str,
183    reference_node_name: &str,
184) {
185    node.source_name = source_name.to_string();
186    node.reference_node_name = reference_node_name.to_string();
187    node.source_kind = source_kind_for_scope_context_with_type(
188        scope,
189        scope_type,
190        source_name,
191        reference_node_name,
192    );
193}
194
195/// Iterator for walking the lineage graph
196pub struct LineageWalker<'a> {
197    stack: Vec<&'a LineageNode>,
198}
199
200impl<'a> Iterator for LineageWalker<'a> {
201    type Item = &'a LineageNode;
202
203    fn next(&mut self) -> Option<Self::Item> {
204        if let Some(node) = self.stack.pop() {
205            // Add children in reverse order so they're visited in order
206            for child in node.downstream.iter().rev() {
207                self.stack.push(child);
208            }
209            Some(node)
210        } else {
211            None
212        }
213    }
214}
215
216// ---------------------------------------------------------------------------
217// ColumnRef: name or positional index for column lookup
218// ---------------------------------------------------------------------------
219
220/// Column reference for lineage tracing — by name or positional index.
221enum ColumnRef<'a> {
222    Name(&'a str),
223    Index(usize),
224}
225
226// ---------------------------------------------------------------------------
227// Public API
228// ---------------------------------------------------------------------------
229
230/// Build the lineage graph for a column in a SQL query
231///
232/// # Arguments
233/// * `column` - The column name to trace lineage for
234/// * `sql` - The SQL expression (SELECT, UNION, etc.)
235/// * `dialect` - Optional dialect for parsing
236/// * `trim_selects` - If true, trim the source SELECT to only include the target column
237///
238/// # Returns
239/// The root lineage node for the specified column
240///
241/// # Example
242/// ```ignore
243/// use polyglot_sql::lineage::lineage;
244/// use polyglot_sql::parse_one;
245/// use polyglot_sql::DialectType;
246///
247/// let sql = "SELECT a, b + 1 AS c FROM t";
248/// let expr = parse_one(sql, DialectType::Generic).unwrap();
249/// let node = lineage("c", &expr, None, false).unwrap();
250/// ```
251pub fn lineage(
252    column: &str,
253    sql: &Expression,
254    dialect: Option<DialectType>,
255    trim_selects: bool,
256) -> Result<LineageNode> {
257    let prepared = prepare_lineage_expression(sql, None, dialect, false)?;
258    lineage_from_column_ref(ColumnRef::Name(column), &prepared, dialect, trim_selects)
259}
260
261/// Build the lineage graph for the column at a zero-based output ordinal.
262pub fn lineage_at(
263    ordinal: usize,
264    sql: &Expression,
265    dialect: Option<DialectType>,
266    trim_selects: bool,
267) -> Result<LineageNode> {
268    let prepared = prepare_lineage_expression(sql, None, dialect, false)?;
269    lineage_from_column_ref(ColumnRef::Index(ordinal), &prepared, dialect, trim_selects)
270}
271
272/// Build the lineage graph for a column in a SQL query using optional schema metadata.
273///
274/// When `schema` is provided, the query is first qualified with
275/// `optimizer::qualify_columns`, allowing more accurate lineage for unqualified or
276/// ambiguous column references.
277///
278/// # Arguments
279/// * `column` - The column name to trace lineage for
280/// * `sql` - The SQL expression (SELECT, UNION, etc.)
281/// * `schema` - Optional schema used for qualification
282/// * `dialect` - Optional dialect for qualification and lineage handling
283/// * `trim_selects` - If true, trim the source SELECT to only include the target column
284///
285/// # Returns
286/// The root lineage node for the specified column
287pub fn lineage_with_schema(
288    column: &str,
289    sql: &Expression,
290    schema: Option<&dyn Schema>,
291    dialect: Option<DialectType>,
292    trim_selects: bool,
293) -> Result<LineageNode> {
294    let prepared = prepare_lineage_expression(sql, schema, dialect, true)?;
295    lineage_from_column_ref(ColumnRef::Name(column), &prepared, dialect, trim_selects)
296}
297
298/// Build schema-aware lineage for the column at a zero-based output ordinal.
299pub fn lineage_at_with_schema(
300    ordinal: usize,
301    sql: &Expression,
302    schema: Option<&dyn Schema>,
303    dialect: Option<DialectType>,
304    trim_selects: bool,
305) -> Result<LineageNode> {
306    let prepared = prepare_lineage_expression(sql, schema, dialect, true)?;
307    lineage_from_column_ref(ColumnRef::Index(ordinal), &prepared, dialect, trim_selects)
308}
309
310/// Return the ordered output description of a query.
311pub fn output_columns(sql: &Expression, dialect: Option<DialectType>) -> Result<QueryOutput> {
312    let prepared = prepare_lineage_expression(sql, None, dialect, false)?;
313    query_output_from_expression(&prepared, dialect)
314}
315
316/// Return the ordered output description of a query after schema-aware expansion.
317pub fn output_columns_with_schema(
318    sql: &Expression,
319    schema: Option<&dyn Schema>,
320    dialect: Option<DialectType>,
321) -> Result<QueryOutput> {
322    let prepared = prepare_lineage_expression(sql, schema, dialect, true)?;
323    query_output_from_expression(&prepared, dialect)
324}
325
326fn prepare_lineage_expression(
327    sql: &Expression,
328    schema: Option<&dyn Schema>,
329    dialect: Option<DialectType>,
330    schema_aware: bool,
331) -> Result<Expression> {
332    let normalized = lineage_normalized_expression(sql);
333    let mut prepared = if schema_aware {
334        if let Some(schema) = schema {
335            qualify_schema_aware_expression(normalized.clone(), schema, dialect).map_err(
336                |error| {
337                    Error::internal(format!("Lineage qualification failed with schema: {error}"))
338                },
339            )?
340        } else {
341            normalized
342        }
343    } else {
344        normalized
345    };
346
347    if schema_aware {
348        annotate_types(&mut prepared, schema, dialect);
349        expand_cte_stars(&mut prepared, schema);
350    } else if has_lineage_with_clause(&prepared) {
351        expand_cte_stars(&mut prepared, None);
352    }
353
354    Ok(prepared)
355}
356
357fn lineage_from_column_ref(
358    column: ColumnRef<'_>,
359    sql: &Expression,
360    dialect: Option<DialectType>,
361    trim_selects: bool,
362) -> Result<LineageNode> {
363    let scope = build_scope(sql);
364    to_node(column, scope, dialect, "", "", "", trim_selects)
365}
366
367#[cfg(feature = "generate")]
368pub(crate) fn lineage_by_index_from_expression(
369    column_index: usize,
370    sql: &Expression,
371    dialect: Option<DialectType>,
372    trim_selects: bool,
373) -> Result<LineageNode> {
374    let prepared = prepare_lineage_expression(sql, None, dialect, false)?;
375    lineage_from_column_ref(
376        ColumnRef::Index(column_index),
377        &prepared,
378        dialect,
379        trim_selects,
380    )
381}
382
383fn lineage_normalized_expression(sql: &Expression) -> Expression {
384    match sql {
385        Expression::Prepare(prepare) => lineage_normalized_expression(&prepare.statement),
386        Expression::CreateTable(create) => create
387            .as_select
388            .as_ref()
389            .map(|query| attach_with_to_query(query.clone(), create.with_cte.clone()))
390            .unwrap_or_else(|| sql.clone()),
391        Expression::CreateView(create) => lineage_normalized_expression(&create.query),
392        Expression::Insert(insert) => insert
393            .query
394            .as_ref()
395            .map(|query| attach_with_to_query(query.clone(), insert.with.clone()))
396            .unwrap_or_else(|| sql.clone()),
397        _ => sql.clone(),
398    }
399}
400
401fn attach_with_to_query(
402    mut query: Expression,
403    with: Option<crate::expressions::With>,
404) -> Expression {
405    if let Some(with) = with {
406        attach_with_to_query_mut(&mut query, with);
407    }
408    query
409}
410
411fn attach_with_to_query_mut(query: &mut Expression, with: crate::expressions::With) {
412    match query {
413        Expression::Select(select) => {
414            if select.with.is_none() {
415                select.with = Some(with);
416            }
417        }
418        Expression::Union(union) => {
419            if union.with.is_none() {
420                union.with = Some(with);
421            }
422        }
423        Expression::Intersect(intersect) => {
424            if intersect.with.is_none() {
425                intersect.with = Some(with);
426            }
427        }
428        Expression::Except(except) => {
429            if except.with.is_none() {
430                except.with = Some(with);
431            }
432        }
433        Expression::Paren(paren) => attach_with_to_query_mut(&mut paren.this, with),
434        _ => {}
435    }
436}
437
438fn has_lineage_with_clause(expr: &Expression) -> bool {
439    match expr {
440        Expression::Select(select) => select.with.is_some(),
441        Expression::Union(union) => {
442            union.with.is_some()
443                || has_lineage_with_clause(&union.left)
444                || has_lineage_with_clause(&union.right)
445        }
446        Expression::Intersect(intersect) => {
447            intersect.with.is_some()
448                || has_lineage_with_clause(&intersect.left)
449                || has_lineage_with_clause(&intersect.right)
450        }
451        Expression::Except(except) => {
452            except.with.is_some()
453                || has_lineage_with_clause(&except.left)
454                || has_lineage_with_clause(&except.right)
455        }
456        Expression::Paren(paren) => has_lineage_with_clause(&paren.this),
457        _ => false,
458    }
459}
460
461// ---------------------------------------------------------------------------
462// CTE star expansion
463// ---------------------------------------------------------------------------
464
465/// Normalize an identifier for CTE name matching.
466///
467/// Follows SQL semantics: unquoted identifiers are case-insensitive (lowercased),
468/// quoted identifiers preserve their original case. This matches sqlglot's
469/// `normalize_identifiers` behavior.
470fn normalize_cte_name(ident: &Identifier) -> String {
471    if ident.quoted {
472        ident.name.clone()
473    } else {
474        ident.name.to_lowercase()
475    }
476}
477
478/// Expand SELECT * in CTEs by walking CTE definitions in order and propagating
479/// resolved column lists. This handles nested CTEs (e.g., cte2 AS (SELECT * FROM cte1))
480/// which qualify_columns cannot resolve because it processes each SELECT independently.
481///
482/// When `schema` is provided, stars from external tables (not CTEs) are also resolved
483/// by looking up column names in the schema. This enables correct expansion of patterns
484/// like `WITH cte AS (SELECT * FROM external_table) SELECT * FROM cte`.
485///
486/// CTE name matching follows SQL identifier semantics: unquoted names are compared
487/// case-insensitively (lowercased), while quoted names preserve their original case.
488/// This matches sqlglot's `normalize_identifiers` behavior.
489pub fn expand_cte_stars(expr: &mut Expression, schema: Option<&dyn Schema>) {
490    if let Expression::Prepare(prepare) = expr {
491        expand_cte_stars(&mut prepare.statement, schema);
492        return;
493    }
494
495    let resolved_cte_columns = {
496        let with = match query_with_mut(expr) {
497            Some(with) => with,
498            None => return,
499        };
500        let is_recursive_with = with.recursive;
501        let mut resolved_cte_columns: HashMap<String, Vec<String>> = HashMap::new();
502
503        for cte in &mut with.ctes {
504            let cte_name = normalize_cte_name(&cte.alias);
505            let explicit_columns = (!cte.columns.is_empty())
506                .then(|| cte.columns.iter().map(|c| c.name.clone()).collect());
507
508            // Skip recursive CTE bodies — resolving their self-references safely is
509            // more complex than ordered, non-recursive CTE propagation. Inspect every
510            // set-operation arm because the recursive reference normally appears in
511            // the right branch after a non-recursive base case.
512            if is_recursive_with && query_references_source(&cte.this, &cte_name) {
513                if let Some(columns) = explicit_columns {
514                    resolved_cte_columns.insert(cte_name, columns);
515                }
516                continue;
517            }
518
519            // Rewrite every SELECT arm, but derive the CTE's implicit output names
520            // from the leftmost arm only. Explicit CTE column aliases override those
521            // implicit names without preventing safe body expansion.
522            let implicit_columns =
523                rewrite_stars_in_query(&mut cte.this, &resolved_cte_columns, schema);
524            if let Some(columns) = explicit_columns.or(implicit_columns) {
525                resolved_cte_columns.insert(cte_name, columns);
526            }
527        }
528
529        resolved_cte_columns
530    };
531
532    // Also expand stars in every arm of the outer query. WITH can be attached
533    // directly to a root set operation, so limiting this to Expression::Select
534    // would skip the entire query.
535    rewrite_stars_in_query(expr, &resolved_cte_columns, schema);
536}
537
538/// Get the WITH clause attached to a query root, drilling through parentheses.
539fn query_with_mut(expr: &mut Expression) -> Option<&mut With> {
540    let mut current = expr;
541    loop {
542        match current {
543            Expression::Select(select) => return select.with.as_mut(),
544            Expression::Union(union) => return union.with.as_mut(),
545            Expression::Intersect(intersect) => return intersect.with.as_mut(),
546            Expression::Except(except) => return except.with.as_mut(),
547            Expression::Paren(p) => current = &mut p.this,
548            Expression::Subquery(subquery) => current = &mut subquery.this,
549            _ => return None,
550        }
551    }
552}
553
554/// Whether any SELECT arm in a query directly references `source_name`.
555///
556/// This is used to identify recursive CTEs whose self-reference commonly lives
557/// in a non-leftmost set-operation branch.
558fn query_references_source(expr: &Expression, source_name: &str) -> bool {
559    let mut stack = vec![expr];
560
561    while let Some(current) = stack.pop() {
562        match current {
563            Expression::Select(select) => {
564                if get_select_sources(select)
565                    .iter()
566                    .any(|source| source.normalized == source_name)
567                {
568                    return true;
569                }
570            }
571            Expression::Union(union) => {
572                stack.push(&union.right);
573                stack.push(&union.left);
574            }
575            Expression::Intersect(intersect) => {
576                stack.push(&intersect.right);
577                stack.push(&intersect.left);
578            }
579            Expression::Except(except) => {
580                stack.push(&except.right);
581                stack.push(&except.left);
582            }
583            Expression::Paren(paren) => stack.push(&paren.this),
584            Expression::Subquery(subquery) => stack.push(&subquery.this),
585            _ => {}
586        }
587    }
588
589    false
590}
591
592/// Rewrite stars in every SELECT arm of a query.
593///
594/// The traversal visits left branches first, so the first returned column list
595/// remains the set operation's output column list while all later arms are still
596/// rewritten independently. An explicit stack avoids adding recursion pressure
597/// for deeply nested set-operation chains.
598fn rewrite_stars_in_query(
599    expr: &mut Expression,
600    resolved_ctes: &HashMap<String, Vec<String>>,
601    schema: Option<&dyn Schema>,
602) -> Option<Vec<String>> {
603    let mut leftmost_columns = None;
604    let mut stack = vec![expr];
605
606    while let Some(current) = stack.pop() {
607        match current {
608            Expression::Select(select) => {
609                let columns = rewrite_stars_in_select(select, resolved_ctes, schema);
610                if leftmost_columns.is_none() {
611                    leftmost_columns = Some(columns);
612                }
613            }
614            Expression::Union(union) => {
615                stack.push(&mut union.right);
616                stack.push(&mut union.left);
617            }
618            Expression::Intersect(intersect) => {
619                stack.push(&mut intersect.right);
620                stack.push(&mut intersect.left);
621            }
622            Expression::Except(except) => {
623                stack.push(&mut except.right);
624                stack.push(&mut except.left);
625            }
626            Expression::Paren(paren) => stack.push(&mut paren.this),
627            Expression::Subquery(subquery) => stack.push(&mut subquery.this),
628            _ => {}
629        }
630    }
631
632    leftmost_columns
633}
634
635/// Rewrite star expressions in a SELECT using resolved CTE column lists.
636/// Falls back to `schema` for external table column lookup.
637/// Returns the list of output column names after expansion.
638fn rewrite_stars_in_select(
639    select: &mut Select,
640    resolved_ctes: &HashMap<String, Vec<String>>,
641    schema: Option<&dyn Schema>,
642) -> Vec<String> {
643    // The AST represents star expressions in two forms depending on syntax:
644    //   - `SELECT *`      → Expression::Star (unqualified star)
645    //   - `SELECT table.*` → Expression::Column { name: "*", table: Some(...) } (qualified star)
646    // Both must be checked to handle all star patterns.
647    let has_star = select
648        .expressions
649        .iter()
650        .any(|e| matches!(e, Expression::Star(_)));
651    let has_qualified_star = select
652        .expressions
653        .iter()
654        .any(|e| matches!(e, Expression::Column(c) if c.name.name == "*"));
655
656    if !has_star && !has_qualified_star {
657        // No stars — just extract column names without rewriting
658        return select
659            .expressions
660            .iter()
661            .filter_map(get_expression_output_name)
662            .collect();
663    }
664
665    let sources = get_select_sources(select);
666    let mut new_expressions = Vec::new();
667    let mut result_columns = Vec::new();
668
669    for expr in &select.expressions {
670        match expr {
671            Expression::Star(star) => {
672                let qual = star.table.as_ref();
673                if let Some(expanded) =
674                    expand_star_from_sources(qual, &sources, resolved_ctes, schema)
675                {
676                    for (src_alias, col_name) in &expanded {
677                        let table_id = Identifier::new(src_alias);
678                        new_expressions.push(make_column_expr(col_name, Some(&table_id)));
679                        result_columns.push(col_name.clone());
680                    }
681                } else {
682                    new_expressions.push(expr.clone());
683                    result_columns.push("*".to_string());
684                }
685            }
686            Expression::Column(c) if c.name.name == "*" => {
687                let qual = c.table.as_ref();
688                if let Some(expanded) =
689                    expand_star_from_sources(qual, &sources, resolved_ctes, schema)
690                {
691                    for (_src_alias, col_name) in &expanded {
692                        // Keep the original table qualifier for qualified stars (table.*)
693                        new_expressions.push(make_column_expr(col_name, c.table.as_ref()));
694                        result_columns.push(col_name.clone());
695                    }
696                } else {
697                    new_expressions.push(expr.clone());
698                    result_columns.push("*".to_string());
699                }
700            }
701            _ => {
702                new_expressions.push(expr.clone());
703                if let Some(name) = get_expression_output_name(expr) {
704                    result_columns.push(name);
705                }
706            }
707        }
708    }
709
710    select.expressions = new_expressions;
711    result_columns
712}
713
714/// Try to expand a star expression by looking up source columns from resolved CTEs,
715/// falling back to the schema for external tables.
716/// Returns (source_alias, column_name) pairs so the caller can set table qualifiers.
717/// `qualifier`: Optional table qualifier (for `table.*`). If None, expand all sources.
718fn expand_star_from_sources(
719    qualifier: Option<&Identifier>,
720    sources: &[SourceInfo],
721    resolved_ctes: &HashMap<String, Vec<String>>,
722    schema: Option<&dyn Schema>,
723) -> Option<Vec<(String, String)>> {
724    let mut expanded = Vec::new();
725
726    if let Some(qual) = qualifier {
727        // Qualified star: table.*
728        let qual_normalized = normalize_cte_name(qual);
729        for src in sources {
730            if src.normalized == qual_normalized || src.alias.to_lowercase() == qual_normalized {
731                // Try CTE first
732                if let Some(cols) = resolved_ctes.get(&src.normalized) {
733                    expanded.extend(cols.iter().map(|c| (src.alias.clone(), c.clone())));
734                    return Some(expanded);
735                }
736                // Fall back to schema
737                if let Some(cols) = lookup_schema_columns(schema, &src.fq_name) {
738                    expanded.extend(cols.into_iter().map(|c| (src.alias.clone(), c)));
739                    return Some(expanded);
740                }
741            }
742        }
743        None
744    } else {
745        // Unqualified star: expand all sources.
746        // Intentionally conservative: if any source can't be resolved, the entire
747        // expansion is aborted. Partial expansion would produce an incomplete column
748        // list, causing downstream lineage resolution to silently omit columns.
749        // This matches sqlglot's behavior (raises SqlglotError when schema is missing).
750        let mut any_expanded = false;
751        for src in sources {
752            if let Some(cols) = resolved_ctes.get(&src.normalized) {
753                expanded.extend(cols.iter().map(|c| (src.alias.clone(), c.clone())));
754                any_expanded = true;
755            } else if let Some(cols) = lookup_schema_columns(schema, &src.fq_name) {
756                expanded.extend(cols.into_iter().map(|c| (src.alias.clone(), c)));
757                any_expanded = true;
758            } else {
759                return None;
760            }
761        }
762        if any_expanded {
763            Some(expanded)
764        } else {
765            None
766        }
767    }
768}
769
770/// Look up column names for a table from the schema.
771fn lookup_schema_columns(schema: Option<&dyn Schema>, fq_name: &str) -> Option<Vec<String>> {
772    let schema = schema?;
773    if fq_name.is_empty() {
774        return None;
775    }
776    schema
777        .column_names(fq_name)
778        .ok()
779        .filter(|cols| !cols.is_empty() && !cols.contains(&"*".to_string()))
780}
781
782/// Create a Column expression with the given name and optional table qualifier.
783fn make_column_expr(name: &str, table: Option<&Identifier>) -> Expression {
784    Expression::Column(Box::new(crate::expressions::Column {
785        name: Identifier::new(name),
786        table: table.cloned(),
787        join_mark: false,
788        trailing_comments: Vec::new(),
789        span: None,
790        inferred_type: None,
791    }))
792}
793
794/// Extract the output name of a SELECT expression.
795fn get_expression_output_name(expr: &Expression) -> Option<String> {
796    match expr {
797        Expression::Alias(a) => Some(a.alias.name.clone()),
798        Expression::Column(c) => Some(c.name.name.clone()),
799        Expression::Identifier(id) => Some(id.name.clone()),
800        Expression::Star(_) => Some("*".to_string()),
801        _ => None,
802    }
803}
804
805/// Source info extracted from a SELECT's FROM/JOIN clauses in a single pass.
806struct SourceInfo {
807    alias: String,
808    /// Whether this source was introduced through a quoted identifier.
809    ///
810    /// The schema-less star passthrough heuristic must stay conservative for
811    /// quoted sources because unresolved quoted table names can be distinct
812    /// from similarly named CTEs that older scope paths still compare
813    /// case-insensitively.
814    quoted: bool,
815    /// Normalized name for CTE lookup: unquoted → lowercased, quoted → as-is.
816    normalized: String,
817    /// Fully-qualified table name for schema lookup (e.g., "db.schema.table").
818    fq_name: String,
819}
820
821/// Extract source info (alias, normalized CTE name, fully-qualified name) from a
822/// SELECT's FROM and JOIN clauses in a single pass.
823fn get_select_sources(select: &Select) -> Vec<SourceInfo> {
824    let mut sources = Vec::new();
825
826    fn extract_source(expr: &Expression) -> Option<SourceInfo> {
827        fn virtual_source_info(alias: &Identifier) -> SourceInfo {
828            SourceInfo {
829                alias: alias.name.clone(),
830                quoted: alias.quoted,
831                normalized: normalize_cte_name(alias),
832                fq_name: alias.name.clone(),
833            }
834        }
835
836        fn named_virtual_source_info(alias: &str) -> SourceInfo {
837            SourceInfo {
838                alias: alias.to_string(),
839                quoted: false,
840                normalized: alias.to_lowercase(),
841                fq_name: alias.to_string(),
842            }
843        }
844
845        match expr {
846            Expression::Table(t) => {
847                let normalized = normalize_cte_name(&t.name);
848                let alias = t
849                    .alias
850                    .as_ref()
851                    .map(|a| a.name.clone())
852                    .unwrap_or_else(|| t.name.name.clone());
853                let mut parts = Vec::new();
854                if let Some(catalog) = &t.catalog {
855                    parts.push(catalog.name.clone());
856                }
857                if let Some(schema) = &t.schema {
858                    parts.push(schema.name.clone());
859                }
860                parts.push(t.name.name.clone());
861                let fq_name = parts.join(".");
862                Some(SourceInfo {
863                    alias,
864                    quoted: t.name.quoted,
865                    normalized,
866                    fq_name,
867                })
868            }
869            Expression::Subquery(s) => {
870                let alias_identifier = s.alias.as_ref()?;
871                let alias = alias_identifier.name.clone();
872                let normalized = alias.to_lowercase();
873                let fq_name = alias.clone();
874                Some(SourceInfo {
875                    alias,
876                    quoted: alias_identifier.quoted,
877                    normalized,
878                    fq_name,
879                })
880            }
881            Expression::Unnest(u) => u.alias.as_ref().map(virtual_source_info),
882            Expression::Alias(a) if matches!(&a.this, Expression::Unnest(_)) => {
883                Some(virtual_source_info(&a.alias))
884            }
885            Expression::Alias(a) if is_query_like_relation(&a.this) => {
886                Some(virtual_source_info(&a.alias))
887            }
888            Expression::Lateral(lateral) => lateral.alias.as_deref().map(named_virtual_source_info),
889            Expression::LateralView(lateral_view) => lateral_view
890                .table_alias
891                .as_ref()
892                .or_else(|| lateral_view.column_aliases.first())
893                .map(virtual_source_info),
894            Expression::Pivot(pivot) => {
895                let alias = pivot_lineage_source_name(
896                    &pivot.this,
897                    pivot.alias.as_ref().map(|alias| alias.name.as_str()),
898                );
899                Some(SourceInfo {
900                    alias: alias.clone(),
901                    quoted: false,
902                    normalized: alias.to_lowercase(),
903                    fq_name: alias,
904                })
905            }
906            Expression::Unpivot(unpivot) => {
907                let alias = pivot_lineage_source_name(
908                    &unpivot.this,
909                    unpivot.alias.as_ref().map(|alias| alias.name.as_str()),
910                );
911                Some(SourceInfo {
912                    alias: alias.clone(),
913                    quoted: false,
914                    normalized: alias.to_lowercase(),
915                    fq_name: alias,
916                })
917            }
918            Expression::Paren(p) => extract_source(&p.this),
919            _ => None,
920        }
921    }
922
923    if let Some(from) = &select.from {
924        for expr in &from.expressions {
925            if let Some(info) = extract_source(expr) {
926                sources.push(info);
927            }
928        }
929    }
930    for join in &select.joins {
931        if is_semi_or_anti_join_kind(join.kind) {
932            continue;
933        }
934        if let Some(info) = extract_source(&join.this) {
935            sources.push(info);
936        }
937    }
938    for lateral_view in &select.lateral_views {
939        if let Some(info) = extract_source(&Expression::LateralView(Box::new(lateral_view.clone())))
940        {
941            sources.push(info);
942        }
943    }
944    sources
945}
946
947fn pivot_lineage_source_name(source: &Expression, explicit_alias: Option<&str>) -> String {
948    if let Some(alias) = explicit_alias {
949        return alias.to_string();
950    }
951
952    match source {
953        Expression::Table(table) => table
954            .alias
955            .as_ref()
956            .map(|alias| alias.name.clone())
957            .unwrap_or_else(|| table.name.name.clone()),
958        Expression::Subquery(subquery) => subquery
959            .alias
960            .as_ref()
961            .map(|alias| alias.name.clone())
962            .unwrap_or_else(|| "_0".to_string()),
963        Expression::Paren(paren) => pivot_lineage_source_name(&paren.this, explicit_alias),
964        _ => "_0".to_string(),
965    }
966}
967
968/// Get all source tables from a lineage graph
969pub fn get_source_tables(node: &LineageNode) -> HashSet<String> {
970    let mut tables = HashSet::new();
971    collect_source_tables(node, &mut tables);
972    tables
973}
974
975/// Recursively collect source table names from lineage graph
976pub fn collect_source_tables(node: &LineageNode, tables: &mut HashSet<String>) {
977    if let Expression::Table(table) = &node.source {
978        tables.insert(table.name.name.clone());
979    }
980    for child in &node.downstream {
981        collect_source_tables(child, tables);
982    }
983}
984
985// ---------------------------------------------------------------------------
986// Core recursive lineage builder
987// ---------------------------------------------------------------------------
988
989/// Maximum recursion depth for lineage tracing to prevent stack overflow
990/// on circular or deeply nested CTE chains.
991const MAX_LINEAGE_DEPTH: usize = 64;
992
993#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
994struct ScopeId(usize);
995
996struct IndexedScope {
997    scope: Scope,
998    subquery_scopes: Vec<ScopeId>,
999    derived_table_scopes: Vec<ScopeId>,
1000    cte_scopes: Vec<ScopeId>,
1001    union_scopes: Vec<ScopeId>,
1002}
1003
1004struct LineageScopeContext {
1005    scopes: Vec<IndexedScope>,
1006    /// Usage analysis must not claim undeclared qualifiers as physical tables.
1007    conservative: bool,
1008}
1009
1010/// Reuse the lineage resolver for analysis of non-output column occurrences.
1011/// The caller selects the lexical owner and checks ambiguous/open sources first.
1012/// This keeps CTE, derived-table, virtual-source and set-operation tracing shared
1013/// with projection lineage, without manufacturing SELECT projections.
1014pub(crate) struct ScopedLineage {
1015    context: LineageScopeContext,
1016    root: ScopeId,
1017    ctes: Vec<ScopeId>,
1018    dialect: Option<DialectType>,
1019}
1020
1021impl ScopedLineage {
1022    pub(crate) fn new(scope: Scope, inherited_ctes: &[Scope], dialect: DialectType) -> Self {
1023        let visible_ctes = scope.cte_sources.clone();
1024        let (mut context, root) = LineageScopeContext::from_scope(scope);
1025        context.conservative = true;
1026        let mut ctes = context.indexed(root).cte_scopes.clone();
1027        for cte_scope in inherited_ctes {
1028            if let Expression::Cte(cte) = &cte_scope.expression {
1029                if visible_ctes
1030                    .get(&cte.alias.name)
1031                    .is_some_and(|source| source.expression == cte_scope.expression)
1032                    && !ctes
1033                        .iter()
1034                        .any(|id| context.scope(*id).expression == cte_scope.expression)
1035                {
1036                    ctes.push(context.insert_scope(cte_scope.clone()));
1037                }
1038            }
1039        }
1040        Self {
1041            context,
1042            root,
1043            ctes,
1044            dialect: Some(dialect),
1045        }
1046    }
1047
1048    pub(crate) fn column(&self, source: &str, column: &str) -> LineageNode {
1049        let mut node = LineageNode::new(
1050            column,
1051            Expression::qualified_column(source, column),
1052            self.context.scope(self.root).expression.clone(),
1053        );
1054        resolve_qualified_column(
1055            &mut node,
1056            &self.context,
1057            self.root,
1058            self.dialect,
1059            source,
1060            column,
1061            column,
1062            false,
1063            &self.ctes,
1064            0,
1065        );
1066        node
1067    }
1068
1069    pub(crate) fn output(&self, ordinal: usize) -> Result<LineageNode> {
1070        to_node_inner(
1071            ColumnRef::Index(ordinal),
1072            &self.context,
1073            self.root,
1074            self.dialect,
1075            "",
1076            "",
1077            "",
1078            false,
1079            &self.ctes,
1080            0,
1081        )
1082    }
1083
1084    pub(crate) fn output_names(&self) -> Vec<String> {
1085        crate::ast_transforms::get_output_column_names_for_dialect(
1086            effective_scope_expression(&self.context.scope(self.root).expression),
1087            self.dialect,
1088        )
1089    }
1090
1091    pub(crate) fn subquery_output(&self, index: usize) -> Result<LineageNode> {
1092        let scope_id = *self
1093            .context
1094            .indexed(self.root)
1095            .subquery_scopes
1096            .get(index)
1097            .ok_or_else(|| Error::internal("missing scalar subquery scope"))?;
1098        to_node_inner(
1099            ColumnRef::Index(0),
1100            &self.context,
1101            scope_id,
1102            self.dialect,
1103            "",
1104            "",
1105            "",
1106            false,
1107            &self.ctes,
1108            0,
1109        )
1110    }
1111}
1112
1113impl LineageScopeContext {
1114    fn from_scope(scope: Scope) -> (Self, ScopeId) {
1115        let mut context = Self {
1116            scopes: Vec::new(),
1117            conservative: false,
1118        };
1119        let root = context.insert_scope(scope);
1120        (context, root)
1121    }
1122
1123    fn insert_scope(&mut self, mut scope: Scope) -> ScopeId {
1124        let subquery_scopes = std::mem::take(&mut scope.subquery_scopes)
1125            .into_iter()
1126            .map(|child| self.insert_scope(child))
1127            .collect();
1128        let derived_table_scopes = std::mem::take(&mut scope.derived_table_scopes)
1129            .into_iter()
1130            .map(|child| self.insert_scope(child))
1131            .collect();
1132        let cte_scopes = std::mem::take(&mut scope.cte_scopes)
1133            .into_iter()
1134            .map(|child| self.insert_scope(child))
1135            .collect();
1136        let union_scopes = std::mem::take(&mut scope.union_scopes)
1137            .into_iter()
1138            .map(|child| self.insert_scope(child))
1139            .collect();
1140
1141        let id = ScopeId(self.scopes.len());
1142        self.scopes.push(IndexedScope {
1143            scope,
1144            subquery_scopes,
1145            derived_table_scopes,
1146            cte_scopes,
1147            union_scopes,
1148        });
1149        id
1150    }
1151
1152    fn indexed(&self, id: ScopeId) -> &IndexedScope {
1153        &self.scopes[id.0]
1154    }
1155
1156    fn scope(&self, id: ScopeId) -> &Scope {
1157        &self.indexed(id).scope
1158    }
1159}
1160
1161/// Recursively build a lineage node for a column in a scope.
1162fn to_node(
1163    column: ColumnRef<'_>,
1164    scope: Scope,
1165    dialect: Option<DialectType>,
1166    scope_name: &str,
1167    source_name: &str,
1168    reference_node_name: &str,
1169    trim_selects: bool,
1170) -> Result<LineageNode> {
1171    let (context, scope_id) = LineageScopeContext::from_scope(scope);
1172    to_node_inner(
1173        column,
1174        &context,
1175        scope_id,
1176        dialect,
1177        scope_name,
1178        source_name,
1179        reference_node_name,
1180        trim_selects,
1181        &[],
1182        0,
1183    )
1184}
1185
1186fn to_node_inner(
1187    column: ColumnRef<'_>,
1188    context: &LineageScopeContext,
1189    scope_id: ScopeId,
1190    dialect: Option<DialectType>,
1191    scope_name: &str,
1192    source_name: &str,
1193    reference_node_name: &str,
1194    trim_selects: bool,
1195    ancestor_cte_scopes: &[ScopeId],
1196    depth: usize,
1197) -> Result<LineageNode> {
1198    if depth > MAX_LINEAGE_DEPTH {
1199        return Err(Error::internal(format!(
1200            "lineage recursion depth exceeded (>{MAX_LINEAGE_DEPTH}) — possible circular CTE reference for scope '{scope_name}'"
1201        )));
1202    }
1203    let scope = context.scope(scope_id);
1204    let scope_expr = &scope.expression;
1205
1206    // Build combined CTE scopes: current scope's cte_scopes + ancestors
1207    let mut all_cte_scopes = context.indexed(scope_id).cte_scopes.clone();
1208    all_cte_scopes.extend_from_slice(ancestor_cte_scopes);
1209    let descendant_cte_scopes = descendant_cte_scope_ids(&all_cte_scopes, scope_id);
1210
1211    // 0. Unwrap CTE scope — CTE scope expressions are Expression::Cte(...)
1212    //    but we need the inner query (SELECT/UNION) for column lookup.
1213    let effective_expr = effective_scope_expression(scope_expr);
1214
1215    // 1. Set operations (UNION / INTERSECT / EXCEPT)
1216    if matches!(
1217        effective_expr,
1218        Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_)
1219    ) {
1220        return handle_set_operation(
1221            &column,
1222            context,
1223            scope_id,
1224            effective_expr,
1225            matches!(scope_expr, Expression::Cte(_)).then_some(ScopeType::Root),
1226            dialect,
1227            scope_name,
1228            source_name,
1229            reference_node_name,
1230            trim_selects,
1231            &descendant_cte_scopes,
1232            depth,
1233        );
1234    }
1235
1236    // 2. Find the select expression for this column
1237    let select_expr = find_select_expr(effective_expr, &column, dialect)?;
1238    let column_name = resolve_column_name(&column, &select_expr);
1239
1240    // 3. Trim source if requested
1241    let node_source = if trim_selects {
1242        trim_source(effective_expr, &select_expr)
1243    } else {
1244        effective_expr.clone()
1245    };
1246
1247    // 4. Create the lineage node
1248    let mut node = LineageNode::new(&column_name, select_expr.clone(), node_source);
1249    apply_scope_context(&mut node, scope, source_name, reference_node_name);
1250
1251    // 5. Star handling — add downstream for each source
1252    if let Expression::Star(star) = &select_expr {
1253        let star_table = star
1254            .table
1255            .as_ref()
1256            .map(|identifier| identifier.name.as_str());
1257        for (name, source_info) in &scope.sources {
1258            if let Some(star_table) = star_table {
1259                let table_matches = name.eq_ignore_ascii_case(star_table)
1260                    || source_info
1261                        .alias
1262                        .as_deref()
1263                        .is_some_and(|alias| alias.eq_ignore_ascii_case(star_table))
1264                    || matches!(
1265                        &source_info.expression,
1266                        Expression::Table(table_ref)
1267                            if table_name_from_table_ref(table_ref).eq_ignore_ascii_case(star_table)
1268                    );
1269                if !table_matches {
1270                    continue;
1271                }
1272            }
1273
1274            let mut child = LineageNode::new(
1275                format!("{}.*", name),
1276                Expression::Star(crate::expressions::Star {
1277                    table: star.table.clone(),
1278                    except: None,
1279                    replace: None,
1280                    rename: None,
1281                    trailing_comments: vec![],
1282                    span: None,
1283                }),
1284                source_info.expression.clone(),
1285            );
1286            apply_source_info_context(&mut child, name, source_info);
1287            node.downstream.push(child);
1288        }
1289        return Ok(node);
1290    }
1291
1292    // 6. Subqueries in select — trace through scalar subqueries
1293    for query in query_expressions_in_scope(&select_expr) {
1294        for &sq_scope_id in &context.indexed(scope_id).subquery_scopes {
1295            if context.scope(sq_scope_id).expression == *query {
1296                if let Ok(child) = to_node_inner(
1297                    ColumnRef::Index(0),
1298                    context,
1299                    sq_scope_id,
1300                    dialect,
1301                    &column_name,
1302                    "",
1303                    "",
1304                    trim_selects,
1305                    &descendant_cte_scopes,
1306                    depth + 1,
1307                ) {
1308                    node.downstream.push(child);
1309                }
1310                break;
1311            }
1312        }
1313    }
1314
1315    // 7. Column references — trace each column to its source
1316    let col_refs = find_column_refs_in_expr_with_select(&select_expr, effective_expr, dialect);
1317    for col_ref in col_refs {
1318        let col_name = &col_ref.column;
1319        if let Some(ref table_id) = col_ref.table {
1320            let tbl = &table_id.name;
1321            resolve_qualified_column(
1322                &mut node,
1323                context,
1324                scope_id,
1325                dialect,
1326                tbl,
1327                col_name,
1328                &column_name,
1329                trim_selects,
1330                &all_cte_scopes,
1331                depth,
1332            );
1333        } else {
1334            if let Some(alias_expr) =
1335                find_prior_select_alias_expr(effective_expr, &select_expr, col_name, dialect)
1336            {
1337                for alias_ref in
1338                    find_column_refs_in_expr_with_select(&alias_expr, effective_expr, dialect)
1339                {
1340                    if let Some(ref table_id) = alias_ref.table {
1341                        resolve_qualified_column(
1342                            &mut node,
1343                            context,
1344                            scope_id,
1345                            dialect,
1346                            &table_id.name,
1347                            &alias_ref.column,
1348                            &column_name,
1349                            trim_selects,
1350                            &all_cte_scopes,
1351                            depth,
1352                        );
1353                    } else {
1354                        resolve_unqualified_column(
1355                            &mut node,
1356                            context,
1357                            scope_id,
1358                            dialect,
1359                            &alias_ref.column,
1360                            &column_name,
1361                            trim_selects,
1362                            &all_cte_scopes,
1363                            depth,
1364                        );
1365                    }
1366                }
1367                continue;
1368            }
1369
1370            resolve_unqualified_column(
1371                &mut node,
1372                context,
1373                scope_id,
1374                dialect,
1375                col_name,
1376                &column_name,
1377                trim_selects,
1378                &all_cte_scopes,
1379                depth,
1380            );
1381        }
1382    }
1383
1384    Ok(node)
1385}
1386
1387fn descendant_cte_scope_ids(all_cte_scopes: &[ScopeId], current_scope: ScopeId) -> Vec<ScopeId> {
1388    all_cte_scopes
1389        .iter()
1390        .copied()
1391        .filter(|scope| *scope != current_scope)
1392        .collect()
1393}
1394
1395fn effective_scope_expression(expr: &Expression) -> &Expression {
1396    match expr {
1397        Expression::Cte(cte) => effective_scope_expression(&cte.this),
1398        Expression::Subquery(subquery) => effective_scope_expression(&subquery.this),
1399        Expression::Paren(paren) => effective_scope_expression(&paren.this),
1400        other => other,
1401    }
1402}
1403
1404fn query_expressions_in_scope(expr: &Expression) -> Vec<&Expression> {
1405    let mut queries = Vec::new();
1406    let mut seen = HashSet::new();
1407
1408    for node in find_all_in_scope(
1409        expr,
1410        |node| {
1411            matches!(
1412                node,
1413                Expression::Subquery(subquery) if subquery.alias.is_none()
1414            ) || matches!(
1415                node,
1416                Expression::Exists(_) | Expression::In(_) | Expression::Any(_) | Expression::All(_)
1417            )
1418        },
1419        false,
1420    ) {
1421        let query = match node {
1422            Expression::Subquery(subquery) if subquery.alias.is_none() => Some(&subquery.this),
1423            Expression::Exists(exists) => Some(&exists.this),
1424            Expression::In(in_expr) => in_expr.query.as_ref(),
1425            Expression::Any(quantified) | Expression::All(quantified) => Some(&quantified.subquery),
1426            _ => None,
1427        };
1428
1429        if let Some(query) = query {
1430            let key = query as *const Expression as usize;
1431            if seen.insert(key) {
1432                queries.push(query);
1433            }
1434        }
1435    }
1436
1437    queries
1438}
1439
1440// ---------------------------------------------------------------------------
1441// Set operation handling
1442// ---------------------------------------------------------------------------
1443
1444fn set_branch_metadata(expression: &Expression, ordinal: usize) -> SetBranch {
1445    match expression {
1446        Expression::Union(union) => SetBranch {
1447            operator: SetOperator::Union,
1448            ordinal,
1449            all: union.all,
1450        },
1451        Expression::Intersect(intersect) => SetBranch {
1452            operator: SetOperator::Intersect,
1453            ordinal,
1454            all: intersect.all,
1455        },
1456        Expression::Except(except) => SetBranch {
1457            operator: SetOperator::Except,
1458            ordinal,
1459            all: except.all,
1460        },
1461        _ => unreachable!("set-operation metadata requires a set operation"),
1462    }
1463}
1464
1465fn handle_set_operation(
1466    column: &ColumnRef<'_>,
1467    context: &LineageScopeContext,
1468    scope_id: ScopeId,
1469    scope_expr: &Expression,
1470    scope_type_override: Option<ScopeType>,
1471    dialect: Option<DialectType>,
1472    scope_name: &str,
1473    source_name: &str,
1474    reference_node_name: &str,
1475    trim_selects: bool,
1476    ancestor_cte_scopes: &[ScopeId],
1477    depth: usize,
1478) -> Result<LineageNode> {
1479    let scope = context.scope(scope_id);
1480    let trace_wildcard_by_name =
1481        matches!(column, ColumnRef::Name(name) if normalize_column_name(name, dialect) == "*");
1482
1483    let aligned_layout = match crate::set_operation::set_operation_layout(scope_expr, dialect) {
1484        Ok(layout) => layout,
1485        Err(error) if error.is_indeterminate() => None,
1486        Err(error) => return Err(Error::invalid_input(error.to_string())),
1487    };
1488
1489    // Determine column index
1490    let col_index = match column {
1491        ColumnRef::Name(_) if trace_wildcard_by_name => 0,
1492        ColumnRef::Name(name) => column_to_index(scope_expr, name, dialect)?,
1493        ColumnRef::Index(i) => *i,
1494    };
1495
1496    if aligned_layout
1497        .as_ref()
1498        .is_some_and(|layout| col_index >= layout.outputs.len())
1499    {
1500        return Err(ordinal_resolution_error(
1501            col_index,
1502            ColumnResolutionReason::NotFound,
1503        ));
1504    }
1505
1506    let col_name = match column {
1507        ColumnRef::Name(name) => name.to_string(),
1508        ColumnRef::Index(_) => aligned_layout
1509            .as_ref()
1510            .and_then(|layout| layout.outputs.get(col_index))
1511            .map(|output| output.identifier.name.clone())
1512            .unwrap_or_else(|| format!("_{col_index}")),
1513    };
1514
1515    let mut node = LineageNode::new(&col_name, scope_expr.clone(), scope_expr.clone());
1516    if let Some(scope_type) = scope_type_override {
1517        apply_scope_context_with_type(
1518            &mut node,
1519            scope,
1520            scope_type,
1521            source_name,
1522            reference_node_name,
1523        );
1524    } else {
1525        apply_scope_context(&mut node, scope, source_name, reference_node_name);
1526    }
1527
1528    let mut resolution_failure = None;
1529
1530    // Recurse into each set-operation branch. Resolution failures are branch-local,
1531    // but genuine parser/internal errors must not be silently discarded.
1532    for (branch_ordinal, &branch_scope_id) in
1533        context.indexed(scope_id).union_scopes.iter().enumerate()
1534    {
1535        let branch_column = if trace_wildcard_by_name {
1536            match column {
1537                ColumnRef::Name(name) => ColumnRef::Name(name),
1538                ColumnRef::Index(_) => unreachable!("wildcard tracing is name-based"),
1539            }
1540        } else if let Some(layout) = &aligned_layout {
1541            let output = &layout.outputs[col_index];
1542            let branch_index = if branch_ordinal == 0 {
1543                output.left_ordinal
1544            } else {
1545                output.right_ordinal
1546            };
1547            let Some(branch_index) = branch_index else {
1548                // This branch contributes a dialect-inserted NULL for the
1549                // result slot, so it has no source expression to trace.
1550                continue;
1551            };
1552            ColumnRef::Index(branch_index)
1553        } else {
1554            ColumnRef::Index(col_index)
1555        };
1556
1557        match to_node_inner(
1558            branch_column,
1559            context,
1560            branch_scope_id,
1561            dialect,
1562            scope_name,
1563            "",
1564            "",
1565            trim_selects,
1566            ancestor_cte_scopes,
1567            depth + 1,
1568        ) {
1569            Ok(mut child) => {
1570                child.set_branch = Some(set_branch_metadata(scope_expr, branch_ordinal));
1571                node.downstream.push(child);
1572            }
1573            Err(Error::ColumnResolution { reason, .. }) => {
1574                resolution_failure = Some(merge_resolution_reason(resolution_failure, reason));
1575            }
1576            Err(error) => return Err(error),
1577        }
1578    }
1579
1580    if node.downstream.is_empty() {
1581        if let Some(reason) = resolution_failure {
1582            let target = match column {
1583                ColumnRef::Name(name) => ColumnResolutionTarget::Name {
1584                    name: name.to_string(),
1585                },
1586                ColumnRef::Index(ordinal) => ColumnResolutionTarget::Ordinal { ordinal: *ordinal },
1587            };
1588            return Err(column_resolution_error(target, reason));
1589        }
1590    }
1591
1592    Ok(node)
1593}
1594
1595fn merge_resolution_reason(
1596    current: Option<ColumnResolutionReason>,
1597    candidate: ColumnResolutionReason,
1598) -> ColumnResolutionReason {
1599    match (current, candidate) {
1600        (Some(ColumnResolutionReason::Ambiguous), _) | (_, ColumnResolutionReason::Ambiguous) => {
1601            ColumnResolutionReason::Ambiguous
1602        }
1603        (Some(ColumnResolutionReason::Indeterminate), _)
1604        | (_, ColumnResolutionReason::Indeterminate) => ColumnResolutionReason::Indeterminate,
1605        _ => ColumnResolutionReason::NotFound,
1606    }
1607}
1608
1609// ---------------------------------------------------------------------------
1610// Column resolution helpers
1611// ---------------------------------------------------------------------------
1612
1613fn resolve_qualified_column(
1614    node: &mut LineageNode,
1615    context: &LineageScopeContext,
1616    scope_id: ScopeId,
1617    dialect: Option<DialectType>,
1618    table: &str,
1619    col_name: &str,
1620    parent_name: &str,
1621    trim_selects: bool,
1622    all_cte_scopes: &[ScopeId],
1623    depth: usize,
1624) {
1625    let scope = context.scope(scope_id);
1626    // Resolve CTE alias: if `table` is a FROM alias for a CTE (e.g., `FROM my_cte AS t`),
1627    // resolve it to the actual CTE name so the CTE scope lookup succeeds.
1628    let resolved_cte_name = resolve_cte_alias(scope, table);
1629    let effective_table = resolved_cte_name.as_deref().unwrap_or(table);
1630
1631    if let Some(source_info) = scope
1632        .sources
1633        .get(table)
1634        .or_else(|| scope.sources.get(effective_table))
1635    {
1636        match &source_info.expression {
1637            Expression::Pivot(pivot) => {
1638                if attach_pivot_dependencies(
1639                    node,
1640                    context,
1641                    scope_id,
1642                    dialect,
1643                    pivot,
1644                    col_name,
1645                    trim_selects,
1646                    all_cte_scopes,
1647                    depth,
1648                ) {
1649                    return;
1650                }
1651            }
1652            Expression::Unpivot(unpivot) => {
1653                if attach_unpivot_dependencies(
1654                    node,
1655                    context,
1656                    scope_id,
1657                    dialect,
1658                    unpivot,
1659                    col_name,
1660                    trim_selects,
1661                    all_cte_scopes,
1662                    depth,
1663                ) {
1664                    return;
1665                }
1666            }
1667            _ => {}
1668        }
1669    }
1670
1671    // Check if table is a CTE reference — check both the current scope's cte_sources
1672    // and ancestor CTE scopes (for sibling CTEs in parent WITH clauses).
1673    let is_cte = scope.cte_sources.contains_key(effective_table)
1674        || all_cte_scopes.iter().any(
1675            |scope_id| matches!(&context.scope(*scope_id).expression, Expression::Cte(cte) if cte.alias.name == effective_table),
1676        );
1677    if is_cte {
1678        if let Some(child_scope_id) =
1679            find_child_scope_in(context, all_cte_scopes, scope_id, effective_table)
1680        {
1681            if let Ok(child) = to_node_inner(
1682                ColumnRef::Name(col_name),
1683                context,
1684                child_scope_id,
1685                dialect,
1686                parent_name,
1687                effective_table,
1688                parent_name,
1689                trim_selects,
1690                all_cte_scopes,
1691                depth + 1,
1692            ) {
1693                node.downstream.push(child);
1694                return;
1695            }
1696        }
1697
1698        if let Some(source_info) = scope
1699            .sources
1700            .get(table)
1701            .or_else(|| scope.sources.get(effective_table))
1702            .filter(|source_info| source_info.kind == SourceKind::Cte)
1703        {
1704            node.downstream.push(make_table_column_node_from_source(
1705                effective_table,
1706                col_name,
1707                source_info,
1708            ));
1709            return;
1710        }
1711    }
1712
1713    // Check if table is a derived table (is_scope = true in sources)
1714    if let Some(source_info) = scope.sources.get(table) {
1715        if source_info.is_scope {
1716            if let Some(child_scope_id) = find_child_scope(context, scope_id, table) {
1717                if let Ok(child) = to_node_inner(
1718                    ColumnRef::Name(col_name),
1719                    context,
1720                    child_scope_id,
1721                    dialect,
1722                    parent_name,
1723                    table,
1724                    parent_name,
1725                    trim_selects,
1726                    all_cte_scopes,
1727                    depth + 1,
1728                ) {
1729                    node.downstream.push(child);
1730                    return;
1731                }
1732            }
1733        }
1734    }
1735
1736    // Base table source found in current scope: preserve alias in the display name
1737    // but store the resolved table expression and name for downstream consumers.
1738    if let Some(source_info) = scope.sources.get(table) {
1739        if !source_info.is_scope {
1740            let mut child = make_table_column_node_from_source(table, col_name, source_info);
1741            if source_info.kind == SourceKind::Virtual {
1742                attach_virtual_source_dependencies(
1743                    &mut child,
1744                    context,
1745                    scope_id,
1746                    dialect,
1747                    table,
1748                    &source_info.expression,
1749                    trim_selects,
1750                    all_cte_scopes,
1751                    depth,
1752                );
1753            }
1754            node.downstream.push(child);
1755            return;
1756        }
1757    }
1758
1759    // Base table or unresolved — terminal node
1760    let mut child = make_table_column_node(table, col_name);
1761    if context.conservative {
1762        child.source_kind = SourceKind::Unknown;
1763    }
1764    node.downstream.push(child);
1765}
1766
1767fn attach_pivot_dependencies(
1768    node: &mut LineageNode,
1769    context: &LineageScopeContext,
1770    scope_id: ScopeId,
1771    dialect: Option<DialectType>,
1772    pivot: &crate::expressions::Pivot,
1773    col_name: &str,
1774    trim_selects: bool,
1775    all_cte_scopes: &[ScopeId],
1776    depth: usize,
1777) -> bool {
1778    if pivot.unpivot {
1779        return false;
1780    }
1781
1782    let scope = context.scope(scope_id);
1783    let mapping = pivot_lineage_column_mapping(pivot, scope, dialect);
1784    let Some(input_columns) = mapping.get(&normalize_column_name(col_name, dialect)) else {
1785        if pivot_implicit_source_column(pivot, col_name) {
1786            let col_ref = SimpleColumnRef {
1787                table: None,
1788                column: col_name.to_string(),
1789            };
1790            attach_pivot_input_column(
1791                node,
1792                context,
1793                scope_id,
1794                dialect,
1795                &pivot.this,
1796                &col_ref,
1797                trim_selects,
1798                all_cte_scopes,
1799                depth,
1800            );
1801            return true;
1802        }
1803        return false;
1804    };
1805
1806    for col_ref in input_columns {
1807        attach_pivot_input_column(
1808            node,
1809            context,
1810            scope_id,
1811            dialect,
1812            &pivot.this,
1813            col_ref,
1814            trim_selects,
1815            all_cte_scopes,
1816            depth,
1817        );
1818    }
1819    true
1820}
1821
1822fn attach_unpivot_dependencies(
1823    node: &mut LineageNode,
1824    context: &LineageScopeContext,
1825    scope_id: ScopeId,
1826    dialect: Option<DialectType>,
1827    unpivot: &crate::expressions::Unpivot,
1828    col_name: &str,
1829    trim_selects: bool,
1830    all_cte_scopes: &[ScopeId],
1831    depth: usize,
1832) -> bool {
1833    let mapping = unpivot_column_mapping(unpivot, dialect);
1834    let Some(input_columns) = mapping.get(&normalize_column_name(col_name, dialect)) else {
1835        return false;
1836    };
1837
1838    for col_ref in input_columns {
1839        attach_pivot_input_column(
1840            node,
1841            context,
1842            scope_id,
1843            dialect,
1844            &unpivot.this,
1845            col_ref,
1846            trim_selects,
1847            all_cte_scopes,
1848            depth,
1849        );
1850    }
1851    true
1852}
1853
1854fn pivot_column_mapping(
1855    pivot: &crate::expressions::Pivot,
1856    dialect: Option<DialectType>,
1857) -> HashMap<String, Vec<SimpleColumnRef>> {
1858    let aggregations = pivot_aggregation_expressions(pivot);
1859    let output_columns = pivot_generated_output_columns(pivot, dialect);
1860    if aggregations.is_empty() || output_columns.is_empty() {
1861        return HashMap::new();
1862    }
1863
1864    let mut mapping = HashMap::new();
1865    for (agg_index, agg) in aggregations.iter().enumerate() {
1866        let input_columns = find_column_refs_in_expr(agg, dialect);
1867        if input_columns.is_empty() {
1868            continue;
1869        }
1870        for col_index in (agg_index..output_columns.len()).step_by(aggregations.len()) {
1871            mapping.insert(
1872                normalize_column_name(&output_columns[col_index], dialect),
1873                input_columns.clone(),
1874            );
1875        }
1876    }
1877    mapping
1878}
1879
1880fn pivot_lineage_column_mapping(
1881    pivot: &crate::expressions::Pivot,
1882    scope: &Scope,
1883    dialect: Option<DialectType>,
1884) -> HashMap<String, Vec<SimpleColumnRef>> {
1885    let mut mapping = pivot_column_mapping(pivot, dialect);
1886    let Some(pre_pivot_columns) = pre_pivot_output_columns(&pivot.this, scope) else {
1887        return mapping;
1888    };
1889
1890    let output_columns = pivot_output_columns(pivot, &pre_pivot_columns, dialect);
1891    if output_columns.is_empty() {
1892        return mapping;
1893    }
1894
1895    let base_mapping = mapping.clone();
1896    for (post_name, pre_name) in output_columns {
1897        let normalized_pre = normalize_column_name(&pre_name, dialect);
1898        let normalized_post = normalize_column_name(&post_name, dialect);
1899
1900        if let Some(input_columns) = base_mapping.get(&normalized_pre) {
1901            mapping.insert(normalized_post, input_columns.clone());
1902        } else {
1903            mapping.insert(
1904                normalized_post,
1905                vec![SimpleColumnRef {
1906                    table: None,
1907                    column: pre_name,
1908                }],
1909            );
1910        }
1911    }
1912
1913    mapping
1914}
1915
1916fn pre_pivot_output_columns(source: &Expression, scope: &Scope) -> Option<Vec<String>> {
1917    match source {
1918        Expression::Subquery(subquery) => known_output_columns(&subquery.this),
1919        Expression::Table(table) if table.schema.is_none() && table.catalog.is_none() => scope
1920            .cte_sources
1921            .get(&table.name.name)
1922            .and_then(|source| known_output_columns(&source.expression)),
1923        Expression::Paren(paren) => pre_pivot_output_columns(&paren.this, scope),
1924        _ => None,
1925    }
1926}
1927
1928fn known_output_columns(expression: &Expression) -> Option<Vec<String>> {
1929    let expression = match expression {
1930        Expression::Cte(cte) => &cte.this,
1931        Expression::Subquery(subquery) => &subquery.this,
1932        other => other,
1933    };
1934    let columns = crate::ast_transforms::get_output_column_names(expression);
1935    if columns.is_empty() || columns.iter().any(|column| column == "*") {
1936        None
1937    } else {
1938        Some(columns)
1939    }
1940}
1941
1942fn pivot_output_columns(
1943    pivot: &crate::expressions::Pivot,
1944    pre_pivot_columns: &[String],
1945    dialect: Option<DialectType>,
1946) -> Vec<(String, String)> {
1947    let generated_outputs = pivot_generated_output_columns(pivot, dialect);
1948    let excluded = pivot_excluded_source_columns(pivot, dialect);
1949
1950    if excluded.is_empty() || generated_outputs.is_empty() {
1951        return Vec::new();
1952    }
1953
1954    let mut pre_rename: Vec<String> = pre_pivot_columns
1955        .iter()
1956        .filter(|column| !excluded.contains(&normalize_column_name(column, dialect)))
1957        .cloned()
1958        .collect();
1959    pre_rename.extend(generated_outputs);
1960
1961    let post_rename = if pivot.alias_columns.is_empty() {
1962        pre_rename.clone()
1963    } else {
1964        let mut names: Vec<String> = pivot
1965            .alias_columns
1966            .iter()
1967            .map(|column| column.name.clone())
1968            .collect();
1969        names.extend(pre_rename.iter().skip(names.len()).cloned());
1970        names
1971    };
1972
1973    post_rename.into_iter().zip(pre_rename).collect()
1974}
1975
1976fn pivot_excluded_source_columns(
1977    pivot: &crate::expressions::Pivot,
1978    dialect: Option<DialectType>,
1979) -> HashSet<String> {
1980    pivot
1981        .fields
1982        .iter()
1983        .chain(pivot.expressions.iter())
1984        .chain(pivot.using.iter())
1985        .flat_map(|expr| find_column_refs_in_expr(expr, dialect))
1986        .map(|column| normalize_column_name(&column.column, dialect))
1987        .collect()
1988}
1989
1990fn pivot_generated_output_columns(
1991    pivot: &crate::expressions::Pivot,
1992    _dialect: Option<DialectType>,
1993) -> Vec<String> {
1994    let fields = pivot_field_output_names(pivot);
1995    if fields.is_empty() {
1996        return Vec::new();
1997    }
1998
1999    let aggregations = pivot_aggregation_expressions(pivot);
2000    if aggregations.is_empty() {
2001        return Vec::new();
2002    }
2003
2004    let needs_suffix = aggregations.len() > 1;
2005    let mut outputs = Vec::new();
2006    for field in fields {
2007        for aggregation in aggregations {
2008            if let Some(suffix) = pivot_aggregation_output_suffix(aggregation, needs_suffix) {
2009                outputs.push(format!("{field}_{suffix}"));
2010            } else {
2011                outputs.push(field.clone());
2012            }
2013        }
2014    }
2015    outputs
2016}
2017
2018fn pivot_aggregation_expressions(pivot: &crate::expressions::Pivot) -> &[Expression] {
2019    if pivot.using.is_empty() {
2020        &pivot.expressions
2021    } else {
2022        &pivot.using
2023    }
2024}
2025
2026fn pivot_aggregation_output_suffix(expr: &Expression, needs_suffix: bool) -> Option<String> {
2027    match expr {
2028        Expression::Alias(alias) => Some(alias.alias.name.clone()),
2029        _ if needs_suffix => pivot_generated_aggregation_suffix(expr),
2030        _ => None,
2031    }
2032}
2033
2034#[cfg(feature = "generate")]
2035fn pivot_generated_aggregation_suffix(expr: &Expression) -> Option<String> {
2036    Generator::sql(expr).ok().map(|sql| sql.to_lowercase())
2037}
2038
2039#[cfg(not(feature = "generate"))]
2040fn pivot_generated_aggregation_suffix(expr: &Expression) -> Option<String> {
2041    pivot_expr_output_name(expr).or_else(|| Some(expr.variant_name().to_string()))
2042}
2043
2044fn pivot_field_output_names(pivot: &crate::expressions::Pivot) -> Vec<String> {
2045    let mut names = Vec::new();
2046    for field in &pivot.fields {
2047        if let Expression::In(in_expr) = field {
2048            for expr in &in_expr.expressions {
2049                if let Some(name) = pivot_expr_output_name(expr) {
2050                    names.push(name);
2051                }
2052            }
2053        }
2054    }
2055    names
2056}
2057
2058fn pivot_expr_output_name(expr: &Expression) -> Option<String> {
2059    match expr {
2060        Expression::PivotAlias(alias) => pivot_expr_output_name(&alias.alias),
2061        Expression::Alias(alias) => Some(alias.alias.name.clone()),
2062        Expression::Identifier(identifier) => Some(identifier.name.clone()),
2063        Expression::Column(column) => Some(column.name.name.clone()),
2064        Expression::Literal(literal) => Some(literal.value_str().to_string()),
2065        Expression::Var(var) => Some(var.this.clone()),
2066        Expression::Tuple(tuple) => tuple.expressions.first().and_then(pivot_expr_output_name),
2067        _ => None,
2068    }
2069}
2070
2071fn pivot_implicit_source_column(pivot: &crate::expressions::Pivot, col_name: &str) -> bool {
2072    let pivot_columns: HashSet<String> = pivot
2073        .fields
2074        .iter()
2075        .filter_map(|field| match field {
2076            Expression::In(in_expr) => Some(&in_expr.this),
2077            _ => None,
2078        })
2079        .flat_map(|expr| find_column_refs_in_expr(expr, None))
2080        .map(|col| col.column.to_lowercase())
2081        .collect();
2082    let aggregation_columns: HashSet<String> = pivot
2083        .expressions
2084        .iter()
2085        .flat_map(|expr| find_column_refs_in_expr(expr, None))
2086        .map(|col| col.column.to_lowercase())
2087        .collect();
2088
2089    let normalized = col_name.to_lowercase();
2090    !pivot_columns.contains(&normalized) && !aggregation_columns.contains(&normalized)
2091}
2092
2093fn unpivot_column_mapping(
2094    unpivot: &crate::expressions::Unpivot,
2095    dialect: Option<DialectType>,
2096) -> HashMap<String, Vec<SimpleColumnRef>> {
2097    let value_columns: Vec<String> = std::iter::once(unpivot.value_column.name.clone())
2098        .chain(
2099            unpivot
2100                .extra_value_columns
2101                .iter()
2102                .map(|column| column.name.clone()),
2103        )
2104        .collect();
2105    let mut all_input_columns = Vec::new();
2106    let mut value_input_columns: Vec<Vec<SimpleColumnRef>> = vec![Vec::new(); value_columns.len()];
2107
2108    for entry in &unpivot.columns {
2109        let columns = unpivot_entry_columns(entry);
2110        all_input_columns.extend(columns.clone());
2111        if columns.len() == value_columns.len() {
2112            for (idx, col_ref) in columns.into_iter().enumerate() {
2113                value_input_columns[idx].push(col_ref);
2114            }
2115        } else {
2116            for inputs in &mut value_input_columns {
2117                inputs.extend(columns.clone());
2118            }
2119        }
2120    }
2121
2122    let mut mapping = HashMap::new();
2123    mapping.insert(
2124        normalize_column_name(&unpivot.name_column.name, dialect),
2125        all_input_columns.clone(),
2126    );
2127    for (idx, value_column) in value_columns.iter().enumerate() {
2128        mapping.insert(
2129            normalize_column_name(value_column, dialect),
2130            value_input_columns.get(idx).cloned().unwrap_or_default(),
2131        );
2132    }
2133    mapping
2134}
2135
2136fn unpivot_entry_columns(expr: &Expression) -> Vec<SimpleColumnRef> {
2137    match expr {
2138        Expression::PivotAlias(alias) => unpivot_entry_columns(&alias.this),
2139        Expression::Tuple(tuple) => tuple
2140            .expressions
2141            .iter()
2142            .flat_map(unpivot_entry_columns)
2143            .collect(),
2144        Expression::Column(column) => vec![SimpleColumnRef {
2145            table: column.table.clone(),
2146            column: column.name.name.clone(),
2147        }],
2148        Expression::Identifier(identifier) => vec![SimpleColumnRef {
2149            table: None,
2150            column: identifier.name.clone(),
2151        }],
2152        _ => find_column_refs_in_expr(expr, None),
2153    }
2154}
2155
2156fn attach_pivot_input_column(
2157    node: &mut LineageNode,
2158    context: &LineageScopeContext,
2159    scope_id: ScopeId,
2160    dialect: Option<DialectType>,
2161    source_expr: &Expression,
2162    col_ref: &SimpleColumnRef,
2163    trim_selects: bool,
2164    all_cte_scopes: &[ScopeId],
2165    depth: usize,
2166) {
2167    let scope = context.scope(scope_id);
2168    match source_expr {
2169        Expression::Table(table) => {
2170            let table_name = col_ref
2171                .table
2172                .as_ref()
2173                .map(|identifier| identifier.name.as_str())
2174                .unwrap_or(table.name.name.as_str());
2175            if scope.cte_sources.contains_key(table_name) {
2176                resolve_qualified_column(
2177                    node,
2178                    context,
2179                    scope_id,
2180                    dialect,
2181                    table_name,
2182                    &col_ref.column,
2183                    &node.name.clone(),
2184                    trim_selects,
2185                    all_cte_scopes,
2186                    depth + 1,
2187                );
2188            } else {
2189                let mut source = ScopeSourceInfo::new(
2190                    Expression::Table(Box::new(table.as_ref().clone())),
2191                    false,
2192                    SourceKind::Table,
2193                );
2194                if let Some(alias) = &table.alias {
2195                    source = source.with_alias(alias.name.clone());
2196                }
2197                let source_key = table
2198                    .alias
2199                    .as_ref()
2200                    .map(|alias| alias.name.as_str())
2201                    .unwrap_or(table.name.name.as_str());
2202                node.downstream.push(make_table_column_node_from_source(
2203                    source_key,
2204                    &col_ref.column,
2205                    &source,
2206                ));
2207            }
2208        }
2209        Expression::Subquery(subquery) => {
2210            let Some(source_scope_id) =
2211                find_derived_scope_for_query(context, scope_id, &subquery.this)
2212            else {
2213                return;
2214            };
2215            let child = if let Some(table) = &col_ref.table {
2216                let mut child_node = LineageNode::new(
2217                    &col_ref.column,
2218                    subquery.this.clone(),
2219                    subquery.this.clone(),
2220                );
2221                resolve_qualified_column(
2222                    &mut child_node,
2223                    context,
2224                    source_scope_id,
2225                    dialect,
2226                    &table.name,
2227                    &col_ref.column,
2228                    &node.name.clone(),
2229                    trim_selects,
2230                    all_cte_scopes,
2231                    depth + 1,
2232                );
2233                Ok(child_node)
2234            } else {
2235                to_node_inner(
2236                    ColumnRef::Name(&col_ref.column),
2237                    context,
2238                    source_scope_id,
2239                    dialect,
2240                    "",
2241                    "",
2242                    "",
2243                    trim_selects,
2244                    all_cte_scopes,
2245                    depth + 1,
2246                )
2247            };
2248            if let Ok(child) = child {
2249                node.downstream.push(child);
2250            }
2251        }
2252        Expression::Paren(paren) => attach_pivot_input_column(
2253            node,
2254            context,
2255            scope_id,
2256            dialect,
2257            &paren.this,
2258            col_ref,
2259            trim_selects,
2260            all_cte_scopes,
2261            depth,
2262        ),
2263        _ => {
2264            if let Some(table) = &col_ref.table {
2265                resolve_qualified_column(
2266                    node,
2267                    context,
2268                    scope_id,
2269                    dialect,
2270                    &table.name,
2271                    &col_ref.column,
2272                    &node.name.clone(),
2273                    trim_selects,
2274                    all_cte_scopes,
2275                    depth + 1,
2276                );
2277            } else {
2278                node.downstream
2279                    .push(make_table_column_node("_", &col_ref.column));
2280            }
2281        }
2282    }
2283}
2284
2285/// Resolve a FROM alias to the original CTE name.
2286///
2287/// When a query uses `FROM my_cte AS alias`, the scope's `sources` map contains
2288/// `"alias"` → CTE expression, but `cte_sources` only contains `"my_cte"`.
2289/// This function checks if `name` is such an alias and returns the CTE name.
2290fn resolve_cte_alias(scope: &Scope, name: &str) -> Option<String> {
2291    // If it's already a known CTE name, no resolution needed
2292    if scope.cte_sources.contains_key(name) {
2293        return None;
2294    }
2295    // Check if the source's expression is a CTE — if so, extract the CTE name
2296    if let Some(source_info) = scope.sources.get(name) {
2297        if source_info.is_scope {
2298            if let Expression::Cte(cte) = &source_info.expression {
2299                let cte_name = &cte.alias.name;
2300                if scope.cte_sources.contains_key(cte_name) {
2301                    return Some(cte_name.clone());
2302                }
2303            }
2304        }
2305    }
2306    None
2307}
2308
2309fn resolve_unqualified_column(
2310    node: &mut LineageNode,
2311    context: &LineageScopeContext,
2312    scope_id: ScopeId,
2313    dialect: Option<DialectType>,
2314    col_name: &str,
2315    parent_name: &str,
2316    trim_selects: bool,
2317    all_cte_scopes: &[ScopeId],
2318    depth: usize,
2319) {
2320    let scope = context.scope(scope_id);
2321    // Try to find which source this column belongs to.
2322    // Build the source list from the actual FROM/JOIN clauses to avoid
2323    // mixing in CTE definitions that are in scope but not referenced.
2324    let from_source_names = source_names_from_from_join(scope);
2325
2326    if let Some(tbl) = unique_virtual_source_for_column(scope, &from_source_names, col_name) {
2327        resolve_qualified_column(
2328            node,
2329            context,
2330            scope_id,
2331            dialect,
2332            &tbl,
2333            col_name,
2334            parent_name,
2335            trim_selects,
2336            all_cte_scopes,
2337            depth,
2338        );
2339        return;
2340    }
2341
2342    if from_source_names.len() == 1 {
2343        let tbl = &from_source_names[0];
2344        resolve_qualified_column(
2345            node,
2346            context,
2347            scope_id,
2348            dialect,
2349            tbl,
2350            col_name,
2351            parent_name,
2352            trim_selects,
2353            all_cte_scopes,
2354            depth,
2355        );
2356        return;
2357    }
2358
2359    // Multiple sources — can't resolve without schema info, add unqualified node
2360    let child = LineageNode::new(
2361        col_name.to_string(),
2362        Expression::Column(Box::new(crate::expressions::Column {
2363            name: crate::expressions::Identifier::new(col_name.to_string()),
2364            table: None,
2365            join_mark: false,
2366            trailing_comments: vec![],
2367            span: None,
2368            inferred_type: None,
2369        })),
2370        node.source.clone(),
2371    );
2372    node.downstream.push(child);
2373}
2374
2375fn unique_virtual_source_for_column(
2376    scope: &Scope,
2377    source_names: &[String],
2378    col_name: &str,
2379) -> Option<String> {
2380    let mut matches = source_names.iter().filter_map(|source_name| {
2381        let source = scope.sources.get(source_name)?;
2382        if source.kind == SourceKind::Virtual
2383            && virtual_source_output_columns(source)
2384                .any(|column| column.eq_ignore_ascii_case(col_name))
2385        {
2386            Some(source_name.clone())
2387        } else {
2388            None
2389        }
2390    });
2391
2392    let first = matches.next()?;
2393    if matches.next().is_none() {
2394        Some(first)
2395    } else {
2396        None
2397    }
2398}
2399
2400fn virtual_source_output_columns(
2401    source_info: &ScopeSourceInfo,
2402) -> Box<dyn Iterator<Item = String> + '_> {
2403    match &source_info.expression {
2404        Expression::Unnest(unnest) => Box::new(unnest_output_columns(unnest)),
2405        Expression::Alias(alias) if matches!(&alias.this, Expression::Unnest(_)) => {
2406            Box::new(alias_output_columns(alias))
2407        }
2408        Expression::Lateral(lateral) => Box::new(lateral_output_columns(lateral)),
2409        Expression::LateralView(lateral_view) => {
2410            Box::new(lateral_view_output_columns(lateral_view))
2411        }
2412        _ => Box::new(source_info.alias.clone().into_iter()),
2413    }
2414}
2415
2416fn unnest_output_types(unnest: &crate::expressions::UnnestFunc) -> Vec<DataType> {
2417    let element_type = |expression: &Expression| match expression.inferred_type() {
2418        Some(DataType::Array { element_type, .. }) => (**element_type).clone(),
2419        _ => DataType::Unknown,
2420    };
2421
2422    let mut types = vec![unnest
2423        .inferred_type
2424        .clone()
2425        .unwrap_or_else(|| element_type(&unnest.this))];
2426    types.extend(unnest.expressions.iter().map(element_type));
2427    if unnest.with_ordinality || unnest.offset_alias.is_some() {
2428        types.push(DataType::BigInt { length: None });
2429    }
2430    types
2431}
2432
2433fn virtual_source_column_type(source_info: &ScopeSourceInfo, column: &str) -> Option<DataType> {
2434    let find_type = |names: Vec<String>, types: Vec<DataType>| {
2435        names
2436            .iter()
2437            .position(|name| name.eq_ignore_ascii_case(column))
2438            .and_then(|index| types.get(index).cloned())
2439    };
2440
2441    match &source_info.expression {
2442        Expression::Unnest(unnest) => find_type(
2443            unnest_output_columns(unnest).collect(),
2444            unnest_output_types(unnest),
2445        ),
2446        Expression::Alias(alias) => match &alias.this {
2447            Expression::Unnest(unnest) => find_type(
2448                alias_output_columns(alias).collect(),
2449                unnest_output_types(unnest),
2450            ),
2451            _ => None,
2452        },
2453        Expression::Lateral(lateral) => match lateral.this.as_ref() {
2454            Expression::Unnest(unnest) => find_type(
2455                lateral_output_columns(lateral).collect(),
2456                unnest_output_types(unnest),
2457            ),
2458            _ => None,
2459        },
2460        _ => None,
2461    }
2462}
2463
2464fn unnest_output_columns(
2465    unnest: &crate::expressions::UnnestFunc,
2466) -> impl Iterator<Item = String> + '_ {
2467    unnest
2468        .alias
2469        .iter()
2470        .map(|alias| alias.name.clone())
2471        .chain(unnest.offset_alias.iter().map(|alias| alias.name.clone()))
2472}
2473
2474fn alias_output_columns(
2475    alias: &crate::expressions::Alias,
2476) -> Box<dyn Iterator<Item = String> + '_> {
2477    if alias.column_aliases.is_empty() {
2478        Box::new(std::iter::once(alias.alias.name.clone()))
2479    } else {
2480        Box::new(
2481            alias
2482                .column_aliases
2483                .iter()
2484                .map(|column| column.name.clone()),
2485        )
2486    }
2487}
2488
2489fn lateral_output_columns(
2490    lateral: &crate::expressions::Lateral,
2491) -> Box<dyn Iterator<Item = String> + '_> {
2492    if lateral.column_aliases.is_empty() {
2493        default_virtual_output_columns(&lateral.this)
2494    } else {
2495        Box::new(lateral.column_aliases.iter().cloned())
2496    }
2497}
2498
2499fn lateral_view_output_columns(
2500    lateral_view: &crate::expressions::LateralView,
2501) -> Box<dyn Iterator<Item = String> + '_> {
2502    Box::new(
2503        lateral_view
2504            .column_aliases
2505            .iter()
2506            .map(|column| column.name.clone()),
2507    )
2508}
2509
2510fn default_virtual_output_columns(expr: &Expression) -> Box<dyn Iterator<Item = String> + '_> {
2511    match expr {
2512        Expression::Unnest(unnest) => Box::new(unnest_output_columns(unnest)),
2513        Expression::Alias(alias) if matches!(&alias.this, Expression::Unnest(_)) => {
2514            alias_output_columns(alias)
2515        }
2516        Expression::Function(function) if function.name.eq_ignore_ascii_case("FLATTEN") => {
2517            Box::new(
2518                ["seq", "key", "path", "index", "value", "this"]
2519                    .into_iter()
2520                    .map(String::from),
2521            )
2522        }
2523        _ => Box::new(std::iter::empty()),
2524    }
2525}
2526
2527fn attach_virtual_source_dependencies(
2528    node: &mut LineageNode,
2529    context: &LineageScopeContext,
2530    scope_id: ScopeId,
2531    dialect: Option<DialectType>,
2532    source_alias: &str,
2533    source_expr: &Expression,
2534    trim_selects: bool,
2535    all_cte_scopes: &[ScopeId],
2536    depth: usize,
2537) {
2538    let scope = context.scope(scope_id);
2539    let parent_name = node.name.clone();
2540    let mut seen = HashSet::new();
2541    for col_ref in find_column_refs_in_expr(source_expr, dialect) {
2542        let key = (
2543            col_ref.table.as_ref().map(|t| t.name.clone()),
2544            col_ref.column.clone(),
2545        );
2546        if !seen.insert(key) {
2547            continue;
2548        }
2549
2550        if let Some(table_id) = col_ref.table {
2551            let table = table_id.name;
2552            if table == source_alias {
2553                continue;
2554            }
2555            resolve_qualified_column(
2556                node,
2557                context,
2558                scope_id,
2559                dialect,
2560                &table,
2561                &col_ref.column,
2562                &parent_name,
2563                trim_selects,
2564                all_cte_scopes,
2565                depth + 1,
2566            );
2567        } else {
2568            let non_virtual_sources = non_virtual_source_names_from_from_join(scope);
2569            if non_virtual_sources.len() == 1 {
2570                resolve_qualified_column(
2571                    node,
2572                    context,
2573                    scope_id,
2574                    dialect,
2575                    &non_virtual_sources[0],
2576                    &col_ref.column,
2577                    &parent_name,
2578                    trim_selects,
2579                    all_cte_scopes,
2580                    depth + 1,
2581                );
2582            }
2583        }
2584    }
2585}
2586
2587fn source_names_from_from_join(scope: &Scope) -> Vec<String> {
2588    fn source_name(expr: &Expression) -> Option<String> {
2589        match expr {
2590            Expression::Table(table) => Some(
2591                table
2592                    .alias
2593                    .as_ref()
2594                    .map(|a| a.name.clone())
2595                    .unwrap_or_else(|| table.name.name.clone()),
2596            ),
2597            Expression::Subquery(subquery) => {
2598                subquery.alias.as_ref().map(|alias| alias.name.clone())
2599            }
2600            Expression::Unnest(unnest) => unnest.alias.as_ref().map(|alias| alias.name.clone()),
2601            Expression::Alias(alias) if matches!(&alias.this, Expression::Unnest(_)) => {
2602                Some(alias.alias.name.clone())
2603            }
2604            Expression::Alias(alias) if is_query_like_relation(&alias.this) => {
2605                Some(alias.alias.name.clone())
2606            }
2607            Expression::Lateral(lateral) => lateral.alias.clone(),
2608            Expression::LateralView(lateral_view) => lateral_view
2609                .table_alias
2610                .as_ref()
2611                .or_else(|| lateral_view.column_aliases.first())
2612                .map(|alias| alias.name.clone()),
2613            Expression::Pivot(pivot) => Some(pivot_lineage_source_name(
2614                &pivot.this,
2615                pivot.alias.as_ref().map(|alias| alias.name.as_str()),
2616            )),
2617            Expression::Unpivot(unpivot) => Some(pivot_lineage_source_name(
2618                &unpivot.this,
2619                unpivot.alias.as_ref().map(|alias| alias.name.as_str()),
2620            )),
2621            Expression::Paren(paren) => source_name(&paren.this),
2622            _ => None,
2623        }
2624    }
2625
2626    let effective_expr = match &scope.expression {
2627        Expression::Cte(cte) => &cte.this,
2628        expr => expr,
2629    };
2630
2631    let mut names = Vec::new();
2632    let mut seen = std::collections::HashSet::new();
2633
2634    if let Expression::Select(select) = effective_expr {
2635        if let Some(from) = &select.from {
2636            for expr in &from.expressions {
2637                if let Some(name) = source_name(expr) {
2638                    if !name.is_empty() && seen.insert(name.clone()) {
2639                        names.push(name);
2640                    }
2641                }
2642            }
2643        }
2644        for join in &select.joins {
2645            if is_semi_or_anti_join_kind(join.kind) {
2646                continue;
2647            }
2648            if let Some(name) = source_name(&join.this) {
2649                if !name.is_empty() && seen.insert(name.clone()) {
2650                    names.push(name);
2651                }
2652            }
2653        }
2654        for lateral_view in &select.lateral_views {
2655            if let Some(name) =
2656                source_name(&Expression::LateralView(Box::new(lateral_view.clone())))
2657            {
2658                if !name.is_empty() && seen.insert(name.clone()) {
2659                    names.push(name);
2660                }
2661            }
2662        }
2663    }
2664
2665    names
2666}
2667
2668fn is_semi_or_anti_join_kind(kind: JoinKind) -> bool {
2669    matches!(
2670        kind,
2671        JoinKind::Semi
2672            | JoinKind::Anti
2673            | JoinKind::LeftSemi
2674            | JoinKind::LeftAnti
2675            | JoinKind::RightSemi
2676            | JoinKind::RightAnti
2677    )
2678}
2679
2680fn is_query_like_relation(expr: &Expression) -> bool {
2681    match expr {
2682        Expression::Select(_)
2683        | Expression::Subquery(_)
2684        | Expression::Union(_)
2685        | Expression::Intersect(_)
2686        | Expression::Except(_) => true,
2687        Expression::Paren(paren) => is_query_like_relation(&paren.this),
2688        _ => false,
2689    }
2690}
2691
2692fn derived_source_query(expr: &Expression) -> Option<&Expression> {
2693    match expr {
2694        Expression::Subquery(subquery) => Some(&subquery.this),
2695        Expression::Alias(alias) if is_query_like_relation(&alias.this) => Some(&alias.this),
2696        Expression::Select(_)
2697        | Expression::Union(_)
2698        | Expression::Intersect(_)
2699        | Expression::Except(_) => Some(expr),
2700        Expression::Paren(paren) => derived_source_query(&paren.this),
2701        _ => None,
2702    }
2703}
2704
2705fn expressions_equivalent_after_wrappers(left: &Expression, right: &Expression) -> bool {
2706    left == right || effective_scope_expression(left) == effective_scope_expression(right)
2707}
2708
2709fn non_virtual_source_names_from_from_join(scope: &Scope) -> Vec<String> {
2710    source_names_from_from_join(scope)
2711        .into_iter()
2712        .filter(|name| {
2713            !matches!(
2714                scope.sources.get(name).map(|source| source.kind),
2715                Some(SourceKind::Virtual)
2716            )
2717        })
2718        .collect()
2719}
2720
2721// ---------------------------------------------------------------------------
2722// Helper functions
2723// ---------------------------------------------------------------------------
2724
2725#[derive(Debug, Clone)]
2726struct OutputLayoutEntry {
2727    column: OutputColumn,
2728    projection_index: usize,
2729}
2730
2731fn query_output_from_expression(
2732    expression: &Expression,
2733    dialect: Option<DialectType>,
2734) -> Result<QueryOutput> {
2735    match crate::set_operation::set_operation_layout(expression, dialect) {
2736        Ok(Some(layout)) => {
2737            return Ok(QueryOutput {
2738                columns: layout
2739                    .outputs
2740                    .into_iter()
2741                    .enumerate()
2742                    .map(|(ordinal, output)| OutputColumn::Named {
2743                        name: output.identifier.name,
2744                        ordinal: Some(ordinal),
2745                    })
2746                    .collect(),
2747                ordinal_complete: true,
2748            })
2749        }
2750        Ok(None) => {}
2751        Err(error) if error.is_indeterminate() => {}
2752        Err(error) => return Err(Error::invalid_input(error.to_string())),
2753    }
2754
2755    let select = leftmost_output_select(expression).ok_or_else(|| {
2756        Error::invalid_input("output_columns requires a SELECT or set-operation query")
2757    })?;
2758    let entries = output_layout(select);
2759    let ordinal_complete = !entries
2760        .iter()
2761        .any(|entry| matches!(entry.column, OutputColumn::Wildcard { .. }));
2762
2763    Ok(QueryOutput {
2764        columns: entries.into_iter().map(|entry| entry.column).collect(),
2765        ordinal_complete,
2766    })
2767}
2768
2769fn leftmost_output_select(expression: &Expression) -> Option<&Select> {
2770    match expression {
2771        Expression::Select(select) => Some(select),
2772        Expression::Union(set_op) => leftmost_output_select(&set_op.left),
2773        Expression::Intersect(set_op) => leftmost_output_select(&set_op.left),
2774        Expression::Except(set_op) => leftmost_output_select(&set_op.left),
2775        Expression::Subquery(subquery) => leftmost_output_select(&subquery.this),
2776        Expression::Cte(cte) => leftmost_output_select(&cte.this),
2777        Expression::Paren(paren) => leftmost_output_select(&paren.this),
2778        _ => None,
2779    }
2780}
2781
2782fn output_layout(select: &Select) -> Vec<OutputLayoutEntry> {
2783    let mut entries = Vec::new();
2784    let mut next_ordinal = Some(0usize);
2785
2786    for (projection_index, projection) in select.expressions.iter().enumerate() {
2787        let projection = unwrap_output_annotation(projection);
2788
2789        if let Some(qualifier) = output_wildcard_qualifier(projection) {
2790            entries.push(OutputLayoutEntry {
2791                column: OutputColumn::Wildcard {
2792                    qualifier,
2793                    start_ordinal: next_ordinal,
2794                },
2795                projection_index,
2796            });
2797            next_ordinal = None;
2798            continue;
2799        }
2800
2801        if let Expression::Aliases(aliases) = projection {
2802            if !aliases.expressions.is_empty() {
2803                for alias in &aliases.expressions {
2804                    let ordinal = take_output_ordinal(&mut next_ordinal);
2805                    let column = get_alias_or_name(alias)
2806                        .map(|name| OutputColumn::Named { name, ordinal })
2807                        .unwrap_or(OutputColumn::Unnamed { ordinal });
2808                    entries.push(OutputLayoutEntry {
2809                        column,
2810                        projection_index,
2811                    });
2812                }
2813                continue;
2814            }
2815        }
2816
2817        let ordinal = take_output_ordinal(&mut next_ordinal);
2818        let column = get_alias_or_name(projection)
2819            .map(|name| OutputColumn::Named { name, ordinal })
2820            .unwrap_or(OutputColumn::Unnamed { ordinal });
2821        entries.push(OutputLayoutEntry {
2822            column,
2823            projection_index,
2824        });
2825    }
2826
2827    entries
2828}
2829
2830fn take_output_ordinal(next_ordinal: &mut Option<usize>) -> Option<usize> {
2831    let ordinal = *next_ordinal;
2832    if let Some(value) = ordinal {
2833        *next_ordinal = Some(value + 1);
2834    }
2835    ordinal
2836}
2837
2838fn unwrap_output_annotation(mut expression: &Expression) -> &Expression {
2839    while let Expression::Annotated(annotated) = expression {
2840        expression = &annotated.this;
2841    }
2842    expression
2843}
2844
2845/// Return `Some(qualifier)` for a wildcard, where the inner option is the qualifier.
2846fn output_wildcard_qualifier(expression: &Expression) -> Option<Option<String>> {
2847    match expression {
2848        Expression::Star(star) => Some(star.table.as_ref().map(|table| table.name.clone())),
2849        Expression::Column(column) if column.name.name == "*" => {
2850            Some(column.table.as_ref().map(|table| table.name.clone()))
2851        }
2852        _ => None,
2853    }
2854}
2855
2856fn column_resolution_error(
2857    target: ColumnResolutionTarget,
2858    reason: ColumnResolutionReason,
2859) -> Error {
2860    Error::column_resolution(target, reason)
2861}
2862
2863fn name_resolution_error(name: &str, reason: ColumnResolutionReason) -> Error {
2864    column_resolution_error(
2865        ColumnResolutionTarget::Name {
2866            name: name.to_string(),
2867        },
2868        reason,
2869    )
2870}
2871
2872fn ordinal_resolution_error(ordinal: usize, reason: ColumnResolutionReason) -> Error {
2873    column_resolution_error(ColumnResolutionTarget::Ordinal { ordinal }, reason)
2874}
2875
2876fn find_select_expr_by_name(
2877    select: &Select,
2878    name: &str,
2879    dialect: Option<DialectType>,
2880) -> Result<Expression> {
2881    let normalized_name = normalize_column_name(name, dialect);
2882    let layout = output_layout(select);
2883    let mut matches = Vec::new();
2884
2885    for entry in &layout {
2886        let is_match = match &entry.column {
2887            OutputColumn::Named {
2888                name: output_name, ..
2889            } => normalize_column_name(output_name, dialect) == normalized_name,
2890            OutputColumn::Wildcard { .. } => normalized_name == "*",
2891            OutputColumn::Unnamed { .. } => false,
2892        };
2893        if is_match {
2894            matches.push(entry.projection_index);
2895        }
2896    }
2897    match matches.as_slice() {
2898        [projection_index] => return Ok(select.expressions[*projection_index].clone()),
2899        [_, ..] => {
2900            return Err(name_resolution_error(
2901                name,
2902                ColumnResolutionReason::Ambiguous,
2903            ))
2904        }
2905        [] => {}
2906    }
2907
2908    if let Some(expression) = synthesize_star_passthrough_expr(select, name) {
2909        return Ok(expression);
2910    }
2911
2912    let reason = if layout
2913        .iter()
2914        .any(|entry| matches!(entry.column, OutputColumn::Wildcard { .. }))
2915    {
2916        ColumnResolutionReason::Indeterminate
2917    } else {
2918        ColumnResolutionReason::NotFound
2919    };
2920    Err(name_resolution_error(name, reason))
2921}
2922
2923fn find_select_expr_by_ordinal(select: &Select, ordinal: usize) -> Result<Expression> {
2924    let layout = output_layout(select);
2925
2926    for entry in &layout {
2927        match &entry.column {
2928            OutputColumn::Named {
2929                ordinal: Some(candidate),
2930                ..
2931            }
2932            | OutputColumn::Unnamed {
2933                ordinal: Some(candidate),
2934            } if *candidate == ordinal => {
2935                return Ok(select.expressions[entry.projection_index].clone())
2936            }
2937            OutputColumn::Wildcard { start_ordinal, .. } => {
2938                if match start_ordinal {
2939                    Some(start) => ordinal >= *start,
2940                    None => true,
2941                } {
2942                    return Err(ordinal_resolution_error(
2943                        ordinal,
2944                        ColumnResolutionReason::Indeterminate,
2945                    ));
2946                }
2947            }
2948            _ => {}
2949        }
2950    }
2951
2952    Err(ordinal_resolution_error(
2953        ordinal,
2954        ColumnResolutionReason::NotFound,
2955    ))
2956}
2957
2958fn output_name_to_ordinal(
2959    expression: &Expression,
2960    name: &str,
2961    dialect: Option<DialectType>,
2962) -> Result<usize> {
2963    match crate::set_operation::set_operation_layout(expression, dialect) {
2964        Ok(Some(layout)) => {
2965            let lookup = Identifier::new(name);
2966            let lookup_key = crate::set_operation::identifier_key(&lookup, dialect);
2967            let matches: Vec<_> = layout
2968                .outputs
2969                .iter()
2970                .enumerate()
2971                .filter(|(_, output)| {
2972                    (output.identifier.quoted && output.identifier.name == name)
2973                        || crate::set_operation::identifier_key(&output.identifier, dialect)
2974                            == lookup_key
2975                })
2976                .map(|(ordinal, _)| ordinal)
2977                .collect();
2978
2979            return match matches.as_slice() {
2980                [ordinal] => Ok(*ordinal),
2981                [_, ..] => Err(name_resolution_error(
2982                    name,
2983                    ColumnResolutionReason::Ambiguous,
2984                )),
2985                [] => Err(name_resolution_error(
2986                    name,
2987                    ColumnResolutionReason::NotFound,
2988                )),
2989            };
2990        }
2991        Ok(None) => {}
2992        Err(error) if error.is_indeterminate() => {}
2993        Err(error) => return Err(Error::invalid_input(error.to_string())),
2994    }
2995
2996    let select = leftmost_output_select(expression).ok_or_else(|| {
2997        Error::invalid_input("column resolution requires a SELECT or set-operation query")
2998    })?;
2999    let normalized_name = normalize_column_name(name, dialect);
3000    let layout = output_layout(select);
3001    let mut matches = Vec::new();
3002
3003    for entry in &layout {
3004        if let OutputColumn::Named {
3005            name: output_name,
3006            ordinal,
3007        } = &entry.column
3008        {
3009            if normalize_column_name(output_name, dialect) == normalized_name {
3010                matches.push(*ordinal);
3011            }
3012        }
3013    }
3014
3015    if matches.len() > 1 {
3016        return Err(name_resolution_error(
3017            name,
3018            ColumnResolutionReason::Ambiguous,
3019        ));
3020    }
3021    if let Some(ordinal) = matches.into_iter().next() {
3022        return ordinal
3023            .ok_or_else(|| name_resolution_error(name, ColumnResolutionReason::Indeterminate));
3024    }
3025
3026    let reason = if layout
3027        .iter()
3028        .any(|entry| matches!(entry.column, OutputColumn::Wildcard { .. }))
3029    {
3030        ColumnResolutionReason::Indeterminate
3031    } else {
3032        ColumnResolutionReason::NotFound
3033    };
3034    Err(name_resolution_error(name, reason))
3035}
3036
3037/// Get the alias or name of an expression
3038fn get_alias_or_name(expr: &Expression) -> Option<String> {
3039    match expr {
3040        Expression::Alias(alias) => Some(alias.alias.name.clone()),
3041        Expression::Column(col) => Some(col.name.name.clone()),
3042        Expression::Identifier(id) => Some(id.name.clone()),
3043        Expression::Star(_) => Some("*".to_string()),
3044        // Annotated wraps an expression with trailing comments (e.g. `SELECT\n-- comment\na`).
3045        // Unwrap to get the actual column/alias name from the inner expression.
3046        Expression::Annotated(a) => get_alias_or_name(&a.this),
3047        _ => None,
3048    }
3049}
3050
3051fn find_prior_select_alias_expr(
3052    scope_expr: &Expression,
3053    target_expr: &Expression,
3054    alias_name: &str,
3055    dialect: Option<DialectType>,
3056) -> Option<Expression> {
3057    let Expression::Select(select) = scope_expr else {
3058        return None;
3059    };
3060
3061    let normalized_alias = normalize_column_name(alias_name, dialect);
3062    for expr in &select.expressions {
3063        if expr == target_expr {
3064            return None;
3065        }
3066
3067        if let Expression::Alias(alias) = expr {
3068            if normalize_column_name(&alias.alias.name, dialect) == normalized_alias {
3069                return Some(alias.this.clone());
3070            }
3071        }
3072    }
3073
3074    None
3075}
3076
3077/// Resolve the display name for a column reference.
3078fn resolve_column_name(column: &ColumnRef<'_>, select_expr: &Expression) -> String {
3079    match column {
3080        ColumnRef::Name(n) => n.to_string(),
3081        ColumnRef::Index(_) => get_alias_or_name(select_expr).unwrap_or_else(|| "?".to_string()),
3082    }
3083}
3084
3085/// Find the select expression matching a column reference.
3086fn find_select_expr(
3087    scope_expr: &Expression,
3088    column: &ColumnRef<'_>,
3089    dialect: Option<DialectType>,
3090) -> Result<Expression> {
3091    if let Expression::Select(ref select) = scope_expr {
3092        match column {
3093            ColumnRef::Name(name) => find_select_expr_by_name(select, name, dialect),
3094            ColumnRef::Index(ordinal) => find_select_expr_by_ordinal(select, *ordinal),
3095        }
3096    } else {
3097        Err(Error::invalid_input(
3098            "column resolution requires a SELECT expression",
3099        ))
3100    }
3101}
3102
3103fn synthesize_star_passthrough_expr(select: &Select, name: &str) -> Option<Expression> {
3104    let sources = get_select_sources(select);
3105    if sources.is_empty() {
3106        return None;
3107    }
3108
3109    let mut candidate_aliases = Vec::new();
3110    let mut seen = HashSet::new();
3111
3112    for expr in &select.expressions {
3113        let aliases = match star_passthrough_source_aliases(expr, &sources) {
3114            StarPassthroughSources::None => continue,
3115            StarPassthroughSources::Ambiguous => return None,
3116            StarPassthroughSources::Aliases(aliases) => aliases,
3117        };
3118
3119        for alias in aliases {
3120            if seen.insert(alias.clone()) {
3121                candidate_aliases.push(alias);
3122            }
3123        }
3124    }
3125
3126    match candidate_aliases.as_slice() {
3127        [alias] => {
3128            let table = Identifier::new(alias.clone());
3129            Some(make_column_expr(name, Some(&table)))
3130        }
3131        _ => None,
3132    }
3133}
3134
3135enum StarPassthroughSources {
3136    None,
3137    Ambiguous,
3138    Aliases(Vec<String>),
3139}
3140
3141fn star_passthrough_source_aliases(
3142    expr: &Expression,
3143    sources: &[SourceInfo],
3144) -> StarPassthroughSources {
3145    match expr {
3146        Expression::Star(star) => star_source_aliases(star.table.as_ref(), sources),
3147        Expression::Column(column) if column.name.name == "*" => {
3148            star_source_aliases(column.table.as_ref(), sources)
3149        }
3150        Expression::Annotated(annotated) => {
3151            star_passthrough_source_aliases(&annotated.this, sources)
3152        }
3153        _ => StarPassthroughSources::None,
3154    }
3155}
3156
3157fn star_source_aliases(
3158    qualifier: Option<&Identifier>,
3159    sources: &[SourceInfo],
3160) -> StarPassthroughSources {
3161    if let Some(qualifier) = qualifier {
3162        let mut aliases = Vec::new();
3163
3164        for source in sources {
3165            if source_matches_star_qualifier(source, qualifier) {
3166                aliases.push(source.alias.clone());
3167            }
3168        }
3169
3170        return match aliases.len() {
3171            0 => StarPassthroughSources::None,
3172            1 => StarPassthroughSources::Aliases(aliases),
3173            _ => StarPassthroughSources::Ambiguous,
3174        };
3175    }
3176
3177    match sources {
3178        // Do not synthesize a source column for unresolved quoted table stars.
3179        // This keeps quoted CTE/table case semantics intact while still allowing
3180        // the schema-less fallback for common unquoted SELECT * passthroughs.
3181        [source] if source.quoted => StarPassthroughSources::None,
3182        [source] => StarPassthroughSources::Aliases(vec![source.alias.clone()]),
3183        [] => StarPassthroughSources::None,
3184        _ => StarPassthroughSources::Ambiguous,
3185    }
3186}
3187
3188fn source_matches_star_qualifier(source: &SourceInfo, qualifier: &Identifier) -> bool {
3189    if source.normalized == normalize_cte_name(qualifier) {
3190        return true;
3191    }
3192
3193    if qualifier.quoted {
3194        source.alias == qualifier.name
3195    } else {
3196        source.alias.eq_ignore_ascii_case(&qualifier.name)
3197    }
3198}
3199
3200/// Find the positional index of a column name in a set operation's first SELECT branch.
3201fn column_to_index(
3202    set_op_expr: &Expression,
3203    name: &str,
3204    dialect: Option<DialectType>,
3205) -> Result<usize> {
3206    output_name_to_ordinal(set_op_expr, name, dialect)
3207}
3208
3209fn normalize_column_name(name: &str, dialect: Option<DialectType>) -> String {
3210    normalize_name(name, dialect, false, true)
3211}
3212
3213/// If trim_selects is enabled, return a copy of the SELECT with only the target column.
3214fn trim_source(select_expr: &Expression, target_expr: &Expression) -> Expression {
3215    if let Expression::Select(select) = select_expr {
3216        let mut trimmed = select.as_ref().clone();
3217        trimmed.expressions = vec![target_expr.clone()];
3218        Expression::Select(Box::new(trimmed))
3219    } else {
3220        select_expr.clone()
3221    }
3222}
3223
3224/// Find the child scope (CTE or derived table) for a given source name.
3225fn find_child_scope(
3226    context: &LineageScopeContext,
3227    scope_id: ScopeId,
3228    source_name: &str,
3229) -> Option<ScopeId> {
3230    let indexed = context.indexed(scope_id);
3231    let scope = &indexed.scope;
3232
3233    // Check CTE scopes
3234    if scope.cte_sources.contains_key(source_name) {
3235        for &cte_scope_id in &indexed.cte_scopes {
3236            let cte_scope = context.scope(cte_scope_id);
3237            if let Expression::Cte(cte) = &cte_scope.expression {
3238                if cte.alias.name == source_name {
3239                    return Some(cte_scope_id);
3240                }
3241            }
3242        }
3243    }
3244
3245    // Check derived table scopes
3246    if let Some(source_info) = scope.sources.get(source_name) {
3247        if source_info.is_scope && !scope.cte_sources.contains_key(source_name) {
3248            if let Some(query) = derived_source_query(&source_info.expression) {
3249                for &dt_scope_id in &indexed.derived_table_scopes {
3250                    let dt_scope = context.scope(dt_scope_id);
3251                    if expressions_equivalent_after_wrappers(&dt_scope.expression, query) {
3252                        return Some(dt_scope_id);
3253                    }
3254                }
3255            }
3256        }
3257    }
3258
3259    None
3260}
3261
3262/// Find a CTE scope by name, searching through a combined list of CTE scopes.
3263/// This handles nested CTEs where the current scope doesn't have the CTE scope
3264/// as a direct child but knows about it via cte_sources.
3265fn find_child_scope_in(
3266    context: &LineageScopeContext,
3267    all_cte_scopes: &[ScopeId],
3268    scope_id: ScopeId,
3269    source_name: &str,
3270) -> Option<ScopeId> {
3271    let indexed = context.indexed(scope_id);
3272    let scope = &indexed.scope;
3273
3274    // First try the scope's own cte_scopes
3275    for &cte_scope_id in &indexed.cte_scopes {
3276        let cte_scope = context.scope(cte_scope_id);
3277        if let Expression::Cte(cte) = &cte_scope.expression {
3278            if cte.alias.name == source_name {
3279                return Some(cte_scope_id);
3280            }
3281        }
3282    }
3283
3284    // Then search through all ancestor CTE scopes
3285    for &cte_scope_id in all_cte_scopes {
3286        let cte_scope = context.scope(cte_scope_id);
3287        if let Expression::Cte(cte) = &cte_scope.expression {
3288            if cte.alias.name == source_name {
3289                return Some(cte_scope_id);
3290            }
3291        }
3292    }
3293
3294    // Fall back to derived table scopes
3295    if let Some(source_info) = scope.sources.get(source_name) {
3296        if source_info.is_scope {
3297            if let Some(query) = derived_source_query(&source_info.expression) {
3298                for &dt_scope_id in &indexed.derived_table_scopes {
3299                    let dt_scope = context.scope(dt_scope_id);
3300                    if expressions_equivalent_after_wrappers(&dt_scope.expression, query) {
3301                        return Some(dt_scope_id);
3302                    }
3303                }
3304            }
3305        }
3306    }
3307
3308    None
3309}
3310
3311fn find_derived_scope_for_query(
3312    context: &LineageScopeContext,
3313    scope_id: ScopeId,
3314    query: &Expression,
3315) -> Option<ScopeId> {
3316    context
3317        .indexed(scope_id)
3318        .derived_table_scopes
3319        .iter()
3320        .copied()
3321        .find(|derived_scope_id| {
3322            expressions_equivalent_after_wrappers(
3323                &context.scope(*derived_scope_id).expression,
3324                query,
3325            )
3326        })
3327}
3328
3329/// Create a terminal lineage node for a table.column reference.
3330fn make_table_column_node(table: &str, column: &str) -> LineageNode {
3331    let mut node = LineageNode::new(
3332        format!("{}.{}", table, column),
3333        Expression::Column(Box::new(crate::expressions::Column {
3334            name: crate::expressions::Identifier::new(column.to_string()),
3335            table: Some(crate::expressions::Identifier::new(table.to_string())),
3336            join_mark: false,
3337            trailing_comments: vec![],
3338            span: None,
3339            inferred_type: None,
3340        })),
3341        Expression::Table(Box::new(crate::expressions::TableRef::new(table))),
3342    );
3343    node.source_name = table.to_string();
3344    node.source_kind = SourceKind::Table;
3345    node
3346}
3347
3348fn table_name_from_table_ref(table_ref: &crate::expressions::TableRef) -> String {
3349    let mut parts: Vec<String> = Vec::new();
3350    if let Some(catalog) = &table_ref.catalog {
3351        parts.push(catalog.name.clone());
3352    }
3353    if let Some(schema) = &table_ref.schema {
3354        parts.push(schema.name.clone());
3355    }
3356    parts.push(table_ref.name.name.clone());
3357    parts.join(".")
3358}
3359
3360fn apply_source_info_context(
3361    node: &mut LineageNode,
3362    source_key: &str,
3363    source_info: &ScopeSourceInfo,
3364) {
3365    node.source_kind = source_info.kind;
3366    node.source_name =
3367        source_info
3368            .lineage_name
3369            .clone()
3370            .unwrap_or_else(|| match &source_info.expression {
3371                Expression::Table(table_ref) => table_name_from_table_ref(table_ref),
3372                _ => source_key.to_string(),
3373            });
3374    node.source_alias = source_info.alias.clone();
3375}
3376
3377fn make_table_column_node_from_source(
3378    source_key: &str,
3379    column: &str,
3380    source_info: &ScopeSourceInfo,
3381) -> LineageNode {
3382    let lineage_name = source_info.lineage_name.as_deref().unwrap_or(source_key);
3383    let inferred_type = (source_info.kind == SourceKind::Virtual)
3384        .then(|| virtual_source_column_type(source_info, column))
3385        .flatten();
3386    let mut node = LineageNode::new(
3387        format!("{}.{}", lineage_name, column),
3388        Expression::Column(Box::new(crate::expressions::Column {
3389            name: crate::expressions::Identifier::new(column.to_string()),
3390            table: Some(crate::expressions::Identifier::new(
3391                lineage_name.to_string(),
3392            )),
3393            join_mark: false,
3394            trailing_comments: vec![],
3395            span: None,
3396            inferred_type,
3397        })),
3398        source_info.expression.clone(),
3399    );
3400
3401    apply_source_info_context(&mut node, source_key, source_info);
3402
3403    node
3404}
3405
3406/// Simple column reference extracted from an expression
3407#[derive(Debug, Clone)]
3408struct SimpleColumnRef {
3409    table: Option<crate::expressions::Identifier>,
3410    column: String,
3411}
3412
3413/// Find all column references in an expression (does not recurse into subqueries).
3414fn find_column_refs_in_expr(
3415    expr: &Expression,
3416    dialect: Option<DialectType>,
3417) -> Vec<SimpleColumnRef> {
3418    let mut refs = Vec::new();
3419    collect_column_refs(expr, dialect, &mut refs, None);
3420    refs
3421}
3422
3423fn find_column_refs_in_expr_with_select(
3424    expr: &Expression,
3425    select_expr: &Expression,
3426    dialect: Option<DialectType>,
3427) -> Vec<SimpleColumnRef> {
3428    let named_windows = match select_expr {
3429        Expression::Select(select) => select.windows.as_deref(),
3430        _ => None,
3431    };
3432    let mut refs = Vec::new();
3433    collect_column_refs(expr, dialect, &mut refs, named_windows);
3434    refs
3435}
3436
3437fn is_bigquery_safe_namespace_receiver(expr: &Expression) -> bool {
3438    match expr {
3439        Expression::Column(col) => {
3440            col.table.is_none() && !col.name.quoted && col.name.name.eq_ignore_ascii_case("SAFE")
3441        }
3442        Expression::Identifier(id) => !id.quoted && id.name.eq_ignore_ascii_case("SAFE"),
3443        _ => false,
3444    }
3445}
3446
3447fn collect_column_refs(
3448    expr: &Expression,
3449    dialect: Option<DialectType>,
3450    refs: &mut Vec<SimpleColumnRef>,
3451    named_windows: Option<&[NamedWindow]>,
3452) {
3453    let mut stack: Vec<&Expression> = vec![expr];
3454
3455    while let Some(current) = stack.pop() {
3456        match current {
3457            // === Leaf: collect Column references ===
3458            Expression::Column(col) => {
3459                refs.push(SimpleColumnRef {
3460                    table: col.table.clone(),
3461                    column: col.name.name.clone(),
3462                });
3463            }
3464
3465            // === Boundary: don't recurse into subqueries (handled separately) ===
3466            Expression::Subquery(_) | Expression::Exists(_) => {}
3467
3468            // === BinaryOp variants: left, right ===
3469            Expression::And(op)
3470            | Expression::Or(op)
3471            | Expression::Eq(op)
3472            | Expression::Neq(op)
3473            | Expression::Lt(op)
3474            | Expression::Lte(op)
3475            | Expression::Gt(op)
3476            | Expression::Gte(op)
3477            | Expression::Add(op)
3478            | Expression::Sub(op)
3479            | Expression::Mul(op)
3480            | Expression::Div(op)
3481            | Expression::Mod(op)
3482            | Expression::BitwiseAnd(op)
3483            | Expression::BitwiseOr(op)
3484            | Expression::BitwiseXor(op)
3485            | Expression::BitwiseLeftShift(op)
3486            | Expression::BitwiseRightShift(op)
3487            | Expression::Concat(op)
3488            | Expression::Adjacent(op)
3489            | Expression::TsMatch(op)
3490            | Expression::PropertyEQ(op)
3491            | Expression::ArrayContainsAll(op)
3492            | Expression::ArrayContainedBy(op)
3493            | Expression::ArrayOverlaps(op)
3494            | Expression::JSONBContainsAllTopKeys(op)
3495            | Expression::JSONBContainsAnyTopKeys(op)
3496            | Expression::JSONBDeleteAtPath(op)
3497            | Expression::ExtendsLeft(op)
3498            | Expression::ExtendsRight(op)
3499            | Expression::Is(op)
3500            | Expression::MemberOf(op)
3501            | Expression::NullSafeEq(op)
3502            | Expression::NullSafeNeq(op)
3503            | Expression::Glob(op)
3504            | Expression::Match(op) => {
3505                stack.push(&op.left);
3506                stack.push(&op.right);
3507            }
3508
3509            // === UnaryOp variants: this ===
3510            Expression::Not(u) | Expression::Neg(u) | Expression::BitwiseNot(u) => {
3511                stack.push(&u.this);
3512            }
3513
3514            // === UnaryFunc variants: this ===
3515            Expression::Upper(f)
3516            | Expression::Lower(f)
3517            | Expression::Length(f)
3518            | Expression::LTrim(f)
3519            | Expression::RTrim(f)
3520            | Expression::Reverse(f)
3521            | Expression::Abs(f)
3522            | Expression::Sqrt(f)
3523            | Expression::Cbrt(f)
3524            | Expression::Ln(f)
3525            | Expression::Exp(f)
3526            | Expression::Sign(f)
3527            | Expression::Date(f)
3528            | Expression::Time(f)
3529            | Expression::DateFromUnixDate(f)
3530            | Expression::UnixDate(f)
3531            | Expression::UnixSeconds(f)
3532            | Expression::UnixMillis(f)
3533            | Expression::UnixMicros(f)
3534            | Expression::TimeStrToDate(f)
3535            | Expression::DateToDi(f)
3536            | Expression::DiToDate(f)
3537            | Expression::TsOrDiToDi(f)
3538            | Expression::TsOrDsToDatetime(f)
3539            | Expression::TsOrDsToTimestamp(f)
3540            | Expression::YearOfWeek(f)
3541            | Expression::YearOfWeekIso(f)
3542            | Expression::Initcap(f)
3543            | Expression::Ascii(f)
3544            | Expression::Chr(f)
3545            | Expression::Soundex(f)
3546            | Expression::ByteLength(f)
3547            | Expression::Hex(f)
3548            | Expression::LowerHex(f)
3549            | Expression::Unicode(f)
3550            | Expression::Radians(f)
3551            | Expression::Degrees(f)
3552            | Expression::Sin(f)
3553            | Expression::Cos(f)
3554            | Expression::Tan(f)
3555            | Expression::Asin(f)
3556            | Expression::Acos(f)
3557            | Expression::Atan(f)
3558            | Expression::IsNan(f)
3559            | Expression::IsInf(f)
3560            | Expression::ArrayLength(f)
3561            | Expression::ArraySize(f)
3562            | Expression::Cardinality(f)
3563            | Expression::ArrayReverse(f)
3564            | Expression::ArrayDistinct(f)
3565            | Expression::ArrayFlatten(f)
3566            | Expression::ArrayCompact(f)
3567            | Expression::Explode(f)
3568            | Expression::ExplodeOuter(f)
3569            | Expression::ToArray(f)
3570            | Expression::MapFromEntries(f)
3571            | Expression::MapKeys(f)
3572            | Expression::MapValues(f)
3573            | Expression::JsonArrayLength(f)
3574            | Expression::JsonKeys(f)
3575            | Expression::JsonType(f)
3576            | Expression::ParseJson(f)
3577            | Expression::ToJson(f)
3578            | Expression::Typeof(f)
3579            | Expression::BitwiseCount(f)
3580            | Expression::Year(f)
3581            | Expression::Month(f)
3582            | Expression::Day(f)
3583            | Expression::Hour(f)
3584            | Expression::Minute(f)
3585            | Expression::Second(f)
3586            | Expression::DayOfWeek(f)
3587            | Expression::DayOfWeekIso(f)
3588            | Expression::DayOfMonth(f)
3589            | Expression::DayOfYear(f)
3590            | Expression::WeekOfYear(f)
3591            | Expression::Quarter(f)
3592            | Expression::Epoch(f)
3593            | Expression::EpochMs(f)
3594            | Expression::TimeStrToUnix(f)
3595            | Expression::SHA(f)
3596            | Expression::SHA1Digest(f)
3597            | Expression::TimeToUnix(f)
3598            | Expression::JSONBool(f)
3599            | Expression::Int64(f)
3600            | Expression::MD5NumberLower64(f)
3601            | Expression::MD5NumberUpper64(f)
3602            | Expression::DateStrToDate(f)
3603            | Expression::DateToDateStr(f) => {
3604                stack.push(&f.this);
3605            }
3606
3607            // === BinaryFunc variants: this, expression ===
3608            Expression::Power(f)
3609            | Expression::NullIf(f)
3610            | Expression::IfNull(f)
3611            | Expression::Nvl(f)
3612            | Expression::UnixToTimeStr(f)
3613            | Expression::Contains(f)
3614            | Expression::StartsWith(f)
3615            | Expression::EndsWith(f)
3616            | Expression::Levenshtein(f)
3617            | Expression::ModFunc(f)
3618            | Expression::Atan2(f)
3619            | Expression::IntDiv(f)
3620            | Expression::AddMonths(f)
3621            | Expression::MonthsBetween(f)
3622            | Expression::NextDay(f)
3623            | Expression::ArrayContains(f)
3624            | Expression::ArrayPosition(f)
3625            | Expression::ArrayAppend(f)
3626            | Expression::ArrayPrepend(f)
3627            | Expression::ArrayUnion(f)
3628            | Expression::ArrayExcept(f)
3629            | Expression::ArrayRemove(f)
3630            | Expression::StarMap(f)
3631            | Expression::MapFromArrays(f)
3632            | Expression::MapContainsKey(f)
3633            | Expression::ElementAt(f)
3634            | Expression::JsonMergePatch(f)
3635            | Expression::JSONBContains(f)
3636            | Expression::JSONBExtract(f) => {
3637                stack.push(&f.this);
3638                stack.push(&f.expression);
3639            }
3640
3641            // === VarArgFunc variants: expressions ===
3642            Expression::Greatest(f)
3643            | Expression::Least(f)
3644            | Expression::Coalesce(f)
3645            | Expression::ArrayConcat(f)
3646            | Expression::ArrayIntersect(f)
3647            | Expression::ArrayZip(f)
3648            | Expression::MapConcat(f)
3649            | Expression::JsonArray(f) => {
3650                for e in &f.expressions {
3651                    stack.push(e);
3652                }
3653            }
3654
3655            // === AggFunc variants: this, filter, having_max, limit ===
3656            Expression::Sum(f)
3657            | Expression::Avg(f)
3658            | Expression::Min(f)
3659            | Expression::Max(f)
3660            | Expression::ArrayAgg(f)
3661            | Expression::CountIf(f)
3662            | Expression::Stddev(f)
3663            | Expression::StddevPop(f)
3664            | Expression::StddevSamp(f)
3665            | Expression::Variance(f)
3666            | Expression::VarPop(f)
3667            | Expression::VarSamp(f)
3668            | Expression::Median(f)
3669            | Expression::Mode(f)
3670            | Expression::First(f)
3671            | Expression::Last(f)
3672            | Expression::AnyValue(f)
3673            | Expression::ApproxDistinct(f)
3674            | Expression::ApproxCountDistinct(f)
3675            | Expression::LogicalAnd(f)
3676            | Expression::LogicalOr(f)
3677            | Expression::Skewness(f)
3678            | Expression::ArrayConcatAgg(f)
3679            | Expression::ArrayUniqueAgg(f)
3680            | Expression::BoolXorAgg(f)
3681            | Expression::BitwiseAndAgg(f)
3682            | Expression::BitwiseOrAgg(f)
3683            | Expression::BitwiseXorAgg(f) => {
3684                stack.push(&f.this);
3685                if let Some(ref filter) = f.filter {
3686                    stack.push(filter);
3687                }
3688                if let Some((ref expr, _)) = f.having_max {
3689                    stack.push(expr);
3690                }
3691                if let Some(ref limit) = f.limit {
3692                    stack.push(limit);
3693                }
3694            }
3695
3696            // === Generic Function / AggregateFunction: args ===
3697            Expression::Function(func) => {
3698                for arg in &func.args {
3699                    stack.push(arg);
3700                }
3701            }
3702            Expression::AggregateFunction(func) => {
3703                for arg in &func.args {
3704                    stack.push(arg);
3705                }
3706                if let Some(ref filter) = func.filter {
3707                    stack.push(filter);
3708                }
3709                if let Some(ref limit) = func.limit {
3710                    stack.push(limit);
3711                }
3712            }
3713
3714            // === WindowFunction: this (skip Over for lineage purposes) ===
3715            Expression::WindowFunction(wf) => {
3716                stack.push(&wf.this);
3717                for e in &wf.over.partition_by {
3718                    stack.push(e);
3719                }
3720                for e in &wf.over.order_by {
3721                    stack.push(&e.this);
3722                }
3723                if let Some(keep) = &wf.keep {
3724                    for e in &keep.order_by {
3725                        stack.push(&e.this);
3726                    }
3727                }
3728                if let (Some(window_name), Some(named_windows)) =
3729                    (&wf.over.window_name, named_windows)
3730                {
3731                    for named_window in named_windows {
3732                        if named_window
3733                            .name
3734                            .name
3735                            .eq_ignore_ascii_case(&window_name.name)
3736                        {
3737                            for e in &named_window.spec.partition_by {
3738                                stack.push(e);
3739                            }
3740                            for e in &named_window.spec.order_by {
3741                                stack.push(&e.this);
3742                            }
3743                        }
3744                    }
3745                }
3746            }
3747
3748            // === Containers and special expressions ===
3749            Expression::Alias(a) => {
3750                stack.push(&a.this);
3751            }
3752            Expression::Cast(c) | Expression::TryCast(c) | Expression::SafeCast(c) => {
3753                stack.push(&c.this);
3754                if let Some(ref fmt) = c.format {
3755                    stack.push(fmt);
3756                }
3757                if let Some(ref def) = c.default {
3758                    stack.push(def);
3759                }
3760            }
3761            Expression::Paren(p) => {
3762                stack.push(&p.this);
3763            }
3764            Expression::Annotated(a) => {
3765                stack.push(&a.this);
3766            }
3767            Expression::Case(case) => {
3768                if let Some(ref operand) = case.operand {
3769                    stack.push(operand);
3770                }
3771                for (cond, result) in &case.whens {
3772                    stack.push(cond);
3773                    stack.push(result);
3774                }
3775                if let Some(ref else_expr) = case.else_ {
3776                    stack.push(else_expr);
3777                }
3778            }
3779            Expression::Collation(c) => {
3780                stack.push(&c.this);
3781            }
3782            Expression::In(i) => {
3783                stack.push(&i.this);
3784                for e in &i.expressions {
3785                    stack.push(e);
3786                }
3787                if let Some(ref q) = i.query {
3788                    stack.push(q);
3789                }
3790                if let Some(ref u) = i.unnest {
3791                    stack.push(u);
3792                }
3793            }
3794            Expression::Between(b) => {
3795                stack.push(&b.this);
3796                stack.push(&b.low);
3797                stack.push(&b.high);
3798            }
3799            Expression::IsNull(n) => {
3800                stack.push(&n.this);
3801            }
3802            Expression::IsTrue(t) | Expression::IsFalse(t) => {
3803                stack.push(&t.this);
3804            }
3805            Expression::IsJson(j) => {
3806                stack.push(&j.this);
3807            }
3808            Expression::Like(l) | Expression::ILike(l) => {
3809                stack.push(&l.left);
3810                stack.push(&l.right);
3811                if let Some(ref esc) = l.escape {
3812                    stack.push(esc);
3813                }
3814            }
3815            Expression::SimilarTo(s) => {
3816                stack.push(&s.this);
3817                stack.push(&s.pattern);
3818                if let Some(ref esc) = s.escape {
3819                    stack.push(esc);
3820                }
3821            }
3822            Expression::Ordered(o) => {
3823                stack.push(&o.this);
3824            }
3825            Expression::Array(a) => {
3826                for e in &a.expressions {
3827                    stack.push(e);
3828                }
3829            }
3830            Expression::Tuple(t) => {
3831                for e in &t.expressions {
3832                    stack.push(e);
3833                }
3834            }
3835            Expression::Struct(s) => {
3836                for (_, e) in &s.fields {
3837                    stack.push(e);
3838                }
3839            }
3840            Expression::Subscript(s) => {
3841                stack.push(&s.this);
3842                stack.push(&s.index);
3843            }
3844            Expression::Dot(d) => {
3845                stack.push(&d.this);
3846            }
3847            Expression::MethodCall(m) => {
3848                if !matches!(dialect, Some(DialectType::BigQuery))
3849                    || !is_bigquery_safe_namespace_receiver(&m.this)
3850                {
3851                    stack.push(&m.this);
3852                }
3853                for arg in &m.args {
3854                    stack.push(arg);
3855                }
3856            }
3857            Expression::ArraySlice(s) => {
3858                stack.push(&s.this);
3859                if let Some(ref start) = s.start {
3860                    stack.push(start);
3861                }
3862                if let Some(ref end) = s.end {
3863                    stack.push(end);
3864                }
3865            }
3866            Expression::Lambda(l) => {
3867                stack.push(&l.body);
3868            }
3869            Expression::NamedArgument(n) => {
3870                stack.push(&n.value);
3871            }
3872            Expression::Lateral(l) => {
3873                stack.push(&l.this);
3874                if let Some(ref view) = l.view {
3875                    stack.push(view);
3876                }
3877                if let Some(ref outer) = l.outer {
3878                    stack.push(outer);
3879                }
3880                if let Some(ref ordinality) = l.ordinality {
3881                    stack.push(ordinality);
3882                }
3883            }
3884            Expression::LateralView(lv) => {
3885                stack.push(&lv.this);
3886            }
3887            Expression::TryCatch(t) => {
3888                for stmt in &t.try_body {
3889                    stack.push(stmt);
3890                }
3891                if let Some(catch_body) = &t.catch_body {
3892                    for stmt in catch_body {
3893                        stack.push(stmt);
3894                    }
3895                }
3896            }
3897            Expression::BracedWildcard(e) | Expression::ReturnStmt(e) => {
3898                stack.push(e);
3899            }
3900
3901            // === Custom function structs ===
3902            Expression::Substring(f) => {
3903                stack.push(&f.this);
3904                stack.push(&f.start);
3905                if let Some(ref len) = f.length {
3906                    stack.push(len);
3907                }
3908            }
3909            Expression::Trim(f) => {
3910                stack.push(&f.this);
3911                if let Some(ref chars) = f.characters {
3912                    stack.push(chars);
3913                }
3914            }
3915            Expression::Replace(f) => {
3916                stack.push(&f.this);
3917                stack.push(&f.old);
3918                stack.push(&f.new);
3919            }
3920            Expression::IfFunc(f) => {
3921                stack.push(&f.condition);
3922                stack.push(&f.true_value);
3923                if let Some(ref fv) = f.false_value {
3924                    stack.push(fv);
3925                }
3926            }
3927            Expression::Nvl2(f) => {
3928                stack.push(&f.this);
3929                stack.push(&f.true_value);
3930                stack.push(&f.false_value);
3931            }
3932            Expression::ConcatWs(f) => {
3933                stack.push(&f.separator);
3934                for e in &f.expressions {
3935                    stack.push(e);
3936                }
3937            }
3938            Expression::Count(f) => {
3939                if let Some(ref this) = f.this {
3940                    stack.push(this);
3941                }
3942                if let Some(ref filter) = f.filter {
3943                    stack.push(filter);
3944                }
3945            }
3946            Expression::GroupConcat(f) => {
3947                stack.push(&f.this);
3948                if let Some(ref sep) = f.separator {
3949                    stack.push(sep);
3950                }
3951                if let Some(ref filter) = f.filter {
3952                    stack.push(filter);
3953                }
3954            }
3955            Expression::StringAgg(f) => {
3956                stack.push(&f.this);
3957                if let Some(ref sep) = f.separator {
3958                    stack.push(sep);
3959                }
3960                if let Some(ref filter) = f.filter {
3961                    stack.push(filter);
3962                }
3963                if let Some(ref limit) = f.limit {
3964                    stack.push(limit);
3965                }
3966            }
3967            Expression::ListAgg(f) => {
3968                stack.push(&f.this);
3969                if let Some(ref sep) = f.separator {
3970                    stack.push(sep);
3971                }
3972                if let Some(ref filter) = f.filter {
3973                    stack.push(filter);
3974                }
3975            }
3976            Expression::SumIf(f) => {
3977                stack.push(&f.this);
3978                stack.push(&f.condition);
3979                if let Some(ref filter) = f.filter {
3980                    stack.push(filter);
3981                }
3982            }
3983            Expression::DateAdd(f) | Expression::DateSub(f) => {
3984                stack.push(&f.this);
3985                stack.push(&f.interval);
3986            }
3987            Expression::DateDiff(f) => {
3988                stack.push(&f.this);
3989                stack.push(&f.expression);
3990            }
3991            Expression::DateTrunc(f) | Expression::TimestampTrunc(f) => {
3992                stack.push(&f.this);
3993            }
3994            Expression::Extract(f) => {
3995                stack.push(&f.this);
3996            }
3997            Expression::Round(f) => {
3998                stack.push(&f.this);
3999                if let Some(ref d) = f.decimals {
4000                    stack.push(d);
4001                }
4002            }
4003            Expression::Floor(f) => {
4004                stack.push(&f.this);
4005                if let Some(ref s) = f.scale {
4006                    stack.push(s);
4007                }
4008                if let Some(ref t) = f.to {
4009                    stack.push(t);
4010                }
4011            }
4012            Expression::Ceil(f) => {
4013                stack.push(&f.this);
4014                if let Some(ref d) = f.decimals {
4015                    stack.push(d);
4016                }
4017                if let Some(ref t) = f.to {
4018                    stack.push(t);
4019                }
4020            }
4021            Expression::Log(f) => {
4022                stack.push(&f.this);
4023                if let Some(ref b) = f.base {
4024                    stack.push(b);
4025                }
4026            }
4027            Expression::AtTimeZone(f) => {
4028                stack.push(&f.this);
4029                stack.push(&f.zone);
4030            }
4031            Expression::Lead(f) | Expression::Lag(f) => {
4032                stack.push(&f.this);
4033                if let Some(ref off) = f.offset {
4034                    stack.push(off);
4035                }
4036                if let Some(ref def) = f.default {
4037                    stack.push(def);
4038                }
4039            }
4040            Expression::FirstValue(f) | Expression::LastValue(f) => {
4041                stack.push(&f.this);
4042            }
4043            Expression::NthValue(f) => {
4044                stack.push(&f.this);
4045                stack.push(&f.offset);
4046            }
4047            Expression::Position(f) => {
4048                stack.push(&f.substring);
4049                stack.push(&f.string);
4050                if let Some(ref start) = f.start {
4051                    stack.push(start);
4052                }
4053            }
4054            Expression::Decode(f) => {
4055                stack.push(&f.this);
4056                for (search, result) in &f.search_results {
4057                    stack.push(search);
4058                    stack.push(result);
4059                }
4060                if let Some(ref def) = f.default {
4061                    stack.push(def);
4062                }
4063            }
4064            Expression::CharFunc(f) => {
4065                for arg in &f.args {
4066                    stack.push(arg);
4067                }
4068            }
4069            Expression::ArraySort(f) => {
4070                stack.push(&f.this);
4071                if let Some(ref cmp) = f.comparator {
4072                    stack.push(cmp);
4073                }
4074            }
4075            Expression::ArrayJoin(f) | Expression::ArrayToString(f) => {
4076                stack.push(&f.this);
4077                stack.push(&f.separator);
4078                if let Some(ref nr) = f.null_replacement {
4079                    stack.push(nr);
4080                }
4081            }
4082            Expression::ArrayFilter(f) => {
4083                stack.push(&f.this);
4084                stack.push(&f.filter);
4085            }
4086            Expression::ArrayTransform(f) => {
4087                stack.push(&f.this);
4088                stack.push(&f.transform);
4089            }
4090            Expression::Sequence(f)
4091            | Expression::Generate(f)
4092            | Expression::ExplodingGenerateSeries(f) => {
4093                stack.push(&f.start);
4094                stack.push(&f.stop);
4095                if let Some(ref step) = f.step {
4096                    stack.push(step);
4097                }
4098            }
4099            Expression::JsonExtract(f)
4100            | Expression::JsonExtractScalar(f)
4101            | Expression::JsonQuery(f)
4102            | Expression::JsonValue(f) => {
4103                stack.push(&f.this);
4104                stack.push(&f.path);
4105            }
4106            Expression::JsonExtractPath(f) | Expression::JsonRemove(f) => {
4107                stack.push(&f.this);
4108                for p in &f.paths {
4109                    stack.push(p);
4110                }
4111            }
4112            Expression::JsonObject(f) => {
4113                for (k, v) in &f.pairs {
4114                    stack.push(k);
4115                    stack.push(v);
4116                }
4117            }
4118            Expression::JsonSet(f) | Expression::JsonInsert(f) => {
4119                stack.push(&f.this);
4120                for (path, val) in &f.path_values {
4121                    stack.push(path);
4122                    stack.push(val);
4123                }
4124            }
4125            Expression::Overlay(f) => {
4126                stack.push(&f.this);
4127                stack.push(&f.replacement);
4128                stack.push(&f.from);
4129                if let Some(ref len) = f.length {
4130                    stack.push(len);
4131                }
4132            }
4133            Expression::Convert(f) => {
4134                stack.push(&f.this);
4135                if let Some(ref style) = f.style {
4136                    stack.push(style);
4137                }
4138            }
4139            Expression::ApproxPercentile(f) => {
4140                stack.push(&f.this);
4141                stack.push(&f.percentile);
4142                if let Some(ref acc) = f.accuracy {
4143                    stack.push(acc);
4144                }
4145                if let Some(ref filter) = f.filter {
4146                    stack.push(filter);
4147                }
4148            }
4149            Expression::Percentile(f)
4150            | Expression::PercentileCont(f)
4151            | Expression::PercentileDisc(f) => {
4152                stack.push(&f.this);
4153                stack.push(&f.percentile);
4154                if let Some(ref filter) = f.filter {
4155                    stack.push(filter);
4156                }
4157            }
4158            Expression::WithinGroup(f) => {
4159                stack.push(&f.this);
4160                for e in &f.order_by {
4161                    stack.push(&e.this);
4162                }
4163            }
4164            Expression::Left(f) | Expression::Right(f) => {
4165                stack.push(&f.this);
4166                stack.push(&f.length);
4167            }
4168            Expression::Repeat(f) => {
4169                stack.push(&f.this);
4170                stack.push(&f.times);
4171            }
4172            Expression::Lpad(f) | Expression::Rpad(f) => {
4173                stack.push(&f.this);
4174                stack.push(&f.length);
4175                if let Some(ref fill) = f.fill {
4176                    stack.push(fill);
4177                }
4178            }
4179            Expression::Split(f) => {
4180                stack.push(&f.this);
4181                stack.push(&f.delimiter);
4182            }
4183            Expression::RegexpLike(f) => {
4184                stack.push(&f.this);
4185                stack.push(&f.pattern);
4186                if let Some(ref flags) = f.flags {
4187                    stack.push(flags);
4188                }
4189            }
4190            Expression::RegexpReplace(f) => {
4191                stack.push(&f.this);
4192                stack.push(&f.pattern);
4193                stack.push(&f.replacement);
4194                if let Some(ref flags) = f.flags {
4195                    stack.push(flags);
4196                }
4197            }
4198            Expression::RegexpExtract(f) => {
4199                stack.push(&f.this);
4200                stack.push(&f.pattern);
4201                if let Some(ref group) = f.group {
4202                    stack.push(group);
4203                }
4204            }
4205            Expression::ToDate(f) => {
4206                stack.push(&f.this);
4207                if let Some(ref fmt) = f.format {
4208                    stack.push(fmt);
4209                }
4210            }
4211            Expression::ToTimestamp(f) => {
4212                stack.push(&f.this);
4213                if let Some(ref fmt) = f.format {
4214                    stack.push(fmt);
4215                }
4216            }
4217            Expression::DateFormat(f) | Expression::FormatDate(f) => {
4218                stack.push(&f.this);
4219                stack.push(&f.format);
4220            }
4221            Expression::LastDay(f) => {
4222                stack.push(&f.this);
4223            }
4224            Expression::FromUnixtime(f) => {
4225                stack.push(&f.this);
4226                if let Some(ref fmt) = f.format {
4227                    stack.push(fmt);
4228                }
4229            }
4230            Expression::UnixTimestamp(f) => {
4231                if let Some(ref this) = f.this {
4232                    stack.push(this);
4233                }
4234                if let Some(ref fmt) = f.format {
4235                    stack.push(fmt);
4236                }
4237            }
4238            Expression::MakeDate(f) => {
4239                stack.push(&f.year);
4240                stack.push(&f.month);
4241                stack.push(&f.day);
4242            }
4243            Expression::MakeTimestamp(f) => {
4244                stack.push(&f.year);
4245                stack.push(&f.month);
4246                stack.push(&f.day);
4247                stack.push(&f.hour);
4248                stack.push(&f.minute);
4249                stack.push(&f.second);
4250                if let Some(ref tz) = f.timezone {
4251                    stack.push(tz);
4252                }
4253            }
4254            Expression::TruncFunc(f) => {
4255                stack.push(&f.this);
4256                if let Some(ref d) = f.decimals {
4257                    stack.push(d);
4258                }
4259            }
4260            Expression::ArrayFunc(f) => {
4261                for e in &f.expressions {
4262                    stack.push(e);
4263                }
4264            }
4265            Expression::Unnest(f) => {
4266                stack.push(&f.this);
4267                for e in &f.expressions {
4268                    stack.push(e);
4269                }
4270            }
4271            Expression::StructFunc(f) => {
4272                for (_, e) in &f.fields {
4273                    stack.push(e);
4274                }
4275            }
4276            Expression::StructExtract(f) => {
4277                stack.push(&f.this);
4278            }
4279            Expression::NamedStruct(f) => {
4280                for (k, v) in &f.pairs {
4281                    stack.push(k);
4282                    stack.push(v);
4283                }
4284            }
4285            Expression::MapFunc(f) => {
4286                for k in &f.keys {
4287                    stack.push(k);
4288                }
4289                for v in &f.values {
4290                    stack.push(v);
4291                }
4292            }
4293            Expression::TransformKeys(f) | Expression::TransformValues(f) => {
4294                stack.push(&f.this);
4295                stack.push(&f.transform);
4296            }
4297            Expression::JsonArrayAgg(f) => {
4298                stack.push(&f.this);
4299                if let Some(ref filter) = f.filter {
4300                    stack.push(filter);
4301                }
4302            }
4303            Expression::JsonObjectAgg(f) => {
4304                stack.push(&f.key);
4305                stack.push(&f.value);
4306                if let Some(ref filter) = f.filter {
4307                    stack.push(filter);
4308                }
4309            }
4310            Expression::NTile(f) => {
4311                if let Some(ref n) = f.num_buckets {
4312                    stack.push(n);
4313                }
4314            }
4315            Expression::Rand(f) => {
4316                if let Some(ref s) = f.seed {
4317                    stack.push(s);
4318                }
4319                if let Some(ref lo) = f.lower {
4320                    stack.push(lo);
4321                }
4322                if let Some(ref hi) = f.upper {
4323                    stack.push(hi);
4324                }
4325            }
4326            Expression::Any(q) | Expression::All(q) => {
4327                stack.push(&q.this);
4328                stack.push(&q.subquery);
4329            }
4330            Expression::Overlaps(o) => {
4331                if let Some(ref this) = o.this {
4332                    stack.push(this);
4333                }
4334                if let Some(ref expr) = o.expression {
4335                    stack.push(expr);
4336                }
4337                if let Some(ref ls) = o.left_start {
4338                    stack.push(ls);
4339                }
4340                if let Some(ref le) = o.left_end {
4341                    stack.push(le);
4342                }
4343                if let Some(ref rs) = o.right_start {
4344                    stack.push(rs);
4345                }
4346                if let Some(ref re) = o.right_end {
4347                    stack.push(re);
4348                }
4349            }
4350            Expression::Interval(i) => {
4351                if let Some(ref this) = i.this {
4352                    stack.push(this);
4353                }
4354            }
4355            Expression::TimeStrToTime(f) => {
4356                stack.push(&f.this);
4357                if let Some(ref zone) = f.zone {
4358                    stack.push(zone);
4359                }
4360            }
4361            Expression::JSONBExtractScalar(f) => {
4362                stack.push(&f.this);
4363                stack.push(&f.expression);
4364                if let Some(ref jt) = f.json_type {
4365                    stack.push(jt);
4366                }
4367            }
4368            Expression::JSONExtract(f) => {
4369                stack.push(&f.this);
4370                stack.push(&f.expression);
4371                for e in &f.expressions {
4372                    stack.push(e);
4373                }
4374                if let Some(ref option) = f.option {
4375                    stack.push(option);
4376                }
4377                if let Some(ref on_condition) = f.on_condition {
4378                    stack.push(on_condition);
4379                }
4380            }
4381
4382            // === True leaves and non-expression-bearing nodes ===
4383            // Literals, Identifier, Star, DataType, Placeholder, Boolean, Null,
4384            // CurrentDate/Time/Timestamp, RowNumber, Rank, DenseRank, PercentRank,
4385            // CumeDist, Random, Pi, SessionUser, DDL statements, clauses, etc.
4386            _ => {}
4387        }
4388    }
4389}
4390
4391// ---------------------------------------------------------------------------
4392// Tests
4393// ---------------------------------------------------------------------------
4394
4395#[cfg(test)]
4396mod tests {
4397    use super::*;
4398    use crate::dialects::{Dialect, DialectType};
4399    use crate::expressions::DataType;
4400    use crate::optimizer::annotate_types::annotate_types;
4401    use crate::parse_one;
4402    use crate::schema::{MappingSchema, Schema};
4403
4404    fn parse(sql: &str) -> Expression {
4405        let dialect = Dialect::get(DialectType::Generic);
4406        let ast = dialect.parse(sql).unwrap();
4407        ast.into_iter().next().unwrap()
4408    }
4409
4410    fn parse_dialect(sql: &str, dialect_type: DialectType) -> Expression {
4411        let dialect = Dialect::get(dialect_type);
4412        let ast = dialect.parse(sql).unwrap();
4413        ast.into_iter().next().unwrap()
4414    }
4415
4416    fn lineage_names(node: &LineageNode) -> Vec<String> {
4417        node.walk().map(|n| n.name.clone()).collect()
4418    }
4419
4420    fn assert_lineage_contains(node: &LineageNode, expected: &str) {
4421        let names = lineage_names(node);
4422        assert!(
4423            names.iter().any(|name| name == expected),
4424            "expected {expected} in lineage, got {names:?}"
4425        );
4426    }
4427
4428    const ISSUE_368_SQL: &str = "with
4429base as (
4430  select 1 as col_a
4431),
4432literal_branch as (
4433  select 2 as col_a
4434),
4435unioned as (
4436  select * from base
4437  union all
4438  select * from literal_branch
4439)
4440select col_a from unioned";
4441
4442    #[test]
4443    fn test_simple_lineage() {
4444        let expr = parse("SELECT a FROM t");
4445        let node = lineage("a", &expr, None, false).unwrap();
4446
4447        assert_eq!(node.name, "a");
4448        assert!(!node.downstream.is_empty(), "Should have downstream nodes");
4449        // Should trace to t.a
4450        let names = node.downstream_names();
4451        assert!(
4452            names.iter().any(|n| n == "t.a"),
4453            "Expected t.a in downstream, got: {:?}",
4454            names
4455        );
4456    }
4457
4458    #[test]
4459    fn test_lineage_walk() {
4460        let root = LineageNode {
4461            name: "col_a".to_string(),
4462            expression: Expression::Null(crate::expressions::Null),
4463            source: Expression::Null(crate::expressions::Null),
4464            downstream: vec![LineageNode::new(
4465                "t.a",
4466                Expression::Null(crate::expressions::Null),
4467                Expression::Null(crate::expressions::Null),
4468            )],
4469            source_name: String::new(),
4470            source_kind: SourceKind::Unknown,
4471            source_alias: None,
4472            set_branch: None,
4473            reference_node_name: String::new(),
4474        };
4475
4476        let names: Vec<_> = root.walk().map(|n| n.name.clone()).collect();
4477        assert_eq!(names.len(), 2);
4478        assert_eq!(names[0], "col_a");
4479        assert_eq!(names[1], "t.a");
4480    }
4481
4482    #[test]
4483    fn test_aliased_column() {
4484        let expr = parse("SELECT a + 1 AS b FROM t");
4485        let node = lineage("b", &expr, None, false).unwrap();
4486
4487        assert_eq!(node.name, "b");
4488        // Should trace through the expression to t.a
4489        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
4490        assert!(
4491            all_names.iter().any(|n| n.contains("a")),
4492            "Expected to trace to column a, got: {:?}",
4493            all_names
4494        );
4495    }
4496
4497    #[test]
4498    fn test_qualified_column() {
4499        let expr = parse("SELECT t.a FROM t");
4500        let node = lineage("a", &expr, None, false).unwrap();
4501
4502        assert_eq!(node.name, "a");
4503        let names = node.downstream_names();
4504        assert!(
4505            names.iter().any(|n| n == "t.a"),
4506            "Expected t.a, got: {:?}",
4507            names
4508        );
4509    }
4510
4511    #[test]
4512    fn test_unqualified_column() {
4513        let expr = parse("SELECT a FROM t");
4514        let node = lineage("a", &expr, None, false).unwrap();
4515
4516        // Unqualified but single source → resolved to t.a
4517        let names = node.downstream_names();
4518        assert!(
4519            names.iter().any(|n| n == "t.a"),
4520            "Expected t.a, got: {:?}",
4521            names
4522        );
4523    }
4524
4525    #[test]
4526    fn test_lineage_with_schema_qualifies_root_expression_issue_40() {
4527        let query = "SELECT name FROM users";
4528        let dialect = Dialect::get(DialectType::BigQuery);
4529        let expr = dialect
4530            .parse(query)
4531            .unwrap()
4532            .into_iter()
4533            .next()
4534            .expect("expected one expression");
4535
4536        let mut schema = MappingSchema::with_dialect(DialectType::BigQuery);
4537        schema
4538            .add_table("users", &[("name".into(), DataType::Text)], None)
4539            .expect("schema setup");
4540
4541        let node_without_schema = lineage("name", &expr, Some(DialectType::BigQuery), false)
4542            .expect("lineage without schema");
4543        let mut expr_without = node_without_schema.expression.clone();
4544        annotate_types(
4545            &mut expr_without,
4546            Some(&schema),
4547            Some(DialectType::BigQuery),
4548        );
4549        assert_eq!(
4550            expr_without.inferred_type(),
4551            None,
4552            "Expected unresolved root type without schema-aware lineage qualification"
4553        );
4554
4555        let node_with_schema = lineage_with_schema(
4556            "name",
4557            &expr,
4558            Some(&schema),
4559            Some(DialectType::BigQuery),
4560            false,
4561        )
4562        .expect("lineage with schema");
4563        let mut expr_with = node_with_schema.expression.clone();
4564        annotate_types(&mut expr_with, Some(&schema), Some(DialectType::BigQuery));
4565
4566        assert_eq!(expr_with.inferred_type(), Some(&DataType::Text));
4567    }
4568
4569    #[test]
4570    fn test_lineage_with_schema_tolerates_partial_schema_for_known_column() {
4571        let expr = parse_dialect("SELECT order_id, amount FROM t", DialectType::DuckDB);
4572        let mut schema = MappingSchema::with_dialect(DialectType::DuckDB);
4573        schema
4574            .add_table(
4575                "t",
4576                &[("amount".into(), DataType::BigInt { length: None })],
4577                None,
4578            )
4579            .expect("schema setup");
4580
4581        let node = lineage_with_schema(
4582            "amount",
4583            &expr,
4584            Some(&schema),
4585            Some(DialectType::DuckDB),
4586            false,
4587        )
4588        .expect("lineage_with_schema should tolerate unrelated unknown columns");
4589
4590        assert_lineage_contains(&node, "t.amount");
4591    }
4592
4593    #[test]
4594    fn test_lineage_with_schema_tolerates_partial_schema_for_unknown_column() {
4595        let expr = parse_dialect("SELECT order_id, amount FROM t", DialectType::DuckDB);
4596        let mut schema = MappingSchema::with_dialect(DialectType::DuckDB);
4597        schema
4598            .add_table(
4599                "t",
4600                &[("amount".into(), DataType::BigInt { length: None })],
4601                None,
4602            )
4603            .expect("schema setup");
4604
4605        let node = lineage_with_schema(
4606            "order_id",
4607            &expr,
4608            Some(&schema),
4609            Some(DialectType::DuckDB),
4610            false,
4611        )
4612        .expect("lineage_with_schema should keep unknown selected columns");
4613
4614        assert_lineage_contains(&node, "t.order_id");
4615    }
4616
4617    #[test]
4618    fn test_lineage_with_schema_tolerates_partial_schema_for_join_conditions() {
4619        let expr = parse_dialect(
4620            "SELECT a.order_id, b.amount FROM t a JOIN u b ON a.id = b.id",
4621            DialectType::DuckDB,
4622        );
4623        let mut schema = MappingSchema::with_dialect(DialectType::DuckDB);
4624        schema
4625            .add_table(
4626                "t",
4627                &[("order_id".into(), DataType::BigInt { length: None })],
4628                None,
4629            )
4630            .expect("schema setup");
4631        schema
4632            .add_table(
4633                "u",
4634                &[("amount".into(), DataType::BigInt { length: None })],
4635                None,
4636            )
4637            .expect("schema setup");
4638
4639        let node = lineage_with_schema(
4640            "amount",
4641            &expr,
4642            Some(&schema),
4643            Some(DialectType::DuckDB),
4644            false,
4645        )
4646        .expect("lineage_with_schema should tolerate unknown join keys");
4647
4648        assert_lineage_contains(&node, "b.amount");
4649    }
4650
4651    #[test]
4652    fn test_lineage_with_schema_correlated_scalar_subquery() {
4653        let query = "SELECT id, (SELECT AVG(val) FROM t2 WHERE t2.id = t1.id) AS avg_val FROM t1";
4654        let dialect = Dialect::get(DialectType::BigQuery);
4655        let expr = dialect
4656            .parse(query)
4657            .unwrap()
4658            .into_iter()
4659            .next()
4660            .expect("expected one expression");
4661
4662        let mut schema = MappingSchema::with_dialect(DialectType::BigQuery);
4663        schema
4664            .add_table(
4665                "t1",
4666                &[("id".into(), DataType::BigInt { length: None })],
4667                None,
4668            )
4669            .expect("schema setup");
4670        schema
4671            .add_table(
4672                "t2",
4673                &[
4674                    ("id".into(), DataType::BigInt { length: None }),
4675                    ("val".into(), DataType::BigInt { length: None }),
4676                ],
4677                None,
4678            )
4679            .expect("schema setup");
4680
4681        let node = lineage_with_schema(
4682            "id",
4683            &expr,
4684            Some(&schema),
4685            Some(DialectType::BigQuery),
4686            false,
4687        )
4688        .expect("lineage_with_schema should handle correlated scalar subqueries");
4689
4690        assert_eq!(node.name, "id");
4691    }
4692
4693    #[test]
4694    fn test_lineage_with_schema_join_using() {
4695        let query = "SELECT a FROM t1 JOIN t2 USING(a)";
4696        let dialect = Dialect::get(DialectType::BigQuery);
4697        let expr = dialect
4698            .parse(query)
4699            .unwrap()
4700            .into_iter()
4701            .next()
4702            .expect("expected one expression");
4703
4704        let mut schema = MappingSchema::with_dialect(DialectType::BigQuery);
4705        schema
4706            .add_table(
4707                "t1",
4708                &[("a".into(), DataType::BigInt { length: None })],
4709                None,
4710            )
4711            .expect("schema setup");
4712        schema
4713            .add_table(
4714                "t2",
4715                &[("a".into(), DataType::BigInt { length: None })],
4716                None,
4717            )
4718            .expect("schema setup");
4719
4720        let node = lineage_with_schema(
4721            "a",
4722            &expr,
4723            Some(&schema),
4724            Some(DialectType::BigQuery),
4725            false,
4726        )
4727        .expect("lineage_with_schema should handle JOIN USING");
4728
4729        assert_eq!(node.name, "a");
4730    }
4731
4732    #[test]
4733    fn test_lineage_with_schema_natural_join_merged_column() {
4734        let query = "SELECT shared_key AS output_key FROM source_table \
4735                     NATURAL JOIN (SELECT shared_key FROM source_table) derived";
4736        let dialect = Dialect::get(DialectType::DuckDB);
4737        let expr = dialect
4738            .parse(query)
4739            .unwrap()
4740            .into_iter()
4741            .next()
4742            .expect("expected one expression");
4743
4744        let mut schema = MappingSchema::with_dialect(DialectType::DuckDB);
4745        schema
4746            .add_table(
4747                "source_table",
4748                &[(
4749                    "shared_key".into(),
4750                    DataType::VarChar {
4751                        length: None,
4752                        parenthesized_length: false,
4753                    },
4754                )],
4755                None,
4756            )
4757            .expect("schema setup");
4758
4759        let node = lineage_with_schema(
4760            "output_key",
4761            &expr,
4762            Some(&schema),
4763            Some(DialectType::DuckDB),
4764            false,
4765        )
4766        .expect("lineage_with_schema should resolve a NATURAL JOIN merged column");
4767
4768        let terminal_nodes: Vec<_> = node
4769            .walk()
4770            .filter(|candidate| candidate.downstream.is_empty())
4771            .collect();
4772        assert_eq!(
4773            terminal_nodes.len(),
4774            2,
4775            "both NATURAL JOIN inputs should remain visible: {node:#?}"
4776        );
4777        assert!(terminal_nodes.iter().all(|candidate| {
4778            candidate.source_kind == SourceKind::Table
4779                && candidate.source_name == "source_table"
4780                && candidate.name.ends_with(".shared_key")
4781        }));
4782    }
4783
4784    #[test]
4785    fn test_lineage_with_schema_propagates_type_through_anonymous_derived_table() {
4786        let expr = parse_dialect(
4787            "SELECT source_value FROM (SELECT source_value FROM source_table)",
4788            DialectType::DuckDB,
4789        );
4790        let mut schema = MappingSchema::with_dialect(DialectType::DuckDB);
4791        let expected = DataType::VarChar {
4792            length: None,
4793            parenthesized_length: false,
4794        };
4795        schema
4796            .add_table(
4797                "source_table",
4798                &[("source_value".into(), expected.clone())],
4799                None,
4800            )
4801            .expect("schema setup");
4802
4803        let node = lineage_with_schema(
4804            "source_value",
4805            &expr,
4806            Some(&schema),
4807            Some(DialectType::DuckDB),
4808            false,
4809        )
4810        .expect("lineage_with_schema should resolve an anonymous derived table");
4811
4812        assert_eq!(node.expression.inferred_type(), Some(&expected));
4813        let terminal_nodes: Vec<_> = node
4814            .walk()
4815            .filter(|candidate| candidate.downstream.is_empty())
4816            .collect();
4817        assert_eq!(terminal_nodes.len(), 1, "{node:#?}");
4818        assert_eq!(terminal_nodes[0].source_kind, SourceKind::Table);
4819        assert_eq!(terminal_nodes[0].source_name, "source_table");
4820        assert_eq!(terminal_nodes[0].name, "source_table.source_value");
4821    }
4822
4823    #[test]
4824    fn test_issue_412_duckdb_date_trunc_preserves_timestamp_lineage_type() {
4825        let expr = parse_dialect(
4826            "SELECT DATE_TRUNC('month', event_timestamp) AS truncated_timestamp \
4827             FROM source_table",
4828            DialectType::DuckDB,
4829        );
4830        let mut schema = MappingSchema::with_dialect(DialectType::DuckDB);
4831        let timestamp = DataType::Timestamp {
4832            precision: None,
4833            timezone: false,
4834        };
4835        schema
4836            .add_table(
4837                "source_table",
4838                &[("event_timestamp".into(), timestamp.clone())],
4839                None,
4840            )
4841            .expect("schema setup");
4842
4843        let node =
4844            lineage_at_with_schema(0, &expr, Some(&schema), Some(DialectType::DuckDB), false)
4845                .expect("schema-aware lineage");
4846
4847        assert_eq!(node.expression.inferred_type(), Some(&timestamp));
4848        let Expression::Alias(alias) = &node.expression else {
4849            panic!("expected aliased DATE_TRUNC projection");
4850        };
4851        assert_eq!(alias.this.inferred_type(), Some(&timestamp));
4852        let Expression::Function(function) = &alias.this else {
4853            panic!("expected generic DATE_TRUNC function");
4854        };
4855        assert_eq!(function.args[1].inferred_type(), Some(&timestamp));
4856    }
4857
4858    #[test]
4859    fn test_lineage_with_schema_qualified_table_name() {
4860        let query = "SELECT a FROM raw.t1";
4861        let dialect = Dialect::get(DialectType::BigQuery);
4862        let expr = dialect
4863            .parse(query)
4864            .unwrap()
4865            .into_iter()
4866            .next()
4867            .expect("expected one expression");
4868
4869        let mut schema = MappingSchema::with_dialect(DialectType::BigQuery);
4870        schema
4871            .add_table(
4872                "raw.t1",
4873                &[("a".into(), DataType::BigInt { length: None })],
4874                None,
4875            )
4876            .expect("schema setup");
4877
4878        let node = lineage_with_schema(
4879            "a",
4880            &expr,
4881            Some(&schema),
4882            Some(DialectType::BigQuery),
4883            false,
4884        )
4885        .expect("lineage_with_schema should handle dotted schema.table names");
4886
4887        assert_eq!(node.name, "a");
4888    }
4889
4890    #[test]
4891    fn test_lineage_with_schema_none_matches_lineage() {
4892        let expr = parse("SELECT a FROM t");
4893        let baseline = lineage("a", &expr, None, false).expect("lineage baseline");
4894        let with_none =
4895            lineage_with_schema("a", &expr, None, None, false).expect("lineage_with_schema");
4896
4897        assert_eq!(with_none.name, baseline.name);
4898        assert_eq!(with_none.downstream_names(), baseline.downstream_names());
4899    }
4900
4901    #[test]
4902    fn test_lineage_with_schema_bigquery_mixed_case_column_names_issue_60() {
4903        let dialect = Dialect::get(DialectType::BigQuery);
4904        let expr = dialect
4905            .parse("SELECT Name AS name FROM teams")
4906            .unwrap()
4907            .into_iter()
4908            .next()
4909            .expect("expected one expression");
4910
4911        let mut schema = MappingSchema::with_dialect(DialectType::BigQuery);
4912        schema
4913            .add_table(
4914                "teams",
4915                &[("Name".into(), DataType::String { length: None })],
4916                None,
4917            )
4918            .expect("schema setup");
4919
4920        let node = lineage_with_schema(
4921            "name",
4922            &expr,
4923            Some(&schema),
4924            Some(DialectType::BigQuery),
4925            false,
4926        )
4927        .expect("lineage_with_schema should resolve mixed-case BigQuery columns");
4928
4929        let names = node.downstream_names();
4930        assert!(
4931            names.iter().any(|n| n == "teams.Name"),
4932            "Expected teams.Name in downstream, got: {:?}",
4933            names
4934        );
4935    }
4936
4937    #[test]
4938    fn test_lineage_bigquery_mixed_case_alias_lookup() {
4939        let dialect = Dialect::get(DialectType::BigQuery);
4940        let expr = dialect
4941            .parse("SELECT Name AS Name FROM teams")
4942            .unwrap()
4943            .into_iter()
4944            .next()
4945            .expect("expected one expression");
4946
4947        let node = lineage("name", &expr, Some(DialectType::BigQuery), false)
4948            .expect("lineage should resolve mixed-case aliases in BigQuery");
4949
4950        assert_eq!(node.name, "name");
4951    }
4952
4953    #[test]
4954    fn test_lineage_bigquery_unnest_alias_source_issue_209() {
4955        let expr = parse_one(
4956            r#"
4957SELECT date_val AS week_start
4958FROM UNNEST(GENERATE_DATE_ARRAY('2024-01-01', '2024-12-31', INTERVAL 1 WEEK)) AS date_val
4959"#,
4960            DialectType::BigQuery,
4961        )
4962        .expect("parse");
4963
4964        let node = lineage("week_start", &expr, Some(DialectType::BigQuery), false)
4965            .expect("lineage should resolve UNNEST alias as a source");
4966        let child = node
4967            .downstream
4968            .first()
4969            .expect("week_start should have downstream lineage");
4970
4971        assert_eq!(child.name, "_0.date_val");
4972        assert_eq!(child.source_name, "_0");
4973        assert_eq!(child.source_kind, SourceKind::Virtual);
4974        assert_eq!(child.source_alias.as_deref(), Some("date_val"));
4975
4976        let Expression::Column(column) = &child.expression else {
4977            panic!(
4978                "expected downstream column expression, got {:?}",
4979                child.expression
4980            );
4981        };
4982        assert_eq!(column.name.name, "date_val");
4983        assert_eq!(
4984            column.table.as_ref().map(|table| table.name.as_str()),
4985            Some("_0")
4986        );
4987        assert!(
4988            matches!(&child.source, Expression::Alias(alias) if matches!(&alias.this, Expression::Unnest(_)) && alias.alias.name == "date_val"),
4989            "expected UNNEST source expression, got {:?}",
4990            child.source
4991        );
4992    }
4993
4994    #[test]
4995    fn test_lineage_real_table_named_like_unnest_alias_is_not_virtual() {
4996        let expr =
4997            parse_one("SELECT date_val.id FROM date_val", DialectType::BigQuery).expect("parse");
4998
4999        let node = lineage("id", &expr, Some(DialectType::BigQuery), false).expect("lineage");
5000        let child = node.downstream.first().expect("id should have lineage");
5001
5002        assert_eq!(child.name, "date_val.id");
5003        assert_eq!(child.source_name, "date_val");
5004        assert_eq!(child.source_kind, SourceKind::Table);
5005        assert_eq!(child.source_alias, None);
5006    }
5007
5008    #[test]
5009    fn test_lineage_multiple_bigquery_unnest_sources_get_stable_virtual_names() {
5010        let expr = parse_one(
5011            r#"
5012SELECT a.a AS first_value, b.b AS second_value
5013FROM UNNEST(GENERATE_ARRAY(1, 2)) AS a
5014JOIN UNNEST(GENERATE_ARRAY(3, 4)) AS b ON TRUE
5015"#,
5016            DialectType::BigQuery,
5017        )
5018        .expect("parse");
5019
5020        let first =
5021            lineage("first_value", &expr, Some(DialectType::BigQuery), false).expect("lineage");
5022        let second =
5023            lineage("second_value", &expr, Some(DialectType::BigQuery), false).expect("lineage");
5024
5025        let first_child = first.downstream.first().expect("first source");
5026        let second_child = second.downstream.first().expect("second source");
5027
5028        assert_eq!(first_child.name, "_0.a");
5029        assert_eq!(first_child.source_name, "_0");
5030        assert_eq!(first_child.source_alias.as_deref(), Some("a"));
5031        assert_eq!(first_child.source_kind, SourceKind::Virtual);
5032
5033        assert_eq!(second_child.name, "_1.b");
5034        assert_eq!(second_child.source_name, "_1");
5035        assert_eq!(second_child.source_alias.as_deref(), Some("b"));
5036        assert_eq!(second_child.source_kind, SourceKind::Virtual);
5037    }
5038
5039    #[test]
5040    fn test_lineage_table_backed_unnest_points_to_real_source_column() {
5041        let expr = parse_one(
5042            r#"
5043SELECT item.item AS item
5044FROM t JOIN UNNEST(t.items) AS item ON TRUE
5045"#,
5046            DialectType::BigQuery,
5047        )
5048        .expect("parse");
5049
5050        let node = lineage("item", &expr, Some(DialectType::BigQuery), false).expect("lineage");
5051        let virtual_child = node.downstream.first().expect("virtual item source");
5052        assert_eq!(virtual_child.name, "_0.item");
5053        assert_eq!(virtual_child.source_kind, SourceKind::Virtual);
5054
5055        let real_child = virtual_child
5056            .downstream
5057            .first()
5058            .expect("UNNEST(t.items) should depend on t.items");
5059        assert_eq!(real_child.name, "t.items");
5060        assert_eq!(real_child.source_name, "t");
5061        assert_eq!(real_child.source_kind, SourceKind::Table);
5062    }
5063
5064    #[test]
5065    fn test_lineage_table_backed_unnest_unqualified_column_resolves_to_virtual_source() {
5066        let expr = parse_one(
5067            r#"
5068SELECT item AS item
5069FROM t JOIN UNNEST(t.items) AS item ON TRUE
5070"#,
5071            DialectType::BigQuery,
5072        )
5073        .expect("parse");
5074
5075        let node = lineage("item", &expr, Some(DialectType::BigQuery), false).expect("lineage");
5076        let virtual_child = node.downstream.first().expect("virtual item source");
5077        assert_eq!(virtual_child.name, "_0.item");
5078        assert_eq!(virtual_child.source_name, "_0");
5079        assert_eq!(virtual_child.source_kind, SourceKind::Virtual);
5080        assert_eq!(virtual_child.source_alias.as_deref(), Some("item"));
5081
5082        let real_child = virtual_child
5083            .downstream
5084            .first()
5085            .expect("UNNEST(t.items) should depend on t.items");
5086        assert_eq!(real_child.name, "t.items");
5087        assert_eq!(real_child.source_name, "t");
5088        assert_eq!(real_child.source_kind, SourceKind::Table);
5089    }
5090
5091    #[test]
5092    fn test_lineage_unnest_alias_columns_resolve_to_virtual_sources_across_dialects() {
5093        let cases = [
5094            (
5095                DialectType::PostgreSQL,
5096                "SELECT x AS out FROM t CROSS JOIN LATERAL UNNEST(items) AS u(x)",
5097            ),
5098            (
5099                DialectType::Presto,
5100                "SELECT x AS out FROM t CROSS JOIN UNNEST(items) AS u(x)",
5101            ),
5102            (
5103                DialectType::Trino,
5104                "SELECT x AS out FROM t CROSS JOIN UNNEST(items) AS u(x)",
5105            ),
5106        ];
5107
5108        for (dialect, sql) in cases {
5109            let expr = parse_one(sql, dialect).unwrap_or_else(|e| panic!("parse {dialect:?}: {e}"));
5110            let node = lineage("out", &expr, Some(dialect), false)
5111                .unwrap_or_else(|e| panic!("lineage {dialect:?}: {e}"));
5112            let virtual_child = node
5113                .downstream
5114                .first()
5115                .unwrap_or_else(|| panic!("expected virtual child for {dialect:?}"));
5116
5117            assert_eq!(
5118                virtual_child.name, "_0.x",
5119                "unexpected virtual child for {dialect:?}"
5120            );
5121            assert_eq!(virtual_child.source_name, "_0");
5122            assert_eq!(virtual_child.source_kind, SourceKind::Virtual);
5123            assert_eq!(virtual_child.source_alias.as_deref(), Some("u"));
5124
5125            let real_child = virtual_child
5126                .downstream
5127                .first()
5128                .unwrap_or_else(|| panic!("expected table dependency for {dialect:?}"));
5129            assert_eq!(real_child.name, "t.items");
5130            assert_eq!(real_child.source_kind, SourceKind::Table);
5131        }
5132    }
5133
5134    #[test]
5135    fn test_lineage_with_schema_propagates_unnest_element_type() {
5136        let expr = parse_dialect(
5137            "SELECT u.tag FROM events AS e, UNNEST(e.tags) AS u(tag)",
5138            DialectType::DuckDB,
5139        );
5140        let mut schema = MappingSchema::with_dialect(DialectType::DuckDB);
5141        schema
5142            .add_table(
5143                "events",
5144                &[(
5145                    "tags".into(),
5146                    DataType::Array {
5147                        element_type: Box::new(DataType::VarChar {
5148                            length: None,
5149                            parenthesized_length: false,
5150                        }),
5151                        dimension: None,
5152                    },
5153                )],
5154                None,
5155            )
5156            .expect("schema setup");
5157
5158        let node = lineage_with_schema(
5159            "tag",
5160            &expr,
5161            Some(&schema),
5162            Some(DialectType::DuckDB),
5163            false,
5164        )
5165        .expect("lineage_with_schema");
5166        let expected = DataType::VarChar {
5167            length: None,
5168            parenthesized_length: false,
5169        };
5170
5171        assert_eq!(node.expression.inferred_type(), Some(&expected));
5172        let virtual_child = node
5173            .downstream
5174            .iter()
5175            .find(|child| child.source_kind == SourceKind::Virtual)
5176            .expect("virtual UNNEST output");
5177        assert_eq!(virtual_child.expression.inferred_type(), Some(&expected));
5178    }
5179
5180    #[test]
5181    fn test_lineage_with_schema_resolves_struct_fields_issue_408() {
5182        let struct_type = DataType::Struct {
5183            fields: vec![crate::expressions::StructField::new(
5184                "field_value".into(),
5185                DataType::Text,
5186            )],
5187            nested: true,
5188        };
5189        let mut schema = MappingSchema::with_dialect(DialectType::DuckDB);
5190        schema
5191            .add_table(
5192                "source_table",
5193                &[
5194                    ("composite_value".into(), struct_type.clone()),
5195                    (
5196                        "nested_items".into(),
5197                        DataType::Array {
5198                            element_type: Box::new(struct_type),
5199                            dimension: None,
5200                        },
5201                    ),
5202                ],
5203                None,
5204            )
5205            .expect("schema setup");
5206
5207        let direct = parse_dialect(
5208            "SELECT composite_value.field_value AS output_value FROM source_table",
5209            DialectType::DuckDB,
5210        );
5211        let direct_node = lineage_with_schema(
5212            "output_value",
5213            &direct,
5214            Some(&schema),
5215            Some(DialectType::DuckDB),
5216            false,
5217        )
5218        .expect("direct struct lineage");
5219        assert_eq!(
5220            direct_node.expression.inferred_type(),
5221            Some(&DataType::Text)
5222        );
5223        assert_lineage_contains(&direct_node, "source_table.composite_value");
5224
5225        let unnested = parse_dialect(
5226            "SELECT item.field_value AS output_value FROM source_table s \
5227             CROSS JOIN UNNEST(s.nested_items) AS expanded(item)",
5228            DialectType::DuckDB,
5229        );
5230        let unnest_node = lineage_with_schema(
5231            "output_value",
5232            &unnested,
5233            Some(&schema),
5234            Some(DialectType::DuckDB),
5235            false,
5236        )
5237        .expect("UNNEST struct lineage");
5238        assert_eq!(
5239            unnest_node.expression.inferred_type(),
5240            Some(&DataType::Text)
5241        );
5242        assert!(
5243            unnest_node.walk().any(|node| {
5244                node.source_kind == SourceKind::Table
5245                    && node.source_name == "source_table"
5246                    && node.name.ends_with(".nested_items")
5247            }),
5248            "expected physical nested_items lineage, got {:?}",
5249            lineage_names(&unnest_node)
5250        );
5251    }
5252
5253    #[test]
5254    fn test_lineage_lateral_view_columns_resolve_to_virtual_sources() {
5255        let cases = [
5256            (
5257                DialectType::Spark,
5258                "SELECT x AS out FROM t LATERAL VIEW EXPLODE(items) u AS x",
5259            ),
5260            (
5261                DialectType::Hive,
5262                "SELECT x AS out FROM t LATERAL VIEW EXPLODE(items) u AS x",
5263            ),
5264        ];
5265
5266        for (dialect, sql) in cases {
5267            let expr = parse_one(sql, dialect).unwrap_or_else(|e| panic!("parse {dialect:?}: {e}"));
5268            let node = lineage("out", &expr, Some(dialect), false)
5269                .unwrap_or_else(|e| panic!("lineage {dialect:?}: {e}"));
5270            let virtual_child = node
5271                .downstream
5272                .first()
5273                .unwrap_or_else(|| panic!("expected virtual child for {dialect:?}"));
5274
5275            assert_eq!(virtual_child.name, "_0.x");
5276            assert_eq!(virtual_child.source_name, "_0");
5277            assert_eq!(virtual_child.source_kind, SourceKind::Virtual);
5278            assert_eq!(virtual_child.source_alias.as_deref(), Some("u"));
5279
5280            let real_child = virtual_child
5281                .downstream
5282                .first()
5283                .unwrap_or_else(|| panic!("expected table dependency for {dialect:?}"));
5284            assert_eq!(real_child.name, "t.items");
5285            assert_eq!(real_child.source_kind, SourceKind::Table);
5286        }
5287    }
5288
5289    #[test]
5290    fn test_lineage_snowflake_lateral_flatten_is_virtual_source() {
5291        let expr = parse_one(
5292            "SELECT f.value AS value FROM raw_events, LATERAL FLATTEN(INPUT => payload:items) AS f",
5293            DialectType::Snowflake,
5294        )
5295        .expect("parse");
5296
5297        let node = lineage("value", &expr, Some(DialectType::Snowflake), false).expect("lineage");
5298        let virtual_child = node.downstream.first().expect("virtual flatten source");
5299        assert_eq!(virtual_child.name, "_0.value");
5300        assert_eq!(virtual_child.source_name, "_0");
5301        assert_eq!(virtual_child.source_kind, SourceKind::Virtual);
5302        assert_eq!(virtual_child.source_alias.as_deref(), Some("f"));
5303
5304        let real_child = virtual_child
5305            .downstream
5306            .first()
5307            .expect("FLATTEN input should depend on raw_events.payload");
5308        assert_eq!(real_child.name, "raw_events.payload");
5309        assert_eq!(real_child.source_kind, SourceKind::Table);
5310    }
5311
5312    #[test]
5313    fn test_lineage_with_schema_snowflake_datediff_date_part_issue_61() {
5314        let expr = parse_one(
5315            "SELECT DATEDIFF(day, date_utc, CURRENT_DATE()) AS recency FROM fact.some_daily_metrics",
5316            DialectType::Snowflake,
5317        )
5318        .expect("parse");
5319
5320        let mut schema = MappingSchema::with_dialect(DialectType::Snowflake);
5321        schema
5322            .add_table(
5323                "fact.some_daily_metrics",
5324                &[("date_utc".to_string(), DataType::Date)],
5325                None,
5326            )
5327            .expect("schema setup");
5328
5329        let node = lineage_with_schema(
5330            "recency",
5331            &expr,
5332            Some(&schema),
5333            Some(DialectType::Snowflake),
5334            false,
5335        )
5336        .expect("lineage_with_schema should not treat date part as a column");
5337
5338        let names = node.downstream_names();
5339        assert!(
5340            names.iter().any(|n| n == "some_daily_metrics.date_utc"),
5341            "Expected some_daily_metrics.date_utc in downstream, got: {:?}",
5342            names
5343        );
5344        assert!(
5345            !names.iter().any(|n| n.ends_with(".day") || n == "day"),
5346            "Did not expect date part to appear as lineage column, got: {:?}",
5347            names
5348        );
5349    }
5350
5351    #[test]
5352    fn test_snowflake_datediff_parses_to_typed_ast() {
5353        let expr = parse_one(
5354            "SELECT DATEDIFF(day, date_utc, CURRENT_DATE()) AS recency FROM fact.some_daily_metrics",
5355            DialectType::Snowflake,
5356        )
5357        .expect("parse");
5358
5359        match expr {
5360            Expression::Select(select) => match &select.expressions[0] {
5361                Expression::Alias(alias) => match &alias.this {
5362                    Expression::DateDiff(f) => {
5363                        assert_eq!(f.unit, Some(crate::expressions::IntervalUnit::Day));
5364                    }
5365                    other => panic!("expected DateDiff, got {other:?}"),
5366                },
5367                other => panic!("expected Alias, got {other:?}"),
5368            },
5369            other => panic!("expected Select, got {other:?}"),
5370        }
5371    }
5372
5373    #[test]
5374    fn test_lineage_with_schema_snowflake_dateadd_date_part_issue_followup() {
5375        let expr = parse_one(
5376            "SELECT DATEADD(day, 1, date_utc) AS next_day FROM fact.some_daily_metrics",
5377            DialectType::Snowflake,
5378        )
5379        .expect("parse");
5380
5381        let mut schema = MappingSchema::with_dialect(DialectType::Snowflake);
5382        schema
5383            .add_table(
5384                "fact.some_daily_metrics",
5385                &[("date_utc".to_string(), DataType::Date)],
5386                None,
5387            )
5388            .expect("schema setup");
5389
5390        let node = lineage_with_schema(
5391            "next_day",
5392            &expr,
5393            Some(&schema),
5394            Some(DialectType::Snowflake),
5395            false,
5396        )
5397        .expect("lineage_with_schema should not treat DATEADD date part as a column");
5398
5399        let names = node.downstream_names();
5400        assert!(
5401            names.iter().any(|n| n == "some_daily_metrics.date_utc"),
5402            "Expected some_daily_metrics.date_utc in downstream, got: {:?}",
5403            names
5404        );
5405        assert!(
5406            !names.iter().any(|n| n.ends_with(".day") || n == "day"),
5407            "Did not expect date part to appear as lineage column, got: {:?}",
5408            names
5409        );
5410    }
5411
5412    #[test]
5413    fn test_lineage_with_schema_snowflake_date_part_identifier_issue_followup() {
5414        let expr = parse_one(
5415            "SELECT DATE_PART(day, date_utc) AS day_part FROM fact.some_daily_metrics",
5416            DialectType::Snowflake,
5417        )
5418        .expect("parse");
5419
5420        let mut schema = MappingSchema::with_dialect(DialectType::Snowflake);
5421        schema
5422            .add_table(
5423                "fact.some_daily_metrics",
5424                &[("date_utc".to_string(), DataType::Date)],
5425                None,
5426            )
5427            .expect("schema setup");
5428
5429        let node = lineage_with_schema(
5430            "day_part",
5431            &expr,
5432            Some(&schema),
5433            Some(DialectType::Snowflake),
5434            false,
5435        )
5436        .expect("lineage_with_schema should not treat DATE_PART identifier as a column");
5437
5438        let names = node.downstream_names();
5439        assert!(
5440            names.iter().any(|n| n == "some_daily_metrics.date_utc"),
5441            "Expected some_daily_metrics.date_utc in downstream, got: {:?}",
5442            names
5443        );
5444        assert!(
5445            !names.iter().any(|n| n.ends_with(".day") || n == "day"),
5446            "Did not expect date part to appear as lineage column, got: {:?}",
5447            names
5448        );
5449    }
5450
5451    #[test]
5452    fn test_lineage_with_schema_snowflake_date_part_string_literal_control() {
5453        let expr = parse_one(
5454            "SELECT DATE_PART('day', date_utc) AS day_part FROM fact.some_daily_metrics",
5455            DialectType::Snowflake,
5456        )
5457        .expect("parse");
5458
5459        let mut schema = MappingSchema::with_dialect(DialectType::Snowflake);
5460        schema
5461            .add_table(
5462                "fact.some_daily_metrics",
5463                &[("date_utc".to_string(), DataType::Date)],
5464                None,
5465            )
5466            .expect("schema setup");
5467
5468        let node = lineage_with_schema(
5469            "day_part",
5470            &expr,
5471            Some(&schema),
5472            Some(DialectType::Snowflake),
5473            false,
5474        )
5475        .expect("quoted DATE_PART should continue to work");
5476
5477        let names = node.downstream_names();
5478        assert!(
5479            names.iter().any(|n| n == "some_daily_metrics.date_utc"),
5480            "Expected some_daily_metrics.date_utc in downstream, got: {:?}",
5481            names
5482        );
5483    }
5484
5485    #[test]
5486    fn test_snowflake_dateadd_date_part_identifier_stays_generic_function() {
5487        let expr = parse_one(
5488            "SELECT DATEADD(day, 1, date_utc) AS next_day FROM fact.some_daily_metrics",
5489            DialectType::Snowflake,
5490        )
5491        .expect("parse");
5492
5493        match expr {
5494            Expression::Select(select) => match &select.expressions[0] {
5495                Expression::Alias(alias) => match &alias.this {
5496                    Expression::Function(f) => {
5497                        assert_eq!(f.name.to_uppercase(), "DATEADD");
5498                        assert!(matches!(&f.args[0], Expression::Var(v) if v.this == "day"));
5499                    }
5500                    other => panic!("expected generic DATEADD function, got {other:?}"),
5501                },
5502                other => panic!("expected Alias, got {other:?}"),
5503            },
5504            other => panic!("expected Select, got {other:?}"),
5505        }
5506    }
5507
5508    #[test]
5509    fn test_snowflake_date_part_identifier_stays_generic_function_with_var_arg() {
5510        let expr = parse_one(
5511            "SELECT DATE_PART(day, date_utc) AS day_part FROM fact.some_daily_metrics",
5512            DialectType::Snowflake,
5513        )
5514        .expect("parse");
5515
5516        match expr {
5517            Expression::Select(select) => match &select.expressions[0] {
5518                Expression::Alias(alias) => match &alias.this {
5519                    Expression::Function(f) => {
5520                        assert_eq!(f.name.to_uppercase(), "DATE_PART");
5521                        assert!(matches!(&f.args[0], Expression::Var(v) if v.this == "day"));
5522                    }
5523                    other => panic!("expected generic DATE_PART function, got {other:?}"),
5524                },
5525                other => panic!("expected Alias, got {other:?}"),
5526            },
5527            other => panic!("expected Select, got {other:?}"),
5528        }
5529    }
5530
5531    #[test]
5532    fn test_snowflake_date_part_string_literal_stays_generic_function() {
5533        let expr = parse_one(
5534            "SELECT DATE_PART('day', date_utc) AS day_part FROM fact.some_daily_metrics",
5535            DialectType::Snowflake,
5536        )
5537        .expect("parse");
5538
5539        match expr {
5540            Expression::Select(select) => match &select.expressions[0] {
5541                Expression::Alias(alias) => match &alias.this {
5542                    Expression::Function(f) => {
5543                        assert_eq!(f.name.to_uppercase(), "DATE_PART");
5544                    }
5545                    other => panic!("expected generic DATE_PART function, got {other:?}"),
5546                },
5547                other => panic!("expected Alias, got {other:?}"),
5548            },
5549            other => panic!("expected Select, got {other:?}"),
5550        }
5551    }
5552
5553    #[test]
5554    fn test_lineage_join() {
5555        let expr = parse("SELECT t.a, s.b FROM t JOIN s ON t.id = s.id");
5556
5557        let node_a = lineage("a", &expr, None, false).unwrap();
5558        let names_a = node_a.downstream_names();
5559        assert!(
5560            names_a.iter().any(|n| n == "t.a"),
5561            "Expected t.a, got: {:?}",
5562            names_a
5563        );
5564
5565        let node_b = lineage("b", &expr, None, false).unwrap();
5566        let names_b = node_b.downstream_names();
5567        assert!(
5568            names_b.iter().any(|n| n == "s.b"),
5569            "Expected s.b, got: {:?}",
5570            names_b
5571        );
5572    }
5573
5574    #[test]
5575    fn test_lineage_alias_leaf_has_resolved_source_name() {
5576        let expr = parse("SELECT t1.col1 FROM table1 t1 JOIN table2 t2 ON t1.id = t2.id");
5577        let node = lineage("col1", &expr, None, false).unwrap();
5578
5579        // Keep alias in the display lineage edge.
5580        let names = node.downstream_names();
5581        assert!(
5582            names.iter().any(|n| n == "t1.col1"),
5583            "Expected aliased column edge t1.col1, got: {:?}",
5584            names
5585        );
5586
5587        // Leaf should expose the resolved base table for consumers.
5588        let leaf = node
5589            .downstream
5590            .iter()
5591            .find(|n| n.name == "t1.col1")
5592            .expect("Expected t1.col1 leaf");
5593        assert_eq!(leaf.source_name, "table1");
5594        match &leaf.source {
5595            Expression::Table(table) => assert_eq!(table.name.name, "table1"),
5596            _ => panic!("Expected leaf source to be a table expression"),
5597        }
5598    }
5599
5600    #[test]
5601    fn test_lineage_derived_table() {
5602        let expr = parse("SELECT x.a FROM (SELECT a FROM t) AS x");
5603        let node = lineage("a", &expr, None, false).unwrap();
5604
5605        assert_eq!(node.name, "a");
5606        // Should trace through the derived table to t.a
5607        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
5608        assert!(
5609            all_names.iter().any(|n| n == "t.a"),
5610            "Expected to trace through derived table to t.a, got: {:?}",
5611            all_names
5612        );
5613    }
5614
5615    #[test]
5616    fn test_lineage_cte() {
5617        let expr = parse("WITH cte AS (SELECT a FROM t) SELECT a FROM cte");
5618        let node = lineage("a", &expr, None, false).unwrap();
5619
5620        assert_eq!(node.name, "a");
5621        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
5622        assert!(
5623            all_names.iter().any(|n| n == "t.a"),
5624            "Expected to trace through CTE to t.a, got: {:?}",
5625            all_names
5626        );
5627    }
5628
5629    #[test]
5630    fn test_lineage_union() {
5631        let expr = parse("SELECT a FROM t1 UNION SELECT a FROM t2");
5632        let node = lineage("a", &expr, None, false).unwrap();
5633
5634        assert_eq!(node.name, "a");
5635        // Should have 2 downstream branches
5636        assert_eq!(
5637            node.downstream.len(),
5638            2,
5639            "Expected 2 branches for UNION, got {}",
5640            node.downstream.len()
5641        );
5642    }
5643
5644    #[test]
5645    fn test_lineage_cte_union() {
5646        let expr = parse("WITH cte AS (SELECT a FROM t1 UNION SELECT a FROM t2) SELECT a FROM cte");
5647        let node = lineage("a", &expr, None, false).unwrap();
5648
5649        // Should trace through CTE into both UNION branches
5650        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
5651        assert!(
5652            all_names.len() >= 3,
5653            "Expected at least 3 nodes for CTE with UNION, got: {:?}",
5654            all_names
5655        );
5656    }
5657
5658    #[test]
5659    fn test_lineage_star() {
5660        let expr = parse("SELECT * FROM t");
5661        let node = lineage("*", &expr, None, false).unwrap();
5662
5663        assert_eq!(node.name, "*");
5664        // Should have downstream for table t
5665        assert!(
5666            !node.downstream.is_empty(),
5667            "Star should produce downstream nodes"
5668        );
5669    }
5670
5671    #[test]
5672    fn test_lineage_subquery_in_select() {
5673        let expr = parse("SELECT (SELECT MAX(b) FROM s) AS x FROM t");
5674        let node = lineage("x", &expr, None, false).unwrap();
5675
5676        assert_eq!(node.name, "x");
5677        // Should have traced into the scalar subquery
5678        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
5679        assert!(
5680            all_names.len() >= 2,
5681            "Expected tracing into scalar subquery, got: {:?}",
5682            all_names
5683        );
5684    }
5685
5686    #[test]
5687    fn test_lineage_multiple_columns() {
5688        let expr = parse("SELECT a, b FROM t");
5689
5690        let node_a = lineage("a", &expr, None, false).unwrap();
5691        let node_b = lineage("b", &expr, None, false).unwrap();
5692
5693        assert_eq!(node_a.name, "a");
5694        assert_eq!(node_b.name, "b");
5695
5696        // Each should trace independently
5697        let names_a = node_a.downstream_names();
5698        let names_b = node_b.downstream_names();
5699        assert!(names_a.iter().any(|n| n == "t.a"));
5700        assert!(names_b.iter().any(|n| n == "t.b"));
5701    }
5702
5703    #[test]
5704    fn test_get_source_tables() {
5705        let expr = parse("SELECT t.a, s.b FROM t JOIN s ON t.id = s.id");
5706        let node = lineage("a", &expr, None, false).unwrap();
5707
5708        let tables = get_source_tables(&node);
5709        assert!(
5710            tables.contains("t"),
5711            "Expected source table 't', got: {:?}",
5712            tables
5713        );
5714    }
5715
5716    #[test]
5717    fn test_lineage_column_not_found() {
5718        let expr = parse("SELECT a FROM t");
5719        let result = lineage("nonexistent", &expr, None, false);
5720        assert!(result.is_err());
5721    }
5722
5723    #[test]
5724    fn test_lineage_nested_cte() {
5725        let expr = parse(
5726            "WITH cte1 AS (SELECT a FROM t), \
5727             cte2 AS (SELECT a FROM cte1) \
5728             SELECT a FROM cte2",
5729        );
5730        let node = lineage("a", &expr, None, false).unwrap();
5731
5732        // Should trace through cte2 → cte1 → t
5733        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
5734        assert!(
5735            all_names.len() >= 3,
5736            "Expected to trace through nested CTEs, got: {:?}",
5737            all_names
5738        );
5739    }
5740
5741    #[test]
5742    fn test_lineage_deeply_nested_cte_reaches_base_table() {
5743        let expr = parse(
5744            "WITH outer_cte AS (\
5745             WITH middle_cte AS (\
5746             WITH inner_cte AS (SELECT x AS col FROM base_table) \
5747             SELECT col FROM inner_cte\
5748             ) SELECT col FROM middle_cte\
5749             ) SELECT col FROM outer_cte",
5750        );
5751        let node = lineage("col", &expr, None, false).unwrap();
5752
5753        assert_lineage_contains(&node, "base_table.x");
5754        for cte_name in ["outer_cte", "middle_cte", "inner_cte"] {
5755            assert!(
5756                node.walk().any(|child| child.source_name == cte_name),
5757                "expected lineage to include CTE {cte_name}, got {:?}",
5758                lineage_names(&node)
5759            );
5760        }
5761    }
5762
5763    #[test]
5764    fn test_lineage_reused_nested_cte_traces_each_reference() {
5765        let expr = parse(
5766            "WITH shared AS (\
5767             WITH nested AS (SELECT x AS col FROM base_table) \
5768             SELECT col FROM nested\
5769             ) \
5770             SELECT s0.col + s1.col + s2.col AS total \
5771             FROM shared AS s0 \
5772             CROSS JOIN shared AS s1 \
5773             CROSS JOIN shared AS s2",
5774        );
5775        let node = lineage("total", &expr, None, false).unwrap();
5776
5777        let base_references = node
5778            .walk()
5779            .filter(|child| child.name == "base_table.x")
5780            .count();
5781        assert_eq!(
5782            base_references,
5783            3,
5784            "each shared CTE reference should reach base_table.x: {:?}",
5785            lineage_names(&node)
5786        );
5787    }
5788
5789    #[test]
5790    fn test_trim_selects_true() {
5791        let expr = parse("SELECT a, b, c FROM t");
5792        let node = lineage("a", &expr, None, true).unwrap();
5793
5794        // The source should be trimmed to only include 'a'
5795        if let Expression::Select(select) = &node.source {
5796            assert_eq!(
5797                select.expressions.len(),
5798                1,
5799                "Trimmed source should have 1 expression, got {}",
5800                select.expressions.len()
5801            );
5802        } else {
5803            panic!("Expected Select source");
5804        }
5805    }
5806
5807    #[test]
5808    fn test_trim_selects_false() {
5809        let expr = parse("SELECT a, b, c FROM t");
5810        let node = lineage("a", &expr, None, false).unwrap();
5811
5812        // The source should keep all columns
5813        if let Expression::Select(select) = &node.source {
5814            assert_eq!(
5815                select.expressions.len(),
5816                3,
5817                "Untrimmed source should have 3 expressions"
5818            );
5819        } else {
5820            panic!("Expected Select source");
5821        }
5822    }
5823
5824    #[test]
5825    fn test_lineage_expression_in_select() {
5826        let expr = parse("SELECT a + b AS c FROM t");
5827        let node = lineage("c", &expr, None, false).unwrap();
5828
5829        // Should trace to both a and b from t
5830        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
5831        assert!(
5832            all_names.len() >= 3,
5833            "Expected to trace a + b to both columns, got: {:?}",
5834            all_names
5835        );
5836    }
5837
5838    #[test]
5839    fn test_set_operation_by_index() {
5840        let expr = parse("SELECT a FROM t1 UNION SELECT b FROM t2");
5841
5842        // Trace column "a" which is at index 0
5843        let node = lineage("a", &expr, None, false).unwrap();
5844
5845        // UNION branches should be traced by index
5846        assert_eq!(node.downstream.len(), 2);
5847    }
5848
5849    #[test]
5850    fn test_issue_384_output_columns_preserve_unknown_positions() {
5851        let expr = parse("SELECT a, 1, *, tail FROM unknown_source");
5852        let output = output_columns(&expr, None).expect("output columns");
5853
5854        assert!(!output.ordinal_complete);
5855        assert_eq!(
5856            output.columns,
5857            vec![
5858                OutputColumn::Named {
5859                    name: "a".to_string(),
5860                    ordinal: Some(0),
5861                },
5862                OutputColumn::Unnamed { ordinal: Some(1) },
5863                OutputColumn::Wildcard {
5864                    qualifier: None,
5865                    start_ordinal: Some(2),
5866                },
5867                OutputColumn::Named {
5868                    name: "tail".to_string(),
5869                    ordinal: None,
5870                },
5871            ]
5872        );
5873    }
5874
5875    #[test]
5876    fn test_issue_384_output_columns_use_leftmost_set_operation_branch() {
5877        let expr = parse(
5878            "SELECT * FROM unknown_source UNION ALL \
5879             SELECT known_first AS x, known_second AS y FROM known_source",
5880        );
5881        let output = output_columns(&expr, None).expect("output columns");
5882
5883        assert!(!output.ordinal_complete);
5884        assert_eq!(
5885            output.columns,
5886            vec![OutputColumn::Wildcard {
5887                qualifier: None,
5888                start_ordinal: Some(0),
5889            }]
5890        );
5891    }
5892
5893    #[test]
5894    fn test_issue_384_schema_expands_output_wildcard() {
5895        let expr = parse("SELECT * FROM unknown_source");
5896        let mut schema = MappingSchema::new();
5897        schema
5898            .add_table(
5899                "unknown_source",
5900                &[
5901                    ("first_col".to_string(), DataType::Text),
5902                    ("second_col".to_string(), DataType::Text),
5903                ],
5904                None,
5905            )
5906            .expect("schema setup");
5907
5908        let output = output_columns_with_schema(&expr, Some(&schema), None)
5909            .expect("schema-aware output columns");
5910        assert!(output.ordinal_complete);
5911        assert_eq!(
5912            output.columns,
5913            vec![
5914                OutputColumn::Named {
5915                    name: "first_col".to_string(),
5916                    ordinal: Some(0),
5917                },
5918                OutputColumn::Named {
5919                    name: "second_col".to_string(),
5920                    ordinal: Some(1),
5921                },
5922            ]
5923        );
5924    }
5925
5926    #[test]
5927    fn test_issue_411_duckdb_and_snowflake_union_by_name_layout_and_lineage() {
5928        let sql = "SELECT 1 AS left_value UNION ALL BY NAME SELECT 2 AS right_value";
5929
5930        for dialect in [DialectType::DuckDB, DialectType::Snowflake] {
5931            let expr = parse_dialect(sql, dialect);
5932            let output = output_columns(&expr, Some(dialect)).expect("output columns");
5933            assert_eq!(
5934                output.columns,
5935                vec![
5936                    OutputColumn::Named {
5937                        name: "left_value".to_string(),
5938                        ordinal: Some(0),
5939                    },
5940                    OutputColumn::Named {
5941                        name: "right_value".to_string(),
5942                        ordinal: Some(1),
5943                    },
5944                ],
5945                "unexpected output layout for {dialect:?}"
5946            );
5947
5948            let left = lineage_at(0, &expr, Some(dialect), false).expect("left lineage");
5949            assert_eq!(
5950                left.downstream
5951                    .iter()
5952                    .map(|node| node.name.as_str())
5953                    .collect::<Vec<_>>(),
5954                vec!["left_value"]
5955            );
5956            assert_eq!(
5957                left.downstream[0].set_branch,
5958                Some(SetBranch {
5959                    operator: SetOperator::Union,
5960                    ordinal: 0,
5961                    all: true,
5962                })
5963            );
5964
5965            let right = lineage_at(1, &expr, Some(dialect), false).expect("right lineage");
5966            assert_eq!(
5967                right
5968                    .downstream
5969                    .iter()
5970                    .map(|node| node.name.as_str())
5971                    .collect::<Vec<_>>(),
5972                vec!["right_value"]
5973            );
5974            assert_eq!(
5975                right.downstream[0].set_branch,
5976                Some(SetBranch {
5977                    operator: SetOperator::Union,
5978                    ordinal: 1,
5979                    all: true,
5980                })
5981            );
5982        }
5983    }
5984
5985    #[test]
5986    fn test_issue_411_bigquery_strict_by_name_reorders_lineage() {
5987        let dialect = DialectType::BigQuery;
5988        let expr = parse_dialect(
5989            "SELECT 1 AS a, 2 AS b UNION ALL BY NAME SELECT 3 AS b, 4 AS a",
5990            dialect,
5991        );
5992        let output = output_columns(&expr, Some(dialect)).expect("output columns");
5993        assert_eq!(
5994            output.columns,
5995            vec![
5996                OutputColumn::Named {
5997                    name: "a".to_string(),
5998                    ordinal: Some(0),
5999                },
6000                OutputColumn::Named {
6001                    name: "b".to_string(),
6002                    ordinal: Some(1),
6003                },
6004            ]
6005        );
6006
6007        for (ordinal, expected_name) in [(0, "a"), (1, "b")] {
6008            let node = lineage_at(ordinal, &expr, Some(dialect), false).expect("lineage");
6009            assert_eq!(
6010                node.downstream
6011                    .iter()
6012                    .map(|child| child.name.as_str())
6013                    .collect::<Vec<_>>(),
6014                vec![expected_name, expected_name]
6015            );
6016        }
6017    }
6018
6019    #[test]
6020    fn test_issue_411_bigquery_by_name_modes_and_explicit_order() {
6021        let dialect = DialectType::BigQuery;
6022        for (sql, expected) in [
6023            (
6024                "SELECT 1 AS a, 2 AS b INNER UNION ALL BY NAME SELECT 3 AS b, 4 AS c",
6025                vec!["b"],
6026            ),
6027            (
6028                "SELECT 1 AS a, 2 AS b LEFT OUTER UNION ALL BY NAME SELECT 3 AS b, 4 AS c",
6029                vec!["a", "b"],
6030            ),
6031            (
6032                "SELECT 1 AS a, 2 AS b FULL OUTER UNION ALL BY NAME SELECT 3 AS b, 4 AS c",
6033                vec!["a", "b", "c"],
6034            ),
6035            (
6036                "SELECT 1 AS a, 2 AS b FULL OUTER UNION ALL BY NAME ON (c, a) SELECT 3 AS b, 4 AS c",
6037                vec!["c", "a"],
6038            ),
6039            (
6040                "SELECT 1 AS a, 2 AS b INTERSECT DISTINCT BY NAME SELECT 3 AS b, 4 AS a",
6041                vec!["a", "b"],
6042            ),
6043            (
6044                "SELECT 1 AS a, 2 AS b EXCEPT DISTINCT BY NAME SELECT 3 AS b, 4 AS a",
6045                vec!["a", "b"],
6046            ),
6047        ] {
6048            let expr = parse_dialect(sql, dialect);
6049            let output = output_columns(&expr, Some(dialect)).expect("output columns");
6050            assert_eq!(
6051                output
6052                    .columns
6053                    .iter()
6054                    .filter_map(|column| match column {
6055                        OutputColumn::Named { name, .. } => Some(name.as_str()),
6056                        _ => None,
6057                    })
6058                    .collect::<Vec<_>>(),
6059                expected,
6060                "unexpected output layout for {sql}"
6061            );
6062        }
6063    }
6064
6065    #[test]
6066    fn test_issue_411_name_matching_uses_dialect_identifier_rules() {
6067        let sql = "SELECT 1 AS \"lower\" UNION ALL BY NAME SELECT 2 AS lower";
6068
6069        let duckdb = parse_dialect(sql, DialectType::DuckDB);
6070        assert_eq!(
6071            output_columns(&duckdb, Some(DialectType::DuckDB))
6072                .expect("DuckDB output")
6073                .columns
6074                .len(),
6075            1,
6076            "DuckDB quoted identifiers are case-insensitive"
6077        );
6078
6079        let snowflake = parse_dialect(sql, DialectType::Snowflake);
6080        assert_eq!(
6081            output_columns(&snowflake, Some(DialectType::Snowflake))
6082                .expect("Snowflake output")
6083                .columns
6084                .len(),
6085            2,
6086            "Snowflake quoted lowercase and unquoted uppercase identifiers differ"
6087        );
6088    }
6089
6090    #[test]
6091    fn test_issue_383_lineage_at_traces_resolvable_set_operation_branch() {
6092        let expr = parse(
6093            "SELECT * FROM unknown_source UNION ALL \
6094             SELECT known_first AS x, known_second AS y FROM known_source",
6095        );
6096        let node = lineage_at(1, &expr, None, false).expect("ordinal lineage");
6097
6098        assert_lineage_contains(&node, "known_source.known_second");
6099        assert_eq!(node.downstream.len(), 1);
6100        assert_eq!(
6101            node.downstream[0].set_branch,
6102            Some(SetBranch {
6103                operator: SetOperator::Union,
6104                ordinal: 1,
6105                all: true,
6106            })
6107        );
6108
6109        let expr = parse(
6110            "SELECT known_first, known_second FROM known_source EXCEPT \
6111             SELECT * FROM unknown_source",
6112        );
6113        let node = lineage_at(1, &expr, None, false).expect("left-only ordinal lineage");
6114
6115        assert_lineage_contains(&node, "known_source.known_second");
6116        assert_eq!(node.downstream.len(), 1);
6117        assert_eq!(
6118            node.downstream[0].set_branch,
6119            Some(SetBranch {
6120                operator: SetOperator::Except,
6121                ordinal: 0,
6122                all: false,
6123            })
6124        );
6125    }
6126
6127    #[test]
6128    fn test_issue_383_unresolved_wildcard_does_not_shift_ordinal() {
6129        let expr = parse(
6130            "SELECT *, tail FROM unknown_source UNION ALL \
6131             SELECT known_first, known_second FROM known_source",
6132        );
6133        let node = lineage_at(1, &expr, None, false).expect("partial ordinal lineage");
6134        let names = lineage_names(&node);
6135
6136        assert!(names.iter().any(|name| name == "known_source.known_second"));
6137        assert!(!names
6138            .iter()
6139            .any(|name| name.ends_with(".tail") || name == "tail"));
6140    }
6141
6142    #[test]
6143    fn test_issue_383_lineage_at_ignores_branch_output_names() {
6144        let expr = parse(
6145            "SELECT left_value AS left_name FROM left_source UNION ALL \
6146             SELECT right_value AS right_name FROM right_source",
6147        );
6148        let node = lineage_at(0, &expr, None, false).expect("ordinal lineage");
6149
6150        assert_lineage_contains(&node, "left_source.left_value");
6151        assert_lineage_contains(&node, "right_source.right_value");
6152    }
6153
6154    #[test]
6155    fn test_issue_383_lineage_at_supports_all_set_operations() {
6156        for (sql_operator, operator, all) in [
6157            ("UNION ALL", SetOperator::Union, true),
6158            ("INTERSECT", SetOperator::Intersect, false),
6159            ("EXCEPT", SetOperator::Except, false),
6160        ] {
6161            let expr = parse(&format!(
6162                "SELECT left_value AS left_name FROM left_source {sql_operator} \
6163                 SELECT right_value AS right_name FROM right_source"
6164            ));
6165            let node = lineage_at(0, &expr, None, false).expect("ordinal lineage");
6166            assert_eq!(
6167                node.downstream.len(),
6168                2,
6169                "expected both branches for {sql_operator}"
6170            );
6171            assert_eq!(
6172                node.downstream
6173                    .iter()
6174                    .map(|child| child.set_branch)
6175                    .collect::<Vec<_>>(),
6176                vec![
6177                    Some(SetBranch {
6178                        operator,
6179                        ordinal: 0,
6180                        all,
6181                    }),
6182                    Some(SetBranch {
6183                        operator,
6184                        ordinal: 1,
6185                        all,
6186                    }),
6187                ]
6188            );
6189        }
6190    }
6191
6192    #[test]
6193    fn test_issue_383_lineage_at_with_schema_expands_wildcard() {
6194        let expr = parse(
6195            "SELECT * FROM unknown_source UNION ALL \
6196             SELECT known_first, known_second FROM known_source",
6197        );
6198        let mut schema = MappingSchema::new();
6199        schema
6200            .add_table(
6201                "unknown_source",
6202                &[
6203                    ("first_col".to_string(), DataType::Text),
6204                    ("second_col".to_string(), DataType::Text),
6205                ],
6206                None,
6207            )
6208            .expect("schema setup");
6209        schema
6210            .add_table(
6211                "known_source",
6212                &[
6213                    ("known_first".to_string(), DataType::Text),
6214                    ("known_second".to_string(), DataType::Text),
6215                ],
6216                None,
6217            )
6218            .expect("schema setup");
6219
6220        let node = lineage_at_with_schema(1, &expr, Some(&schema), None, false)
6221            .expect("schema-aware ordinal lineage");
6222        assert_lineage_contains(&node, "unknown_source.second_col");
6223        assert_lineage_contains(&node, "known_source.known_second");
6224    }
6225
6226    #[test]
6227    fn test_issue_385_structured_lineage_resolution_errors() {
6228        let out_of_range = lineage_at(1, &parse("SELECT a FROM t"), None, false)
6229            .expect_err("ordinal should be out of range");
6230        assert!(matches!(
6231            out_of_range,
6232            Error::ColumnResolution {
6233                target: ColumnResolutionTarget::Ordinal { ordinal: 1 },
6234                reason: ColumnResolutionReason::NotFound,
6235            }
6236        ));
6237
6238        let indeterminate = lineage(
6239            "tail",
6240            &parse(
6241                "SELECT *, tail FROM unknown_source UNION ALL \
6242                 SELECT known_first, known_second FROM known_source",
6243            ),
6244            None,
6245            false,
6246        )
6247        .expect_err("tail ordinal should be indeterminate");
6248        assert!(matches!(
6249            indeterminate,
6250            Error::ColumnResolution {
6251                target: ColumnResolutionTarget::Name { ref name },
6252                reason: ColumnResolutionReason::Indeterminate,
6253            } if name == "tail"
6254        ));
6255
6256        let ambiguous = lineage(
6257            "duplicate",
6258            &parse("SELECT a AS duplicate, b AS duplicate FROM t"),
6259            None,
6260            false,
6261        )
6262        .expect_err("duplicate output name should be ambiguous");
6263        assert!(matches!(
6264            ambiguous,
6265            Error::ColumnResolution {
6266                target: ColumnResolutionTarget::Name { ref name },
6267                reason: ColumnResolutionReason::Ambiguous,
6268            } if name == "duplicate"
6269        ));
6270    }
6271
6272    // --- Tests for column lineage inside function calls (issue #18) ---
6273
6274    fn print_node(node: &LineageNode, indent: usize) {
6275        let pad = "  ".repeat(indent);
6276        println!(
6277            "{pad}name={:?} source_name={:?}",
6278            node.name, node.source_name
6279        );
6280        for child in &node.downstream {
6281            print_node(child, indent + 1);
6282        }
6283    }
6284
6285    #[test]
6286    fn test_issue18_repro() {
6287        // Exact scenario from the issue
6288        let query = "SELECT UPPER(name) as upper_name FROM users";
6289        println!("Query: {query}\n");
6290
6291        let dialect = crate::dialects::Dialect::get(DialectType::BigQuery);
6292        let exprs = dialect.parse(query).unwrap();
6293        let expr = &exprs[0];
6294
6295        let node = lineage("upper_name", expr, Some(DialectType::BigQuery), false).unwrap();
6296        println!("lineage(\"upper_name\"):");
6297        print_node(&node, 1);
6298
6299        let names = node.downstream_names();
6300        assert!(
6301            names.iter().any(|n| n == "users.name"),
6302            "Expected users.name in downstream, got: {:?}",
6303            names
6304        );
6305    }
6306
6307    #[test]
6308    fn test_lineage_bigquery_safe_namespace_issue207() {
6309        let query = r#"
6310WITH import_cte AS (
6311  SELECT timestamp, data, operation
6312  FROM `project`.`dataset`.`source_table`
6313),
6314transform_cte AS (
6315  SELECT
6316    timestamp,
6317    SAFE.PARSE_JSON(data) AS json_data
6318  FROM import_cte
6319)
6320SELECT json_data FROM transform_cte
6321"#;
6322        let expr = parse_one(query, DialectType::BigQuery).expect("parse");
6323        let node = lineage("json_data", &expr, Some(DialectType::BigQuery), false)
6324            .expect("lineage should resolve SAFE.PARSE_JSON arguments");
6325        let names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6326
6327        assert!(
6328            names.iter().any(|name| name == "source_table.data"),
6329            "expected source_table.data in lineage, got {names:?}"
6330        );
6331        assert!(
6332            !names
6333                .iter()
6334                .any(|name| name.eq_ignore_ascii_case("import_cte.safe")),
6335            "did not expect SAFE namespace receiver in lineage, got {names:?}"
6336        );
6337    }
6338
6339    #[test]
6340    fn test_lineage_bigquery_safe_namespace_method_call_guard() {
6341        let expr = parse("SELECT SAFE.PARSE_JSON(data) AS json_data FROM t");
6342        let node = lineage("json_data", &expr, Some(DialectType::BigQuery), false)
6343            .expect("lineage should resolve SAFE.PARSE_JSON arguments");
6344        let names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6345
6346        assert!(
6347            names.iter().any(|name| name == "t.data"),
6348            "expected t.data in lineage, got {names:?}"
6349        );
6350        assert!(
6351            !names.iter().any(|name| name.eq_ignore_ascii_case("t.safe")),
6352            "did not expect SAFE namespace receiver in lineage, got {names:?}"
6353        );
6354    }
6355
6356    #[test]
6357    fn test_lineage_method_call_receiver_control() {
6358        let expr = parse("SELECT obj.METHOD(arg) AS out FROM t");
6359        let node = lineage("out", &expr, None, false).expect("lineage");
6360        let names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6361
6362        assert!(
6363            names.iter().any(|name| name == "t.obj"),
6364            "expected ordinary method receiver to remain in lineage, got {names:?}"
6365        );
6366        assert!(
6367            names.iter().any(|name| name == "t.arg"),
6368            "expected method argument in lineage, got {names:?}"
6369        );
6370    }
6371
6372    #[test]
6373    fn test_lineage_upper_function() {
6374        let expr = parse("SELECT UPPER(name) AS upper_name FROM users");
6375        let node = lineage("upper_name", &expr, None, false).unwrap();
6376
6377        let names = node.downstream_names();
6378        assert!(
6379            names.iter().any(|n| n == "users.name"),
6380            "Expected users.name in downstream, got: {:?}",
6381            names
6382        );
6383    }
6384
6385    #[test]
6386    fn test_lineage_round_function() {
6387        let expr = parse("SELECT ROUND(price, 2) AS rounded FROM products");
6388        let node = lineage("rounded", &expr, None, false).unwrap();
6389
6390        let names = node.downstream_names();
6391        assert!(
6392            names.iter().any(|n| n == "products.price"),
6393            "Expected products.price in downstream, got: {:?}",
6394            names
6395        );
6396    }
6397
6398    #[test]
6399    fn test_lineage_coalesce_function() {
6400        let expr = parse("SELECT COALESCE(a, b) AS val FROM t");
6401        let node = lineage("val", &expr, None, false).unwrap();
6402
6403        let names = node.downstream_names();
6404        assert!(
6405            names.iter().any(|n| n == "t.a"),
6406            "Expected t.a in downstream, got: {:?}",
6407            names
6408        );
6409        assert!(
6410            names.iter().any(|n| n == "t.b"),
6411            "Expected t.b in downstream, got: {:?}",
6412            names
6413        );
6414    }
6415
6416    #[test]
6417    fn test_lineage_count_function() {
6418        let expr = parse("SELECT COUNT(id) AS cnt FROM t");
6419        let node = lineage("cnt", &expr, None, false).unwrap();
6420
6421        let names = node.downstream_names();
6422        assert!(
6423            names.iter().any(|n| n == "t.id"),
6424            "Expected t.id in downstream, got: {:?}",
6425            names
6426        );
6427    }
6428
6429    #[test]
6430    fn test_lineage_sum_function() {
6431        let expr = parse("SELECT SUM(amount) AS total FROM t");
6432        let node = lineage("total", &expr, None, false).unwrap();
6433
6434        let names = node.downstream_names();
6435        assert!(
6436            names.iter().any(|n| n == "t.amount"),
6437            "Expected t.amount in downstream, got: {:?}",
6438            names
6439        );
6440    }
6441
6442    #[test]
6443    fn test_lineage_case_with_nested_functions() {
6444        let expr =
6445            parse("SELECT CASE WHEN x > 0 THEN UPPER(name) ELSE LOWER(name) END AS result FROM t");
6446        let node = lineage("result", &expr, None, false).unwrap();
6447
6448        let names = node.downstream_names();
6449        assert!(
6450            names.iter().any(|n| n == "t.x"),
6451            "Expected t.x in downstream, got: {:?}",
6452            names
6453        );
6454        assert!(
6455            names.iter().any(|n| n == "t.name"),
6456            "Expected t.name in downstream, got: {:?}",
6457            names
6458        );
6459    }
6460
6461    #[test]
6462    fn test_lineage_substring_function() {
6463        let expr = parse("SELECT SUBSTRING(name, 1, 3) AS short FROM t");
6464        let node = lineage("short", &expr, None, false).unwrap();
6465
6466        let names = node.downstream_names();
6467        assert!(
6468            names.iter().any(|n| n == "t.name"),
6469            "Expected t.name in downstream, got: {:?}",
6470            names
6471        );
6472    }
6473
6474    // --- CTE + SELECT * tests (ported from sqlglot test_lineage.py) ---
6475
6476    #[test]
6477    fn test_lineage_cte_select_star() {
6478        // Ported from sqlglot: test_lineage_source_with_star
6479        // WITH y AS (SELECT * FROM x) SELECT a FROM y
6480        // After star expansion: SELECT y.a AS a FROM y
6481        let expr = parse("WITH y AS (SELECT * FROM x) SELECT a FROM y");
6482        let node = lineage("a", &expr, None, false).unwrap();
6483
6484        assert_eq!(node.name, "a");
6485        // Should successfully resolve column 'a' through the CTE
6486        // (previously failed with "Cannot find column 'a' in query")
6487        assert!(
6488            !node.downstream.is_empty(),
6489            "Expected downstream nodes tracing through CTE, got none"
6490        );
6491    }
6492
6493    #[test]
6494    fn test_lineage_schema_less_cte_star_passthrough_resolves_base_column() {
6495        let expr = parse("WITH c AS (SELECT * FROM t) SELECT c.x FROM c");
6496        let node = lineage("x", &expr, None, false).unwrap();
6497
6498        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6499        assert!(
6500            all_names.iter().any(|name| name == "t.x"),
6501            "Expected schema-less CTE star passthrough to reach t.x, got: {:?}",
6502            all_names
6503        );
6504
6505        let cte_node = node
6506            .walk()
6507            .find(|child| child.source_kind == SourceKind::Cte && child.source_name == "c")
6508            .expect("expected CTE hop with source_name c");
6509        assert_eq!(cte_node.source_kind, SourceKind::Cte);
6510        assert_eq!(cte_node.source_name, "c");
6511    }
6512
6513    #[test]
6514    fn test_lineage_schema_less_cte_star_passthrough_with_aggregation() {
6515        let expr = parse(
6516            "WITH c AS (SELECT * FROM t) \
6517             SELECT SUM(c.x) AS s FROM c GROUP BY 1",
6518        );
6519        let node = lineage("s", &expr, None, false).unwrap();
6520
6521        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6522        assert!(
6523            all_names.iter().any(|name| name == "t.x"),
6524            "Expected aggregate over CTE star passthrough to reach t.x, got: {:?}",
6525            all_names
6526        );
6527    }
6528
6529    #[test]
6530    fn test_lineage_schema_less_cte_star_passthrough_with_join_and_alias() {
6531        let expr = parse(
6532            "WITH a AS (SELECT * FROM t1), b AS (SELECT * FROM t2) \
6533             SELECT SUM(b.x) AS s FROM a LEFT JOIN b ON b.id = a.id GROUP BY a.k",
6534        );
6535        let node = lineage("s", &expr, None, false).unwrap();
6536
6537        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6538        assert!(
6539            all_names.iter().any(|name| name == "t2.x"),
6540            "Expected joined CTE star passthrough to reach t2.x, got: {:?}",
6541            all_names
6542        );
6543    }
6544
6545    #[test]
6546    fn test_lineage_schema_less_chained_cte_star_passthrough() {
6547        let expr = parse(
6548            "WITH c1 AS (SELECT * FROM t), \
6549             c2 AS (SELECT * FROM c1), \
6550             c3 AS (SELECT * FROM c2) \
6551             SELECT c3.x FROM c3",
6552        );
6553        let node = lineage("x", &expr, None, false).unwrap();
6554
6555        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6556        assert!(
6557            all_names.iter().any(|name| name == "t.x"),
6558            "Expected chained CTE star passthrough to reach t.x, got: {:?}",
6559            all_names
6560        );
6561    }
6562
6563    #[test]
6564    fn test_lineage_schema_less_unqualified_star_with_multiple_sources_does_not_guess() {
6565        let expr = parse("SELECT * FROM t1 JOIN t2 ON t1.id = t2.id");
6566        let result = lineage("x", &expr, None, false);
6567
6568        assert!(
6569            result.is_err(),
6570            "Unqualified star over multiple sources should remain ambiguous, got: {:?}",
6571            result
6572        );
6573    }
6574
6575    #[test]
6576    fn test_lineage_cte_select_star_renamed_column() {
6577        // dbt standard pattern: CTE with column rename + outer SELECT *
6578        // This is the primary use case for dbt projects (jaffle-shop etc.)
6579        let expr =
6580            parse("WITH renamed AS (SELECT id AS customer_id FROM source) SELECT * FROM renamed");
6581        let node = lineage("customer_id", &expr, None, false).unwrap();
6582
6583        assert_eq!(node.name, "customer_id");
6584        // Should trace customer_id → renamed CTE → source.id
6585        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6586        assert!(
6587            all_names.len() >= 2,
6588            "Expected at least 2 nodes (customer_id → source), got: {:?}",
6589            all_names
6590        );
6591    }
6592
6593    #[test]
6594    fn test_lineage_cte_select_star_multiple_columns() {
6595        // CTE exposes multiple columns, outer SELECT * should resolve each
6596        let expr = parse("WITH cte AS (SELECT a, b, c FROM t) SELECT * FROM cte");
6597
6598        for col in &["a", "b", "c"] {
6599            let node = lineage(col, &expr, None, false).unwrap();
6600            assert_eq!(node.name, *col);
6601            // Verify lineage resolves without error (star expanded to explicit columns)
6602            let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6603            assert!(
6604                all_names.len() >= 2,
6605                "Expected at least 2 nodes for column {}, got: {:?}",
6606                col,
6607                all_names
6608            );
6609        }
6610    }
6611
6612    #[test]
6613    fn test_lineage_nested_cte_select_star() {
6614        // Nested CTE star expansion: cte2 references cte1 via SELECT *
6615        let expr = parse(
6616            "WITH cte1 AS (SELECT a FROM t), \
6617             cte2 AS (SELECT * FROM cte1) \
6618             SELECT * FROM cte2",
6619        );
6620        let node = lineage("a", &expr, None, false).unwrap();
6621
6622        assert_eq!(node.name, "a");
6623        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6624        assert!(
6625            all_names.len() >= 3,
6626            "Expected at least 3 nodes (a → cte2 → cte1 → t.a), got: {:?}",
6627            all_names
6628        );
6629    }
6630
6631    #[test]
6632    fn test_lineage_three_level_nested_cte_star() {
6633        // Three-level nested CTE: cte3 → cte2 → cte1 → t
6634        let expr = parse(
6635            "WITH cte1 AS (SELECT x FROM t), \
6636             cte2 AS (SELECT * FROM cte1), \
6637             cte3 AS (SELECT * FROM cte2) \
6638             SELECT * FROM cte3",
6639        );
6640        let node = lineage("x", &expr, None, false).unwrap();
6641
6642        assert_eq!(node.name, "x");
6643        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6644        assert!(
6645            all_names.len() >= 4,
6646            "Expected at least 4 nodes through 3-level CTE chain, got: {:?}",
6647            all_names
6648        );
6649    }
6650
6651    #[test]
6652    fn test_lineage_cte_union_star() {
6653        // CTE with UNION body, outer SELECT * should resolve from left branch
6654        let expr = parse(
6655            "WITH cte AS (SELECT a, b FROM t1 UNION ALL SELECT a, b FROM t2) \
6656             SELECT * FROM cte",
6657        );
6658        let node = lineage("a", &expr, None, false).unwrap();
6659
6660        assert_eq!(node.name, "a");
6661        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6662        assert!(
6663            all_names.len() >= 2,
6664            "Expected at least 2 nodes for CTE union star, got: {:?}",
6665            all_names
6666        );
6667    }
6668
6669    #[test]
6670    fn test_issue_368_expand_cte_stars_rewrites_every_union_arm() {
6671        let mut expr = parse_one(ISSUE_368_SQL, DialectType::BigQuery).unwrap();
6672
6673        expand_cte_stars(&mut expr, None);
6674
6675        assert_eq!(
6676            crate::generate(&expr, DialectType::BigQuery).unwrap(),
6677            "WITH base AS (SELECT 1 AS col_a), literal_branch AS (SELECT 2 AS col_a), \
6678             unioned AS (SELECT base.col_a FROM base UNION ALL \
6679             SELECT literal_branch.col_a FROM literal_branch) \
6680             SELECT col_a FROM unioned"
6681        );
6682    }
6683
6684    #[test]
6685    fn test_issue_368_lineage_resolves_non_leftmost_union_star() {
6686        let expr = parse_one(ISSUE_368_SQL, DialectType::BigQuery).unwrap();
6687
6688        let node = lineage("col_a", &expr, Some(DialectType::BigQuery), false).unwrap();
6689        let names = lineage_names(&node);
6690
6691        assert!(
6692            node.walk().any(|child| {
6693                child.name == "col_a"
6694                    && child.source_name == "literal_branch"
6695                    && child.source_kind == SourceKind::Cte
6696            }),
6697            "expected the right UNION branch to resolve to literal_branch.col_a, got {node:#?}"
6698        );
6699        assert!(
6700            !names
6701                .iter()
6702                .any(|name| name == "*" || name == "literal_branch.*"),
6703            "did not expect an unresolved right-branch star, got {names:?}"
6704        );
6705    }
6706
6707    #[test]
6708    fn test_expand_cte_stars_rewrites_all_set_operation_kinds() {
6709        for operator in ["UNION ALL", "INTERSECT", "EXCEPT"] {
6710            let mut expr = parse(&format!(
6711                "WITH left_cte AS (SELECT 1 AS col_a), \
6712                 right_cte AS (SELECT 2 AS col_a), \
6713                 combined AS (SELECT * FROM left_cte {operator} SELECT * FROM right_cte) \
6714                 SELECT col_a FROM combined"
6715            ));
6716
6717            expand_cte_stars(&mut expr, None);
6718
6719            let sql = crate::generate(&expr, DialectType::Generic).unwrap();
6720            assert!(
6721                sql.contains("SELECT left_cte.col_a FROM left_cte"),
6722                "expected left arm expansion for {operator}, got {sql}"
6723            );
6724            assert!(
6725                sql.contains("SELECT right_cte.col_a FROM right_cte"),
6726                "expected right arm expansion for {operator}, got {sql}"
6727            );
6728        }
6729    }
6730
6731    #[test]
6732    fn test_expand_cte_stars_rewrites_nested_parenthesized_set_operations() {
6733        let mut expr = parse(
6734            "WITH a AS (SELECT 1 AS x), \
6735             b AS (SELECT 2 AS x), \
6736             c AS (SELECT 3 AS x), \
6737             combined AS ((SELECT * FROM a UNION ALL SELECT * FROM b) \
6738             UNION ALL SELECT * FROM c) \
6739             SELECT x FROM combined",
6740        );
6741
6742        expand_cte_stars(&mut expr, None);
6743
6744        let sql = crate::generate(&expr, DialectType::Generic).unwrap();
6745        for source in ["a", "b", "c"] {
6746            assert!(
6747                sql.contains(&format!("SELECT {source}.x FROM {source}")),
6748                "expected nested arm {source} to be expanded, got {sql}"
6749            );
6750        }
6751    }
6752
6753    #[test]
6754    fn test_expand_cte_stars_rewrites_root_set_operation() {
6755        let mut expr = parse(
6756            "WITH a AS (SELECT 1 AS x), b AS (SELECT 2 AS x) \
6757             SELECT * FROM a UNION ALL SELECT * FROM b",
6758        );
6759
6760        expand_cte_stars(&mut expr, None);
6761
6762        let sql = crate::generate(&expr, DialectType::Generic).unwrap();
6763        assert!(
6764            sql.contains("SELECT a.x FROM a UNION ALL SELECT b.x FROM b"),
6765            "expected both root UNION arms to be expanded, got {sql}"
6766        );
6767    }
6768
6769    #[test]
6770    fn test_expand_cte_stars_preserves_leftmost_output_names() {
6771        let mut expr = parse(
6772            "WITH a AS (SELECT 1 AS left_name), \
6773             b AS (SELECT 2 AS right_name), \
6774             combined AS (SELECT * FROM a UNION ALL SELECT * FROM b) \
6775             SELECT * FROM combined",
6776        );
6777
6778        expand_cte_stars(&mut expr, None);
6779
6780        let sql = crate::generate(&expr, DialectType::Generic).unwrap();
6781        assert!(
6782            sql.ends_with("SELECT combined.left_name FROM combined"),
6783            "expected the set operation output name to come from the left arm, got {sql}"
6784        );
6785        assert!(
6786            sql.contains("SELECT b.right_name FROM b"),
6787            "expected the differently named right arm to still be expanded, got {sql}"
6788        );
6789    }
6790
6791    #[test]
6792    fn test_expand_cte_stars_rewrites_body_with_explicit_cte_columns() {
6793        let mut expr = parse(
6794            "WITH a AS (SELECT 1 AS x), \
6795             b AS (SELECT 2 AS x), \
6796             combined(output_name) AS (SELECT * FROM a UNION ALL SELECT * FROM b) \
6797             SELECT * FROM combined",
6798        );
6799
6800        expand_cte_stars(&mut expr, None);
6801
6802        let sql = crate::generate(&expr, DialectType::Generic).unwrap();
6803        assert!(
6804            sql.contains("SELECT a.x FROM a UNION ALL SELECT b.x FROM b"),
6805            "expected explicit aliases not to suppress body expansion, got {sql}"
6806        );
6807        assert!(
6808            sql.ends_with("SELECT combined.output_name FROM combined"),
6809            "expected the explicit CTE output name to override the body name, got {sql}"
6810        );
6811    }
6812
6813    #[test]
6814    fn test_expand_cte_stars_keeps_recursive_self_reference_conservative() {
6815        let mut expr = parse(
6816            "WITH RECURSIVE r(x) AS (\
6817             SELECT 1 AS x UNION ALL SELECT * FROM r\
6818             ) SELECT * FROM r",
6819        );
6820
6821        expand_cte_stars(&mut expr, None);
6822
6823        let sql = crate::generate(&expr, DialectType::Generic).unwrap();
6824        assert!(
6825            sql.contains("UNION ALL SELECT * FROM r"),
6826            "expected the recursive body star to remain untouched, got {sql}"
6827        );
6828        assert!(
6829            sql.ends_with("SELECT r.x FROM r"),
6830            "expected the explicit recursive CTE column to expand the outer star, got {sql}"
6831        );
6832    }
6833
6834    #[test]
6835    fn test_expand_cte_stars_preserves_genuinely_unresolved_branch_star() {
6836        let mut expr = parse(
6837            "WITH known AS (SELECT 1 AS x), \
6838             combined AS (SELECT * FROM known UNION ALL SELECT * FROM missing) \
6839             SELECT * FROM combined",
6840        );
6841
6842        expand_cte_stars(&mut expr, None);
6843
6844        let sql = crate::generate(&expr, DialectType::Generic).unwrap();
6845        assert!(
6846            sql.contains("SELECT known.x FROM known UNION ALL SELECT * FROM missing"),
6847            "expected only the resolvable branch to expand, got {sql}"
6848        );
6849        assert!(
6850            sql.ends_with("SELECT combined.x FROM combined"),
6851            "expected the leftmost output name to remain usable, got {sql}"
6852        );
6853    }
6854
6855    #[test]
6856    fn test_lineage_cte_star_unknown_table() {
6857        // When CTE references an unknown table, star expansion is skipped gracefully
6858        // and lineage falls back to normal resolution (which may fail)
6859        let expr = parse(
6860            "WITH cte AS (SELECT * FROM unknown_table) \
6861             SELECT * FROM cte",
6862        );
6863        // This should not panic — it may succeed or fail depending on resolution,
6864        // but should not crash
6865        let _result = lineage("x", &expr, None, false);
6866    }
6867
6868    #[test]
6869    fn test_lineage_cte_explicit_columns() {
6870        // CTE with explicit column list: cte(x, y) AS (SELECT a, b FROM t)
6871        let expr = parse(
6872            "WITH cte(x, y) AS (SELECT a, b FROM t) \
6873             SELECT * FROM cte",
6874        );
6875        let node = lineage("x", &expr, None, false).unwrap();
6876        assert_eq!(node.name, "x");
6877    }
6878
6879    #[test]
6880    fn test_lineage_cte_qualified_star() {
6881        // Qualified star: SELECT cte.* FROM cte
6882        let expr = parse(
6883            "WITH cte AS (SELECT a, b FROM t) \
6884             SELECT cte.* FROM cte",
6885        );
6886        for col in &["a", "b"] {
6887            let node = lineage(col, &expr, None, false).unwrap();
6888            assert_eq!(node.name, *col);
6889            let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
6890            assert!(
6891                all_names.len() >= 2,
6892                "Expected at least 2 nodes for qualified star column {}, got: {:?}",
6893                col,
6894                all_names
6895            );
6896        }
6897    }
6898
6899    #[test]
6900    fn test_lineage_subquery_select_star() {
6901        // Ported from sqlglot: test_select_star
6902        // SELECT x FROM (SELECT * FROM table_a)
6903        let expr = parse("SELECT x FROM (SELECT * FROM table_a)");
6904        let node = lineage("x", &expr, None, false).unwrap();
6905
6906        assert_eq!(node.name, "x");
6907        assert!(
6908            !node.downstream.is_empty(),
6909            "Expected downstream nodes for subquery with SELECT *, got none"
6910        );
6911    }
6912
6913    #[test]
6914    fn test_lineage_cte_star_with_schema_external_table() {
6915        // CTE references an external table via SELECT * — schema enables expansion
6916        let sql = r#"WITH orders AS (SELECT * FROM stg_orders)
6917SELECT * FROM orders"#;
6918        let expr = parse(sql);
6919
6920        let mut schema = MappingSchema::new();
6921        let cols = vec![
6922            ("order_id".to_string(), DataType::Unknown),
6923            ("customer_id".to_string(), DataType::Unknown),
6924            ("amount".to_string(), DataType::Unknown),
6925        ];
6926        schema.add_table("stg_orders", &cols, None).unwrap();
6927
6928        let node =
6929            lineage_with_schema("order_id", &expr, Some(&schema as &dyn Schema), None, false)
6930                .unwrap();
6931        assert_eq!(node.name, "order_id");
6932    }
6933
6934    #[test]
6935    fn test_lineage_cte_star_with_schema_three_part_name() {
6936        // CTE references an external table with fully-qualified 3-part name
6937        let sql = r#"WITH orders AS (SELECT * FROM "db"."schema"."stg_orders")
6938SELECT * FROM orders"#;
6939        let expr = parse(sql);
6940
6941        let mut schema = MappingSchema::new();
6942        let cols = vec![
6943            ("order_id".to_string(), DataType::Unknown),
6944            ("customer_id".to_string(), DataType::Unknown),
6945        ];
6946        schema
6947            .add_table("db.schema.stg_orders", &cols, None)
6948            .unwrap();
6949
6950        let node = lineage_with_schema(
6951            "customer_id",
6952            &expr,
6953            Some(&schema as &dyn Schema),
6954            None,
6955            false,
6956        )
6957        .unwrap();
6958        assert_eq!(node.name, "customer_id");
6959    }
6960
6961    #[test]
6962    fn test_lineage_cte_star_with_schema_nested() {
6963        // Nested CTEs: outer CTE references inner CTE with SELECT *,
6964        // inner CTE references external table with SELECT *
6965        let sql = r#"WITH
6966            raw AS (SELECT * FROM external_table),
6967            enriched AS (SELECT * FROM raw)
6968        SELECT * FROM enriched"#;
6969        let expr = parse(sql);
6970
6971        let mut schema = MappingSchema::new();
6972        let cols = vec![
6973            ("id".to_string(), DataType::Unknown),
6974            ("name".to_string(), DataType::Unknown),
6975        ];
6976        schema.add_table("external_table", &cols, None).unwrap();
6977
6978        let node =
6979            lineage_with_schema("name", &expr, Some(&schema as &dyn Schema), None, false).unwrap();
6980        assert_eq!(node.name, "name");
6981    }
6982
6983    #[test]
6984    fn test_lineage_cte_qualified_star_with_schema() {
6985        // CTE uses qualified star (orders.*) from a CTE whose columns
6986        // come from an external table via SELECT *
6987        let sql = r#"WITH
6988            orders AS (SELECT * FROM stg_orders),
6989            enriched AS (
6990                SELECT orders.*, 'extra' AS extra
6991                FROM orders
6992            )
6993        SELECT * FROM enriched"#;
6994        let expr = parse(sql);
6995
6996        let mut schema = MappingSchema::new();
6997        let cols = vec![
6998            ("order_id".to_string(), DataType::Unknown),
6999            ("total".to_string(), DataType::Unknown),
7000        ];
7001        schema.add_table("stg_orders", &cols, None).unwrap();
7002
7003        let node =
7004            lineage_with_schema("order_id", &expr, Some(&schema as &dyn Schema), None, false)
7005                .unwrap();
7006        assert_eq!(node.name, "order_id");
7007
7008        // Also verify the extra column works
7009        let extra =
7010            lineage_with_schema("extra", &expr, Some(&schema as &dyn Schema), None, false).unwrap();
7011        assert_eq!(extra.name, "extra");
7012    }
7013
7014    #[test]
7015    fn test_lineage_cte_star_without_schema_still_works() {
7016        // Without schema, CTE-to-CTE star expansion still works
7017        let sql = r#"WITH
7018            cte1 AS (SELECT id, name FROM raw_table),
7019            cte2 AS (SELECT * FROM cte1)
7020        SELECT * FROM cte2"#;
7021        let expr = parse(sql);
7022
7023        // No schema — should still resolve through CTE chain
7024        let node = lineage("id", &expr, None, false).unwrap();
7025        assert_eq!(node.name, "id");
7026    }
7027
7028    #[test]
7029    fn test_lineage_nested_cte_star_with_join_and_schema() {
7030        // Reproduces dbt pattern: CTE chain with qualified star and JOIN
7031        // base_orders -> with_payments (JOIN) -> final -> outer SELECT
7032        let sql = r#"WITH
7033base_orders AS (
7034    SELECT * FROM stg_orders
7035),
7036with_payments AS (
7037    SELECT
7038        base_orders.*,
7039        p.amount
7040    FROM base_orders
7041    LEFT JOIN stg_payments p ON base_orders.order_id = p.order_id
7042),
7043final_cte AS (
7044    SELECT * FROM with_payments
7045)
7046SELECT * FROM final_cte"#;
7047        let expr = parse(sql);
7048
7049        let mut schema = MappingSchema::new();
7050        let order_cols = vec![
7051            (
7052                "order_id".to_string(),
7053                crate::expressions::DataType::Unknown,
7054            ),
7055            (
7056                "customer_id".to_string(),
7057                crate::expressions::DataType::Unknown,
7058            ),
7059            ("status".to_string(), crate::expressions::DataType::Unknown),
7060        ];
7061        let pay_cols = vec![
7062            (
7063                "payment_id".to_string(),
7064                crate::expressions::DataType::Unknown,
7065            ),
7066            (
7067                "order_id".to_string(),
7068                crate::expressions::DataType::Unknown,
7069            ),
7070            ("amount".to_string(), crate::expressions::DataType::Unknown),
7071        ];
7072        schema.add_table("stg_orders", &order_cols, None).unwrap();
7073        schema.add_table("stg_payments", &pay_cols, None).unwrap();
7074
7075        // order_id should trace back to stg_orders
7076        let node =
7077            lineage_with_schema("order_id", &expr, Some(&schema as &dyn Schema), None, false)
7078                .unwrap();
7079        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
7080
7081        // The leaf should be "stg_orders.order_id" (not just "order_id")
7082        let has_table_qualified = all_names
7083            .iter()
7084            .any(|n| n.contains('.') && n.contains("order_id"));
7085        assert!(
7086            has_table_qualified,
7087            "Expected table-qualified leaf like 'stg_orders.order_id', got: {:?}",
7088            all_names
7089        );
7090
7091        // amount should trace back to stg_payments
7092        let node = lineage_with_schema("amount", &expr, Some(&schema as &dyn Schema), None, false)
7093            .unwrap();
7094        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
7095
7096        let has_table_qualified = all_names
7097            .iter()
7098            .any(|n| n.contains('.') && n.contains("amount"));
7099        assert!(
7100            has_table_qualified,
7101            "Expected table-qualified leaf like 'stg_payments.amount', got: {:?}",
7102            all_names
7103        );
7104    }
7105
7106    #[test]
7107    fn test_lineage_cte_alias_resolution() {
7108        // FROM cte_name AS alias pattern: alias should resolve through CTE to source table
7109        let sql = r#"WITH import_stg_items AS (
7110    SELECT item_id, name, status FROM stg_items
7111)
7112SELECT base.item_id, base.status
7113FROM import_stg_items AS base"#;
7114        let expr = parse(sql);
7115
7116        let node = lineage("item_id", &expr, None, false).unwrap();
7117        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
7118        // Should trace through alias "base" → CTE "import_stg_items" → "stg_items.item_id"
7119        assert!(
7120            all_names.iter().any(|n| n == "stg_items.item_id"),
7121            "Expected leaf 'stg_items.item_id', got: {:?}",
7122            all_names
7123        );
7124    }
7125
7126    #[test]
7127    fn test_lineage_cte_alias_with_schema_and_star() {
7128        // CTE alias + SELECT * expansion: FROM cte AS alias with star in CTE body
7129        let sql = r#"WITH import_stg AS (
7130    SELECT * FROM stg_items
7131)
7132SELECT base.item_id, base.status
7133FROM import_stg AS base"#;
7134        let expr = parse(sql);
7135
7136        let mut schema = MappingSchema::new();
7137        schema
7138            .add_table(
7139                "stg_items",
7140                &[
7141                    ("item_id".to_string(), DataType::Unknown),
7142                    ("name".to_string(), DataType::Unknown),
7143                    ("status".to_string(), DataType::Unknown),
7144                ],
7145                None,
7146            )
7147            .unwrap();
7148
7149        let node = lineage_with_schema("item_id", &expr, Some(&schema as &dyn Schema), None, false)
7150            .unwrap();
7151        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
7152        assert!(
7153            all_names.iter().any(|n| n == "stg_items.item_id"),
7154            "Expected leaf 'stg_items.item_id', got: {:?}",
7155            all_names
7156        );
7157    }
7158
7159    #[test]
7160    fn test_lineage_cte_alias_with_join() {
7161        // Multiple CTE aliases in a JOIN: each should resolve independently
7162        let sql = r#"WITH
7163    import_users AS (SELECT id, name FROM users),
7164    import_orders AS (SELECT id, user_id, amount FROM orders)
7165SELECT u.name, o.amount
7166FROM import_users AS u
7167LEFT JOIN import_orders AS o ON u.id = o.user_id"#;
7168        let expr = parse(sql);
7169
7170        let node = lineage("name", &expr, None, false).unwrap();
7171        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
7172        assert!(
7173            all_names.iter().any(|n| n == "users.name"),
7174            "Expected leaf 'users.name', got: {:?}",
7175            all_names
7176        );
7177
7178        let node = lineage("amount", &expr, None, false).unwrap();
7179        let all_names: Vec<_> = node.walk().map(|n| n.name.clone()).collect();
7180        assert!(
7181            all_names.iter().any(|n| n == "orders.amount"),
7182            "Expected leaf 'orders.amount', got: {:?}",
7183            all_names
7184        );
7185    }
7186
7187    // -----------------------------------------------------------------------
7188    // Quoted CTE name tests — verifying SQL identifier case semantics
7189    // -----------------------------------------------------------------------
7190
7191    #[test]
7192    fn test_lineage_unquoted_cte_case_insensitive() {
7193        // Unquoted CTE names are case-insensitive (both normalized to lowercase).
7194        // MyCte and MYCTE should match.
7195        let expr = parse("WITH MyCte AS (SELECT id AS col FROM source) SELECT * FROM MYCTE");
7196        let node = lineage("col", &expr, None, false).unwrap();
7197        assert_eq!(node.name, "col");
7198        assert!(
7199            !node.downstream.is_empty(),
7200            "Unquoted CTE should resolve case-insensitively"
7201        );
7202    }
7203
7204    #[test]
7205    fn test_lineage_quoted_cte_case_preserved() {
7206        // Quoted CTE name preserves case. "MyCte" referenced as "MyCte" should match.
7207        let expr = parse(r#"WITH "MyCte" AS (SELECT id AS col FROM source) SELECT * FROM "MyCte""#);
7208        let node = lineage("col", &expr, None, false).unwrap();
7209        assert_eq!(node.name, "col");
7210        assert!(
7211            !node.downstream.is_empty(),
7212            "Quoted CTE with matching case should resolve"
7213        );
7214    }
7215
7216    #[test]
7217    fn test_lineage_quoted_cte_case_mismatch_no_expansion() {
7218        // Quoted CTE "MyCte" referenced as "mycte" — case mismatch.
7219        // sqlglot treats this as a table reference, not a CTE match.
7220        // Star expansion should NOT resolve through the CTE.
7221        let expr = parse(r#"WITH "MyCte" AS (SELECT id AS col FROM source) SELECT * FROM "mycte""#);
7222        // lineage("col", ...) should fail because "mycte" is treated as an external
7223        // table (not matching CTE "MyCte"), and SELECT * cannot be expanded.
7224        let result = lineage("col", &expr, None, false);
7225        assert!(
7226            result.is_err(),
7227            "Quoted CTE with case mismatch should not expand star: {:?}",
7228            result
7229        );
7230    }
7231
7232    #[test]
7233    fn test_lineage_mixed_quoted_unquoted_cte() {
7234        // Mix of unquoted and quoted CTEs in a nested chain.
7235        let expr = parse(
7236            r#"WITH unquoted AS (SELECT 1 AS a FROM t), "Quoted" AS (SELECT a FROM unquoted) SELECT * FROM "Quoted""#,
7237        );
7238        let node = lineage("a", &expr, None, false).unwrap();
7239        assert_eq!(node.name, "a");
7240        assert!(
7241            !node.downstream.is_empty(),
7242            "Mixed quoted/unquoted CTE chain should resolve"
7243        );
7244    }
7245
7246    // -----------------------------------------------------------------------
7247    // Known bugs: quoted CTE case sensitivity in scope/lineage tracing paths
7248    // -----------------------------------------------------------------------
7249    //
7250    // expand_cte_stars correctly handles quoted vs unquoted CTE names via
7251    // normalize_cte_name(). However, the scope system (scope.rs add_table_to_scope)
7252    // and the lineage tracing path (to_node_inner) use eq_ignore_ascii_case or
7253    // direct string comparison for CTE name matching, ignoring the quoted status.
7254    //
7255    // sqlglot's normalize_identifiers treats quoted identifiers as case-sensitive
7256    // and unquoted as case-insensitive. The scope system should do the same.
7257    //
7258    // Fixing these requires changes across scope.rs and lineage.rs CTE resolution,
7259    // which is broader than the star expansion scope of this PR.
7260
7261    #[test]
7262    fn test_lineage_quoted_cte_case_mismatch_non_star_known_bug() {
7263        // Known bug: scope.rs add_table_to_scope uses eq_ignore_ascii_case for
7264        // all identifiers including quoted ones, so quoted CTE "MyCte" referenced
7265        // as "mycte" incorrectly resolves to the CTE.
7266        //
7267        // Per SQL semantics (and sqlglot behavior), quoted identifiers are
7268        // case-sensitive: "mycte" should NOT match CTE "MyCte".
7269        //
7270        // This test asserts the CURRENT BUGGY behavior. When the bug is fixed,
7271        // this test should fail — update the assertion to match correct behavior:
7272        //   child.source_name should be "" (table ref), not "MyCte" (CTE ref).
7273        let expr = parse(r#"WITH "MyCte" AS (SELECT 1 AS col) SELECT col FROM "mycte""#);
7274        let node = lineage("col", &expr, None, false).unwrap();
7275        assert!(!node.downstream.is_empty());
7276        let child = &node.downstream[0];
7277        // BUG: "mycte" incorrectly resolves to CTE "MyCte"
7278        assert_eq!(
7279            child.source_name, "MyCte",
7280            "Known bug: quoted CTE case mismatch should NOT resolve, but currently does. \
7281             If this fails, the bug may be fixed — update to assert source_name != \"MyCte\""
7282        );
7283    }
7284
7285    #[test]
7286    fn test_lineage_quoted_cte_case_mismatch_qualified_col_known_bug() {
7287        // Known bug: same as above but with qualified column reference ("mycte".col).
7288        // scope.rs resolves "mycte" to CTE "MyCte" case-insensitively even for
7289        // quoted identifiers, so "mycte".col incorrectly traces through CTE "MyCte".
7290        //
7291        // This test asserts the CURRENT BUGGY behavior. When the bug is fixed,
7292        // this test should fail — update to assert source_name != "MyCte".
7293        let expr = parse(r#"WITH "MyCte" AS (SELECT 1 AS col) SELECT "mycte".col FROM "mycte""#);
7294        let node = lineage("col", &expr, None, false).unwrap();
7295        assert!(!node.downstream.is_empty());
7296        let child = &node.downstream[0];
7297        // BUG: "mycte".col incorrectly resolves through CTE "MyCte"
7298        assert_eq!(
7299            child.source_name, "MyCte",
7300            "Known bug: quoted CTE case mismatch should NOT resolve, but currently does. \
7301             If this fails, the bug may be fixed — update to assert source_name != \"MyCte\""
7302        );
7303    }
7304
7305    #[test]
7306    fn test_lineage_recursive_cte_terminates_at_base_case() {
7307        let expr = parse_dialect(
7308            "WITH RECURSIVE nums AS (\
7309             SELECT 1 AS n \
7310             UNION ALL \
7311             SELECT n + 1 FROM nums WHERE n < 5\
7312             ) SELECT n FROM nums",
7313            DialectType::DuckDB,
7314        );
7315        let node = lineage("n", &expr, Some(DialectType::DuckDB), false).unwrap();
7316        let names = lineage_names(&node);
7317
7318        assert!(
7319            names.len() <= 12,
7320            "recursive CTE lineage should not unroll repeatedly, got {names:?}"
7321        );
7322        assert!(
7323            node.walk()
7324                .any(|child| child.source_kind == SourceKind::Cte && child.source_name == "nums"),
7325            "expected recursive source to be marked as a CTE, got {names:?}"
7326        );
7327    }
7328
7329    #[test]
7330    fn test_lineage_window_partition_and_order_columns() {
7331        let expr = parse(
7332            "WITH c AS (SELECT user_id, ts FROM events) \
7333             SELECT ROW_NUMBER() OVER (PARTITION BY c.user_id ORDER BY c.ts) AS out FROM c",
7334        );
7335        let node = lineage("out", &expr, None, false).unwrap();
7336
7337        assert_lineage_contains(&node, "events.user_id");
7338        assert_lineage_contains(&node, "events.ts");
7339    }
7340
7341    #[test]
7342    fn test_lineage_window_aggregate_order_column() {
7343        let expr = parse(
7344            "WITH c AS (SELECT amount, d FROM txns) \
7345             SELECT SUM(c.amount) OVER (ORDER BY c.d) AS running FROM c",
7346        );
7347        let node = lineage("running", &expr, None, false).unwrap();
7348
7349        assert_lineage_contains(&node, "txns.amount");
7350        assert_lineage_contains(&node, "txns.d");
7351    }
7352
7353    #[test]
7354    fn test_lineage_named_window_columns() {
7355        let expr = parse(
7356            "SELECT ROW_NUMBER() OVER w AS out \
7357             FROM events \
7358             WINDOW w AS (PARTITION BY user_id ORDER BY ts)",
7359        );
7360        let node = lineage("out", &expr, None, false).unwrap();
7361
7362        assert_lineage_contains(&node, "events.user_id");
7363        assert_lineage_contains(&node, "events.ts");
7364    }
7365
7366    #[test]
7367    fn test_lineage_within_group_order_column() {
7368        let expr =
7369            parse("SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS p FROM txns");
7370        let node = lineage("p", &expr, None, false).unwrap();
7371
7372        assert_lineage_contains(&node, "txns.amount");
7373    }
7374
7375    #[test]
7376    fn test_lineage_query_wrappers_resolve_inner_select() {
7377        for sql in [
7378            "CREATE TABLE tgt AS SELECT x FROM src",
7379            "CREATE VIEW v AS SELECT x FROM src",
7380            "INSERT INTO tgt SELECT x FROM src",
7381        ] {
7382            let expr = parse(sql);
7383            let node = lineage("x", &expr, None, false).unwrap();
7384            assert_lineage_contains(&node, "src.x");
7385        }
7386    }
7387
7388    #[test]
7389    fn test_lineage_scalar_subquery_through_cte_reaches_base_table() {
7390        let expr = parse(
7391            "WITH c AS (SELECT x FROM t) \
7392             SELECT (SELECT SUM(x) FROM c) AS s FROM c LIMIT 1",
7393        );
7394        let node = lineage("s", &expr, None, false).unwrap();
7395
7396        assert_lineage_contains(&node, "t.x");
7397        assert!(
7398            node.walk()
7399                .any(|child| child.source_kind == SourceKind::Cte && child.source_name == "c"),
7400            "expected scalar subquery CTE hop in lineage, got {:?}",
7401            lineage_names(&node)
7402        );
7403    }
7404
7405    #[test]
7406    fn test_lineage_scalar_subqueries_inside_expression_wrappers() {
7407        for sql in [
7408            "WITH c AS (SELECT a, b FROM t) \
7409             SELECT CASE WHEN c.a > 0 THEN c.b ELSE (SELECT MAX(z) FROM o) END AS r FROM c",
7410            "WITH c AS (SELECT a FROM t) \
7411             SELECT COALESCE(c.a, (SELECT MAX(z) FROM o)) AS r FROM c",
7412            "WITH c AS (SELECT a FROM t) \
7413             SELECT CAST((SELECT MAX(z) FROM o) AS INT) + c.a AS r FROM c",
7414            "WITH c AS (SELECT a FROM t) \
7415             SELECT CASE WHEN c.a BETWEEN 0 AND (SELECT MAX(z) FROM o) THEN c.a END AS r FROM c",
7416        ] {
7417            let expr = parse_dialect(sql, DialectType::DuckDB);
7418            let node = lineage("r", &expr, Some(DialectType::DuckDB), false)
7419                .unwrap_or_else(|error| panic!("lineage failed for {sql}: {error}"));
7420
7421            assert_lineage_contains(&node, "o.z");
7422            assert_lineage_contains(&node, "t.a");
7423        }
7424    }
7425
7426    #[test]
7427    fn test_lineage_nested_set_operation_inside_derived_table() {
7428        let expr = parse_dialect(
7429            "SELECT v FROM ((SELECT v FROM t1 UNION ALL SELECT v FROM t2) \
7430             UNION ALL SELECT v FROM t3) u",
7431            DialectType::DuckDB,
7432        );
7433        let node = lineage("v", &expr, Some(DialectType::DuckDB), false).unwrap();
7434
7435        assert_lineage_contains(&node, "t1.v");
7436        assert_lineage_contains(&node, "t2.v");
7437        assert_lineage_contains(&node, "t3.v");
7438    }
7439
7440    #[test]
7441    fn test_lineage_select_alias_reference_resolves_to_alias_source() {
7442        let expr = parse_dialect(
7443            "WITH c AS (SELECT x FROM t) SELECT c.x AS a, a + 1 AS b FROM c",
7444            DialectType::DuckDB,
7445        );
7446        let node = lineage("b", &expr, Some(DialectType::DuckDB), false).unwrap();
7447
7448        assert_lineage_contains(&node, "t.x");
7449    }
7450
7451    #[test]
7452    fn test_lineage_pivot_output_resolves_aggregation_input() {
7453        let expr = parse_dialect(
7454            "SELECT * FROM (SELECT region, q, amt FROM sales) \
7455             PIVOT(SUM(amt) FOR q IN ('Q1' AS q1))",
7456            DialectType::DuckDB,
7457        );
7458        let node = lineage("q1", &expr, Some(DialectType::DuckDB), false).unwrap();
7459
7460        assert_lineage_contains(&node, "sales.amt");
7461    }
7462
7463    #[test]
7464    fn test_lineage_pivot_multi_aggregate_and_alias_columns() {
7465        let multi = parse_dialect(
7466            "SELECT * FROM (SELECT category, value, price FROM t) \
7467             PIVOT(SUM(value) AS value_sum, MAX(price) FOR category IN ('a' AS cat_a, 'b'))",
7468            DialectType::DuckDB,
7469        );
7470        let value_sum =
7471            lineage("cat_a_value_sum", &multi, Some(DialectType::DuckDB), false).unwrap();
7472        assert_lineage_contains(&value_sum, "t.value");
7473
7474        let max_price =
7475            lineage("cat_a_max(price)", &multi, Some(DialectType::DuckDB), false).unwrap();
7476        assert_lineage_contains(&max_price, "t.price");
7477
7478        let aliased = parse_dialect(
7479            "SELECT * FROM (SELECT region, q, amt FROM sales) \
7480             PIVOT(SUM(amt) FOR q IN ('Q1')) AS p(region2, p1)",
7481            DialectType::DuckDB,
7482        );
7483        let region = lineage("region2", &aliased, Some(DialectType::DuckDB), false).unwrap();
7484        assert_lineage_contains(&region, "sales.region");
7485
7486        let pivot_value = lineage("p1", &aliased, Some(DialectType::DuckDB), false).unwrap();
7487        assert_lineage_contains(&pivot_value, "sales.amt");
7488    }
7489
7490    #[test]
7491    fn test_lineage_pivot_through_cte_resolves_aggregation_input() {
7492        let expr = parse_dialect(
7493            "WITH src AS (SELECT region, q, amt FROM sales) \
7494             SELECT q1 FROM src PIVOT(SUM(amt) FOR q IN ('Q1' AS q1))",
7495            DialectType::DuckDB,
7496        );
7497        let node = lineage("q1", &expr, Some(DialectType::DuckDB), false).unwrap();
7498
7499        assert_lineage_contains(&node, "sales.amt");
7500    }
7501
7502    #[test]
7503    fn test_lineage_unpivot_value_resolves_input_columns() {
7504        let expr = parse_dialect(
7505            "SELECT name, val FROM t UNPIVOT(val FOR col IN (a, b, c))",
7506            DialectType::DuckDB,
7507        );
7508        let node = lineage("val", &expr, Some(DialectType::DuckDB), false).unwrap();
7509
7510        assert_lineage_contains(&node, "t.a");
7511        assert_lineage_contains(&node, "t.b");
7512        assert_lineage_contains(&node, "t.c");
7513    }
7514
7515    #[test]
7516    fn test_lineage_unpivot_multi_value_columns_resolve_positionally() {
7517        let expr = parse_dialect(
7518            "SELECT first_half_sales, second_half_sales, semester \
7519             FROM produce \
7520             UNPIVOT((first_half_sales, second_half_sales) \
7521             FOR semester IN ((q1, q2) AS 'semester_1', (q3, q4) AS 'semester_2'))",
7522            DialectType::BigQuery,
7523        );
7524
7525        let first = lineage(
7526            "first_half_sales",
7527            &expr,
7528            Some(DialectType::BigQuery),
7529            false,
7530        )
7531        .unwrap();
7532        assert_lineage_contains(&first, "produce.q1");
7533        assert_lineage_contains(&first, "produce.q3");
7534
7535        let second = lineage(
7536            "second_half_sales",
7537            &expr,
7538            Some(DialectType::BigQuery),
7539            false,
7540        )
7541        .unwrap();
7542        assert_lineage_contains(&second, "produce.q2");
7543        assert_lineage_contains(&second, "produce.q4");
7544    }
7545
7546    #[test]
7547    fn test_lineage_top_level_union_over_ctes_reaches_base_tables() {
7548        let expr = parse(
7549            "WITH a AS (SELECT x FROM t1), b AS (SELECT x FROM t2) \
7550             SELECT x FROM a UNION SELECT x FROM b",
7551        );
7552        let node = lineage("x", &expr, None, false).unwrap();
7553
7554        assert_lineage_contains(&node, "t1.x");
7555        assert_lineage_contains(&node, "t2.x");
7556        for cte_name in ["a", "b"] {
7557            assert!(
7558                node.walk().any(|child| child.source_name == cte_name),
7559                "expected set-operation lineage to retain CTE source {cte_name}: {:?}",
7560                lineage_names(&node)
7561            );
7562        }
7563    }
7564
7565    #[test]
7566    fn test_lineage_star_excludes_semi_join_rhs_source() {
7567        let expr = parse_dialect(
7568            "SELECT * FROM orders LEFT SEMI JOIN customers ON orders.customer_id = customers.id",
7569            DialectType::DuckDB,
7570        );
7571        let node = lineage("customer_id", &expr, Some(DialectType::DuckDB), false).unwrap();
7572
7573        assert_lineage_contains(&node, "orders.customer_id");
7574    }
7575
7576    // --- Comment handling tests (ported from sqlglot test_lineage.py) ---
7577
7578    /// sqlglot: test_node_name_doesnt_contain_comment
7579    /// Comments in column expressions should not affect lineage resolution.
7580    /// NOTE: This test uses SELECT * from a derived table, which is a separate
7581    /// known limitation in polyglot-sql (star expansion in subqueries).
7582    #[test]
7583    #[ignore = "requires derived table star expansion (separate issue)"]
7584    fn test_node_name_doesnt_contain_comment() {
7585        let expr = parse("SELECT * FROM (SELECT x /* c */ FROM t1) AS t2");
7586        let node = lineage("x", &expr, None, false).unwrap();
7587
7588        assert_eq!(node.name, "x");
7589        assert!(!node.downstream.is_empty());
7590    }
7591
7592    /// A line comment between SELECT and the first column wraps the column
7593    /// in an Annotated node. Lineage must unwrap it to find the column name.
7594    /// Verify that commented and uncommented queries produce identical lineage.
7595    #[test]
7596    fn test_comment_before_first_column_in_cte() {
7597        let sql_with_comment = "with t as (select 1 as a) select\n  -- comment\n  a from t";
7598        let sql_without_comment = "with t as (select 1 as a) select a from t";
7599
7600        // Without comment — baseline
7601        let expr_ok = parse(sql_without_comment);
7602        let node_ok = lineage("a", &expr_ok, None, false).expect("without comment should succeed");
7603
7604        // With comment — should produce identical lineage
7605        let expr_comment = parse(sql_with_comment);
7606        let node_comment = lineage("a", &expr_comment, None, false)
7607            .expect("with comment before first column should succeed");
7608
7609        assert_eq!(node_ok.name, node_comment.name, "node names should match");
7610        assert_eq!(
7611            node_ok.downstream_names(),
7612            node_comment.downstream_names(),
7613            "downstream lineage should be identical with or without comment"
7614        );
7615    }
7616
7617    /// Block comment between SELECT and first column.
7618    #[test]
7619    fn test_block_comment_before_first_column() {
7620        let sql = "with t as (select 1 as a) select /* section */ a from t";
7621        let expr = parse(sql);
7622        let node = lineage("a", &expr, None, false)
7623            .expect("block comment before first column should succeed");
7624        assert_eq!(node.name, "a");
7625        assert!(
7626            !node.downstream.is_empty(),
7627            "should have downstream lineage"
7628        );
7629    }
7630
7631    /// Comment before first column should not affect second column resolution.
7632    #[test]
7633    fn test_comment_before_first_column_second_col_ok() {
7634        let sql = "with t as (select 1 as a, 2 as b) select\n  -- comment\n  a, b from t";
7635        let expr = parse(sql);
7636
7637        let node_a =
7638            lineage("a", &expr, None, false).expect("column a with comment should succeed");
7639        assert_eq!(node_a.name, "a");
7640
7641        let node_b =
7642            lineage("b", &expr, None, false).expect("column b with comment should succeed");
7643        assert_eq!(node_b.name, "b");
7644    }
7645
7646    /// Aliased column with preceding comment.
7647    #[test]
7648    fn test_comment_before_aliased_column() {
7649        let sql = "with t as (select 1 as x) select\n  -- renamed\n  x as y from t";
7650        let expr = parse(sql);
7651        let node =
7652            lineage("y", &expr, None, false).expect("aliased column with comment should succeed");
7653        assert_eq!(node.name, "y");
7654        assert!(
7655            !node.downstream.is_empty(),
7656            "aliased column should have downstream lineage"
7657        );
7658    }
7659}