use std::collections::{BTreeSet, HashMap};
use std::ops::ControlFlow;
use sqlparser::ast::{
Expr, ObjectName, Query, Select, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, TableFactor, TableWithJoins,
Values, Visit, Visitor,
};
use crate::error::LineageError;
use crate::model::{OutputColumn, SourceRef, TableRef};
use crate::scope::{ExpandedColumns, RelExpose, Scope, VirtCol};
const MAX_DEPTH: usize = 64;
pub(crate) type CteMap<'a> = HashMap<String, &'a Query>;
pub(crate) type ActiveCtes = BTreeSet<String>;
fn fold(s: &str) -> String {
s.to_ascii_lowercase()
}
pub(crate) fn resolve(query: &Query) -> Result<Vec<OutputColumn>, LineageError> {
resolve_query(query, &CteMap::new(), &ActiveCtes::new(), 0)
}
fn resolve_query<'a>(
query: &'a Query,
parent: &CteMap<'a>,
active: &ActiveCtes,
depth: usize,
) -> Result<Vec<OutputColumn>, LineageError> {
if depth > MAX_DEPTH {
return Err(LineageError::DepthLimitExceeded);
}
let mut ctes: CteMap<'a> = parent.clone();
if let Some(with) = &query.with {
for cte in &with.cte_tables {
ctes.insert(fold(&cte.alias.name.value), &cte.query);
}
}
resolve_setexpr(&query.body, &ctes, active, depth)
}
fn resolve_setexpr<'a>(
body: &'a SetExpr,
ctes: &CteMap<'a>,
active: &ActiveCtes,
depth: usize,
) -> Result<Vec<OutputColumn>, LineageError> {
match body {
SetExpr::Select(select) => resolve_select(select, ctes, active, depth),
SetExpr::Query(query) => resolve_query(query, ctes, active, depth + 1),
SetExpr::SetOperation { left, right, .. } => {
let left_cols = resolve_setexpr(left, ctes, active, depth)?;
let right_cols = resolve_setexpr(right, ctes, active, depth)?;
let mut out = Vec::with_capacity(left_cols.len());
for (i, lc) in left_cols.into_iter().enumerate() {
let mut sources = lc.sources;
if let Some(rc) = right_cols.get(i) {
sources.extend(rc.sources.iter().cloned());
}
push_column(&mut out, lc.name, sources);
}
Ok(out)
}
SetExpr::Values(values) => resolve_values(values, ctes, active, depth),
_ => Ok(Vec::new()),
}
}
fn resolve_values<'a>(
values: &'a Values,
ctes: &CteMap<'a>,
active: &ActiveCtes,
depth: usize,
) -> Result<Vec<OutputColumn>, LineageError> {
let scope = Scope::default();
let mut out: Vec<OutputColumn> = Vec::new();
for row in &values.rows {
for (i, expr) in row.content.iter().enumerate() {
let sources = collect_sources(expr, &scope, ctes, active, depth)?;
match out.get_mut(i) {
Some(col) => col.sources.extend(sources),
None => push_column(&mut out, None, sources),
}
}
}
Ok(out)
}
fn resolve_select<'a>(
select: &'a Select,
ctes: &CteMap<'a>,
active: &ActiveCtes,
depth: usize,
) -> Result<Vec<OutputColumn>, LineageError> {
let scope = build_scope(&select.from, ctes, active, depth)?;
let mut out: Vec<OutputColumn> = Vec::new();
for item in &select.projection {
let (expr, name) = match item {
SelectItem::UnnamedExpr(expr) => (expr, output_name(expr)),
SelectItem::ExprWithAlias { expr, alias } => (expr, Some(alias.value.clone())),
SelectItem::ExprWithAliases { expr, aliases } => (expr, aliases.first().map(|a| a.value.clone())),
SelectItem::Wildcard(_) => {
push_expanded(&mut out, scope.expand_wildcard()?);
continue;
}
SelectItem::QualifiedWildcard(SelectItemQualifiedWildcardKind::ObjectName(obj), _) => {
push_expanded(&mut out, scope.expand_qualified_wildcard(last_ident(obj))?);
continue;
}
SelectItem::QualifiedWildcard(SelectItemQualifiedWildcardKind::Expr(_), _) => {
return Err(LineageError::UnresolvedColumn {
column: "*".to_string(),
reason: "wildcard on an arbitrary expression is unsupported".to_string(),
});
}
};
let sources = collect_sources(expr, &scope, ctes, active, depth)?;
push_column(&mut out, name, sources);
}
Ok(out)
}
pub(crate) fn build_scope<'a>(
from: &'a [TableWithJoins],
ctes: &CteMap<'a>,
active: &ActiveCtes,
depth: usize,
) -> Result<Scope, LineageError> {
let mut scope = Scope::default();
for twj in from {
add_table_with_joins(twj, &mut scope, ctes, active, depth)?;
}
Ok(scope)
}
pub(crate) fn add_table_with_joins<'a>(
twj: &'a TableWithJoins,
scope: &mut Scope,
ctes: &CteMap<'a>,
active: &ActiveCtes,
depth: usize,
) -> Result<(), LineageError> {
add_table_factor(&twj.relation, scope, ctes, active, depth)?;
for join in &twj.joins {
add_table_factor(&join.relation, scope, ctes, active, depth)?;
}
Ok(())
}
pub(crate) fn add_table_factor<'a>(
factor: &'a TableFactor,
scope: &mut Scope,
ctes: &CteMap<'a>,
active: &ActiveCtes,
depth: usize,
) -> Result<(), LineageError> {
match factor {
TableFactor::Table { name, alias, .. } => {
let last = last_ident(name);
let visible = alias
.as_ref()
.map_or_else(|| last.to_string(), |a| a.name.value.clone());
let folded = fold(last);
if let Some(&cte_query) = ctes.get(folded.as_str()) {
if active.contains(&folded) {
return Err(LineageError::CircularReference { cte: last.to_string() });
}
let mut active = active.clone();
active.insert(folded);
let cols = resolve_query(cte_query, ctes, &active, depth + 1)?;
scope.push(visible, virtual_from(cols));
} else {
let table = table_ref_from_object_name(name);
scope.push(visible, RelExpose::Physical { table });
}
Ok(())
}
TableFactor::Derived { subquery, alias, .. } => {
let cols = resolve_query(subquery, ctes, active, depth + 1)?;
let visible = alias.as_ref().map_or_else(String::new, |a| a.name.value.clone());
scope.push(visible, virtual_from(cols));
Ok(())
}
TableFactor::NestedJoin { table_with_joins, .. } => {
add_table_with_joins(table_with_joins, scope, ctes, active, depth)
}
_ => Ok(()),
}
}
fn push_column(out: &mut Vec<OutputColumn>, name: Option<String>, sources: BTreeSet<SourceRef>) {
let position = out.len();
out.push(OutputColumn {
name,
position,
sources,
});
}
fn push_expanded(out: &mut Vec<OutputColumn>, expanded: ExpandedColumns) {
for (name, sources) in expanded {
push_column(out, name, sources);
}
}
fn virtual_from(cols: Vec<OutputColumn>) -> RelExpose {
let columns = cols
.into_iter()
.map(|c| VirtCol {
name: c.name,
sources: c.sources,
})
.collect();
RelExpose::Virtual { columns }
}
pub(crate) fn last_ident(name: &ObjectName) -> &str {
name.0
.last()
.and_then(|p| p.as_ident())
.map_or("", |i| i.value.as_str())
}
pub(crate) fn table_ref_from_object_name(name: &ObjectName) -> TableRef {
let idents: Vec<&str> = name
.0
.iter()
.filter_map(|p| p.as_ident())
.map(|i| i.value.as_str())
.collect();
match idents.as_slice() {
[] => TableRef::new(name.to_string()),
[table] => TableRef::new(*table),
[schema, table] => TableRef::qualified(*schema, *table),
[.., database, schema, table] => TableRef::builder(*table)
.with_schema(*schema)
.with_database(*database)
.build(),
}
}
fn output_name(expr: &Expr) -> Option<String> {
match expr {
Expr::Identifier(id) => Some(id.value.clone()),
Expr::CompoundIdentifier(parts) => parts.last().map(|i| i.value.clone()),
_ => None,
}
}
pub(crate) fn collect_sources(
expr: &Expr,
scope: &Scope,
ctes: &CteMap<'_>,
active: &ActiveCtes,
depth: usize,
) -> Result<BTreeSet<SourceRef>, LineageError> {
let mut collector = ExprCollector {
scope,
ctes,
active,
depth,
query_depth: 0,
sources: BTreeSet::new(),
};
match expr.visit(&mut collector) {
ControlFlow::Break(err) => Err(err),
ControlFlow::Continue(()) => Ok(collector.sources),
}
}
struct ExprCollector<'a, 's> {
scope: &'s Scope,
ctes: &'s CteMap<'a>,
active: &'s ActiveCtes,
depth: usize,
query_depth: usize,
sources: BTreeSet<SourceRef>,
}
impl Visitor for ExprCollector<'_, '_> {
type Break = LineageError;
fn pre_visit_query(&mut self, query: &Query) -> ControlFlow<Self::Break> {
if self.query_depth == 0 {
match resolve_query(query, self.ctes, self.active, self.depth + 1) {
Ok(cols) => {
for col in cols {
self.sources.extend(col.sources);
}
}
Err(err) => return ControlFlow::Break(err),
}
}
self.query_depth += 1;
ControlFlow::Continue(())
}
fn post_visit_query(&mut self, _query: &Query) -> ControlFlow<Self::Break> {
self.query_depth = self.query_depth.saturating_sub(1);
ControlFlow::Continue(())
}
fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow<Self::Break> {
if self.query_depth > 0 {
return ControlFlow::Continue(());
}
let resolved = match expr {
Expr::Identifier(id) => Some(self.scope.resolve_column(None, &id.value)),
Expr::CompoundIdentifier(parts) => match parts.as_slice() {
[.., qualifier, col] => Some(self.scope.resolve_column(Some(&qualifier.value), &col.value)),
[col] => Some(self.scope.resolve_column(None, &col.value)),
[] => None,
},
_ => None,
};
match resolved {
Some(Ok(found)) => self.sources.extend(found),
Some(Err(err)) => return ControlFlow::Break(err),
None => {}
}
ControlFlow::Continue(())
}
}