use sqlparser::ast::Ident;
use super::logical_plan::{
dml_roots, is_dml_root, output_slots, peel_with, Expr, LogicalPlan, MergeClause, NamedExpr,
Update,
};
use super::origins::{
conflict_value_origins, enter_withs, origins_of_expr, output_operands, TraceContext,
};
use crate::casing::IdentifierCasing;
use crate::extractor::{ColumnLineageEdge, ColumnLineageKind, ColumnTarget, TableLineageEdge};
use crate::reference::{
ColumnRead, ColumnReference, ColumnWrite, ResolutionKind, TableRead, TableReference, TableWrite,
};
pub(super) fn collect_column_lineage(
plan: &LogicalPlan,
casing: IdentifierCasing,
) -> Vec<ColumnLineageEdge> {
let mut context = TraceContext::new(plan, casing);
let mut edges = Vec::new();
let outer = peel_with(plan);
for root in dml_roots(plan) {
dml_relation_lineage(root, std::ptr::eq(root, outer), &mut context, &mut edges);
}
if !is_dml_root(outer) {
query_output_lineage(plan, &mut context, &mut edges);
}
edges
}
fn dml_relation_lineage<'a>(
root: &'a LogicalPlan,
is_outer: bool,
context: &mut TraceContext<'a>,
edges: &mut Vec<ColumnLineageEdge>,
) {
match root {
LogicalPlan::Insert(i) => {
let src = enter_withs(&i.input, context);
if !i.source_wildcard {
relation_lineage(&i.columns, src, context, edges);
}
for a in &i.on_conflict {
emit_edges(
conflict_value_origins(&a.value, &i.columns, src, context),
ColumnTarget::Relation(a.target.clone()),
edges,
);
}
if is_outer {
returning_lineage(&i.returning, &i.input, context, edges);
}
}
LogicalPlan::Update(u) => {
for a in &u.assignments {
emit_edges(
origins_of_expr(&a.value, &u.input, context),
ColumnTarget::Relation(a.target.clone()),
edges,
);
}
if is_outer {
returning_lineage(&u.returning, &u.input, context, edges);
}
}
LogicalPlan::Delete(d) => {
if is_outer {
returning_lineage(&d.returning, &d.input, context, edges);
}
}
LogicalPlan::CreateTableAs(c) => {
let src = enter_withs(&c.input, context);
if pairs_positionally(c.source_wildcard, &c.columns) {
created_relation_lineage(&c.columns, &c.target.reference, src, context, edges);
}
}
LogicalPlan::CreateView(c) => {
let src = enter_withs(&c.input, context);
if pairs_positionally(c.source_wildcard, &c.columns) {
created_relation_lineage(&c.columns, &c.target.reference, src, context, edges);
}
}
LogicalPlan::Merge(m) => {
for clause in &m.clauses {
match clause {
MergeClause::Update { assignments } => {
for a in assignments {
merge_value_edges(&a.target, &a.value, &m.source, context, edges);
}
}
MergeClause::Insert { columns, values } => {
for (column, value) in columns.iter().zip(values) {
merge_value_edges(column, value, &m.source, context, edges);
}
}
MergeClause::Delete => {}
}
}
if is_outer {
returning_lineage(&m.returning, &m.source, context, edges);
}
}
LogicalPlan::Scan(_)
| LogicalPlan::Filter(_)
| LogicalPlan::Join(_)
| LogicalPlan::Aggregate(_)
| LogicalPlan::Projection(_)
| LogicalPlan::Sort(_)
| LogicalPlan::SetOp(_)
| LogicalPlan::SubqueryAlias(_)
| LogicalPlan::TableFunction(_)
| LogicalPlan::With(_)
| LogicalPlan::CteRef(_)
| LogicalPlan::Values(_)
| LogicalPlan::Empty
| LogicalPlan::AlterTable(_)
| LogicalPlan::Drop(_) => {}
}
}
fn query_output_lineage<'a>(
op: &'a LogicalPlan,
context: &mut TraceContext<'a>,
out: &mut Vec<ColumnLineageEdge>,
) {
let operands = output_operands(op);
let result_names: Vec<Option<Ident>> = match operands.first() {
Some(o) => output_slots(o.outputs).map(|(n, _)| n.cloned()).collect(),
None => Vec::new(),
};
for operand in &operands {
let outputs = operand.outputs;
operand.trace(context, |input, cx| {
for (position, (_, expr)) in output_slots(outputs).enumerate() {
let target = ColumnTarget::QueryOutput {
name: result_names.get(position).cloned().flatten(),
position,
};
emit_edges(origins_of_expr(expr, input, cx), target, out);
}
});
}
}
fn emit_edges(
origins: impl IntoIterator<Item = (ColumnRead, ColumnLineageKind)>,
target: ColumnTarget,
out: &mut Vec<ColumnLineageEdge>,
) {
for (source, kind) in origins {
out.push(ColumnLineageEdge {
source,
target: target.clone(),
kind,
});
}
}
fn relation_lineage<'a>(
columns: &[ColumnWrite],
input: &'a LogicalPlan,
context: &mut TraceContext<'a>,
out: &mut Vec<ColumnLineageEdge>,
) {
if let LogicalPlan::Values(values) = input {
for row in &values.rows {
for (target_column, expr) in columns.iter().zip(row) {
emit_edges(
origins_of_expr(expr, input, context),
ColumnTarget::Relation(target_column.clone()),
out,
);
}
}
return;
}
for operand in output_operands(input) {
let outputs = operand.outputs;
operand.trace(context, |src_input, cx| {
for (target_column, (_, expr)) in columns.iter().zip(output_slots(outputs)) {
let tgt = ColumnTarget::Relation(target_column.clone());
emit_edges(origins_of_expr(expr, src_input, cx), tgt, out);
}
});
}
}
fn pairs_positionally(source_wildcard: bool, explicit: &[Ident]) -> bool {
!source_wildcard || explicit.is_empty()
}
fn created_relation_lineage<'a>(
explicit: &[Ident],
target: &TableReference,
input: &'a LogicalPlan,
context: &mut TraceContext<'a>,
out: &mut Vec<ColumnLineageEdge>,
) {
let operands = output_operands(input);
let result_names: Vec<Option<Ident>> = if explicit.is_empty() {
match operands.first() {
Some(o) => output_slots(o.outputs).map(|(n, _)| n.cloned()).collect(),
None => Vec::new(),
}
} else {
explicit.iter().cloned().map(Some).collect()
};
for operand in &operands {
let outputs = operand.outputs;
operand.trace(context, |src_input, cx| {
for (position, (_, expr)) in output_slots(outputs).enumerate() {
let Some(name) = result_names.get(position).cloned().flatten() else {
continue;
};
let tgt = ColumnTarget::Relation(ColumnWrite {
reference: ColumnReference {
table: Some(target.clone()),
name,
},
resolution: ResolutionKind::Inferred,
});
emit_edges(origins_of_expr(expr, src_input, cx), tgt, out);
}
});
}
}
fn returning_lineage<'a>(
returning: &'a [NamedExpr],
input: &'a LogicalPlan,
context: &mut TraceContext<'a>,
out: &mut Vec<ColumnLineageEdge>,
) {
for (position, (name, expr)) in output_slots(returning).enumerate() {
let target = ColumnTarget::QueryOutput {
name: name.cloned(),
position,
};
emit_edges(origins_of_expr(expr, input, context), target, out);
}
}
fn merge_value_edges<'a>(
target: &ColumnWrite,
value: &'a Expr,
source: &'a LogicalPlan,
context: &mut TraceContext<'a>,
out: &mut Vec<ColumnLineageEdge>,
) {
let tgt = ColumnTarget::Relation(target.clone());
emit_edges(origins_of_expr(value, source, context), tgt, out);
}
pub(super) fn collect_table_lineage(
plan: &LogicalPlan,
casing: IdentifierCasing,
) -> Vec<TableLineageEdge> {
let mut context = TraceContext::new(plan, casing);
dml_roots(plan)
.into_iter()
.flat_map(|root| dml_table_lineage(root, &mut context))
.collect()
}
fn dml_table_lineage<'a>(
root: &'a LogicalPlan,
context: &mut TraceContext<'a>,
) -> Vec<TableLineageEdge> {
let mut sources = Vec::new();
let mut fed_ctes: Vec<usize> = Vec::new();
let target = match root {
LogicalPlan::Insert(i) => {
feeding_scans(&i.input, context, &mut fed_ctes, &mut sources);
for a in &i.on_conflict {
expr_feeding(&a.value, context, &mut fed_ctes, &mut sources);
}
&i.target
}
LogicalPlan::Update(u) => return update_table_lineage(u, context),
LogicalPlan::CreateTableAs(c) => {
feeding_scans(&c.input, context, &mut fed_ctes, &mut sources);
if let Some(schema_source) = &c.schema_source {
if schema_source.copies_data {
sources.push(schema_source.source.clone());
}
}
&c.target
}
LogicalPlan::CreateView(c) => {
feeding_scans(&c.input, context, &mut fed_ctes, &mut sources);
&c.target
}
LogicalPlan::Merge(m) => {
let writes_data = m
.clauses
.iter()
.any(|c| matches!(c, MergeClause::Insert { .. } | MergeClause::Update { .. }));
if writes_data {
feeding_scans(&m.source, context, &mut fed_ctes, &mut sources);
}
for clause in &m.clauses {
match clause {
MergeClause::Update { assignments } => {
for a in assignments {
expr_feeding(&a.value, context, &mut fed_ctes, &mut sources);
}
}
MergeClause::Insert { columns, values } => {
for (_col, value) in columns.iter().zip(values) {
expr_feeding(value, context, &mut fed_ctes, &mut sources);
}
}
MergeClause::Delete => {}
}
}
&m.target
}
LogicalPlan::Scan(_)
| LogicalPlan::Filter(_)
| LogicalPlan::Join(_)
| LogicalPlan::Aggregate(_)
| LogicalPlan::Projection(_)
| LogicalPlan::Sort(_)
| LogicalPlan::SetOp(_)
| LogicalPlan::SubqueryAlias(_)
| LogicalPlan::TableFunction(_)
| LogicalPlan::With(_)
| LogicalPlan::CteRef(_)
| LogicalPlan::Values(_)
| LogicalPlan::Empty
| LogicalPlan::Delete(_)
| LogicalPlan::AlterTable(_)
| LogicalPlan::Drop(_) => return Vec::new(),
};
sources
.into_iter()
.map(|source| TableLineageEdge {
source,
target: target.clone(),
})
.collect()
}
fn update_table_lineage<'a>(
u: &'a Update,
context: &mut TraceContext<'a>,
) -> Vec<TableLineageEdge> {
let mut edges: Vec<TableLineageEdge> = Vec::new();
for a in &u.assignments {
let Some(table) = &a.target.reference.table else {
continue;
};
let target = TableWrite {
reference: table.clone(),
resolution: a.target_resolution,
};
for (read, _kind) in origins_of_expr(&a.value, &u.input, context) {
let Some(source) = read.reference.table else {
continue;
};
if edges
.iter()
.any(|e| e.source.reference == source && e.target == target)
{
continue; }
edges.push(TableLineageEdge {
source: TableRead {
reference: source,
resolution: read.resolution,
},
target: target.clone(),
});
}
}
edges
}
fn feeding_scans<'a>(
op: &'a LogicalPlan,
context: &mut TraceContext<'a>,
fed_ctes: &mut Vec<usize>,
out: &mut Vec<TableRead>,
) {
match op {
LogicalPlan::Scan(s) => out.push(TableRead {
reference: s.table.clone(),
resolution: s.resolution,
}),
LogicalPlan::Filter(f) => feeding_scans(&f.input, context, fed_ctes, out),
LogicalPlan::Join(j) => {
feeding_scans(&j.left, context, fed_ctes, out);
feeding_scans(&j.right, context, fed_ctes, out);
}
LogicalPlan::Projection(p) => {
feeding_scans(&p.input, context, fed_ctes, out);
for ne in &p.exprs {
expr_feeding(&ne.expr, context, fed_ctes, out);
}
}
LogicalPlan::Aggregate(a) => feeding_scans(&a.input, context, fed_ctes, out),
LogicalPlan::Sort(s) => feeding_scans(&s.input, context, fed_ctes, out),
LogicalPlan::SubqueryAlias(sa) => feeding_scans(&sa.input, context, fed_ctes, out),
LogicalPlan::TableFunction(tf) => feeding_scans(&tf.input, context, fed_ctes, out),
LogicalPlan::SetOp(so) => {
feeding_scans(&so.left, context, fed_ctes, out);
feeding_scans(&so.right, context, fed_ctes, out);
}
LogicalPlan::With(w) => {
context.with_decls(&w.ctes, |context| {
feeding_scans(&w.body, context, fed_ctes, out)
});
}
LogicalPlan::CteRef(r) => {
context.enter_cte(&r.name, |context, body| {
let id = body as *const LogicalPlan as usize;
if fed_ctes.contains(&id) {
return;
}
fed_ctes.push(id);
feeding_scans(body, context, fed_ctes, out)
});
}
LogicalPlan::Values(v) => {
for expr in v.rows.iter().flatten() {
expr_feeding(expr, context, fed_ctes, out);
}
}
LogicalPlan::Insert(i) => feeding_scans(&i.input, context, fed_ctes, out),
LogicalPlan::Update(u) => feeding_scans(&u.input, context, fed_ctes, out),
LogicalPlan::CreateTableAs(c) => feeding_scans(&c.input, context, fed_ctes, out),
LogicalPlan::CreateView(c) => feeding_scans(&c.input, context, fed_ctes, out),
LogicalPlan::Merge(m) => feeding_scans(&m.source, context, fed_ctes, out),
LogicalPlan::Delete(_)
| LogicalPlan::Drop(_)
| LogicalPlan::AlterTable(_)
| LogicalPlan::Empty => {}
}
}
fn expr_feeding<'a>(
expr: &'a Expr,
context: &mut TraceContext<'a>,
fed_ctes: &mut Vec<usize>,
out: &mut Vec<TableRead>,
) {
for sub in expr.value_subplans() {
feeding_scans(sub, context, fed_ctes, out);
}
for child in expr.value_operands() {
expr_feeding(child, context, fed_ctes, out);
}
}
#[cfg(test)]
mod tests {
use super::super::binder::build_with_diagnostics;
use super::super::{column_lineage, reads, table_reads};
use super::*;
use crate::casing::{canonical_quote, IdentifierCasing, IdentifierStyle};
use sqlparser::dialect::GenericDialect;
use sqlparser::parser::Parser;
fn plan(sql: &str) -> LogicalPlan {
let statements = Parser::parse_sql(&GenericDialect {}, sql).unwrap();
let style = IdentifierStyle {
casing: IdentifierCasing::for_dialect(&GenericDialect {}),
quote: canonical_quote(&GenericDialect {}),
};
build_with_diagnostics(&statements[0], None, style).0
}
fn read_names(plan: &LogicalPlan) -> Vec<String> {
let mut v: Vec<String> = reads(plan)
.iter()
.map(|r| match &r.reference.table {
Some(t) => format!("{}.{}", t.name.value, r.reference.name.value),
None => format!("?.{}", r.reference.name.value),
})
.collect();
v.sort();
v
}
fn lineage_strs(plan: &LogicalPlan) -> Vec<String> {
let casing = IdentifierCasing::for_dialect(&GenericDialect {});
let mut v: Vec<String> = column_lineage(plan, casing)
.iter()
.map(|e| {
let src = e
.source
.reference
.table
.as_ref()
.map_or("?".to_string(), |t| t.name.value.clone());
let tgt = match &e.target {
ColumnTarget::QueryOutput { name, .. } => {
name.as_ref().map_or("?".to_string(), |n| n.value.clone())
}
ColumnTarget::Relation(r) => r.reference.name.value.clone(),
};
let k = match e.kind {
ColumnLineageKind::Passthrough => "=",
ColumnLineageKind::Transformation => "~",
};
format!("{}.{} {}> {}", src, e.source.reference.name.value, k, tgt)
})
.collect();
v.sort();
v
}
#[test]
fn reads_occurrence_based_across_clauses() {
assert_eq!(
read_names(&plan("SELECT a FROM t WHERE a > 0")),
vec!["t.a", "t.a"]
);
}
#[test]
fn passthrough_vs_transformation_lineage() {
assert_eq!(
lineage_strs(&plan("SELECT a, b + c AS s FROM t")),
vec!["t.a => a", "t.b ~> s", "t.c ~> s"]
);
}
#[test]
fn join_reads_both_sides_predicate_not_an_origin() {
let op = plan("SELECT t1.x FROM t1 JOIN t2 ON t1.id = t2.id");
assert_eq!(read_names(&op), vec!["t1.id", "t1.x", "t2.id"]);
assert_eq!(lineage_strs(&op), vec!["t1.x => x"]);
}
#[test]
fn table_reads_one_per_scan() {
let op = plan("SELECT t1.x FROM t1 JOIN t2 ON t1.id = t2.id");
let mut names: Vec<String> = table_reads(&op)
.iter()
.map(|r| r.reference.name.value.clone())
.collect();
names.sort();
assert_eq!(names, vec!["t1", "t2"]);
}
}