use std::collections::BTreeSet;
use crate::error::LineageError;
use crate::model::{SourceRef, TableRef};
#[derive(Debug, Clone)]
pub(crate) struct VirtCol {
pub name: Option<String>,
pub sources: BTreeSet<SourceRef>,
}
#[derive(Debug, Clone)]
pub(crate) enum RelExpose {
Physical {
table: TableRef,
},
Virtual {
columns: Vec<VirtCol>,
},
}
pub(crate) type ExpandedColumns = Vec<(Option<String>, BTreeSet<SourceRef>)>;
#[derive(Debug, Clone)]
pub(crate) struct Relation {
pub visible: String,
pub expose: RelExpose,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Scope {
relations: Vec<Relation>,
}
impl Scope {
pub(crate) fn push(&mut self, visible: String, expose: RelExpose) {
self.relations.push(Relation { visible, expose });
}
pub(crate) fn physical_tables(&self) -> BTreeSet<TableRef> {
self.relations
.iter()
.filter_map(|r| match &r.expose {
RelExpose::Physical { table } => Some(table.clone()),
RelExpose::Virtual { .. } => None,
})
.collect()
}
pub(crate) fn resolve_column(
&self,
qualifier: Option<&str>,
column: &str,
) -> Result<BTreeSet<SourceRef>, LineageError> {
if let Some(q) = qualifier {
return resolve_in_relation(self.relation_named(q)?, column);
}
if self.relations.len() == 1 {
return resolve_in_relation(&self.relations[0], column);
}
let mut candidates: Vec<BTreeSet<SourceRef>> = Vec::new();
let mut has_physical = false;
for rel in &self.relations {
match &rel.expose {
RelExpose::Physical { .. } => has_physical = true,
RelExpose::Virtual { columns } => {
if let Some(vc) = find_virtual_column(columns, column) {
candidates.push(vc.sources.clone());
}
}
}
}
if has_physical {
return Err(LineageError::UnresolvedColumn {
column: column.to_string(),
reason: "ambiguous unqualified column across multiple tables".to_string(),
});
}
match candidates.len() {
1 => Ok(candidates.into_iter().next().unwrap_or_default()),
0 => Err(LineageError::UnresolvedColumn {
column: column.to_string(),
reason: "column not found in any source relation".to_string(),
}),
_ => Err(LineageError::UnresolvedColumn {
column: column.to_string(),
reason: "ambiguous: column present in multiple source relations".to_string(),
}),
}
}
pub(crate) fn expand_wildcard(&self) -> Result<ExpandedColumns, LineageError> {
let mut out = Vec::new();
for rel in &self.relations {
expand_relation(rel, &mut out)?;
}
Ok(out)
}
pub(crate) fn expand_qualified_wildcard(&self, name: &str) -> Result<ExpandedColumns, LineageError> {
let mut out = Vec::new();
expand_relation(self.relation_named(name)?, &mut out)?;
Ok(out)
}
fn relation_named(&self, name: &str) -> Result<&Relation, LineageError> {
self.relations
.iter()
.find(|r| r.visible.eq_ignore_ascii_case(name))
.ok_or_else(|| LineageError::UnknownTable {
table: name.to_string(),
})
}
}
fn find_virtual_column<'a>(columns: &'a [VirtCol], name: &str) -> Option<&'a VirtCol> {
columns
.iter()
.find(|c| c.name.as_deref().is_some_and(|n| n.eq_ignore_ascii_case(name)))
}
fn resolve_in_relation(rel: &Relation, column: &str) -> Result<BTreeSet<SourceRef>, LineageError> {
match &rel.expose {
RelExpose::Physical { table } => {
let mut s = BTreeSet::new();
s.insert(SourceRef::new(table.clone(), column));
Ok(s)
}
RelExpose::Virtual { columns } => find_virtual_column(columns, column)
.map(|c| c.sources.clone())
.ok_or_else(|| LineageError::UnresolvedColumn {
column: column.to_string(),
reason: format!("not produced by relation `{}`", rel.visible),
}),
}
}
fn expand_relation(rel: &Relation, out: &mut ExpandedColumns) -> Result<(), LineageError> {
match &rel.expose {
RelExpose::Physical { .. } => Err(LineageError::UnresolvedColumn {
column: "*".to_string(),
reason: "wildcard over a base table cannot be expanded (no column list)".to_string(),
}),
RelExpose::Virtual { columns } => {
for vc in columns {
out.push((vc.name.clone(), vc.sources.clone()));
}
Ok(())
}
}
}