1use std::collections::{BTreeMap, BTreeSet};
21
22use datafusion::common::tree_node::TreeNode;
23use datafusion::logical_expr::utils::grouping_set_to_exprlist;
24use datafusion::logical_expr::{
25 CreateMemoryTable, CreateView, DdlStatement, Distinct, Expr, JoinType, LogicalPlan, WriteOp,
26};
27
28use crate::config::OpenLineageConfig;
29use crate::extract::dataset_for;
30use crate::naming::DatasetName;
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
34pub struct SourceColumn {
35 pub dataset: DatasetName,
37 pub column: String,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
44pub enum DirectKind {
45 Identity,
47 Transformation,
49 Aggregation,
51}
52
53impl DirectKind {
54 pub fn subtype(self) -> &'static str {
56 match self {
57 DirectKind::Identity => "IDENTITY",
58 DirectKind::Transformation => "TRANSFORMATION",
59 DirectKind::Aggregation => "AGGREGATION",
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
66pub enum IndirectKind {
67 Filter,
69 Join,
71 GroupBy,
73 Sort,
75 Window,
77}
78
79impl IndirectKind {
80 pub fn subtype(self) -> &'static str {
82 match self {
83 IndirectKind::Filter => "FILTER",
84 IndirectKind::Join => "JOIN",
85 IndirectKind::GroupBy => "GROUP_BY",
86 IndirectKind::Sort => "SORT",
87 IndirectKind::Window => "WINDOW",
88 }
89 }
90}
91
92#[derive(Debug, Clone, Default, PartialEq, Eq)]
94pub struct ColumnSources {
95 pub direct: BTreeMap<SourceColumn, DirectKind>,
97}
98
99impl ColumnSources {
100 fn single(source: SourceColumn, kind: DirectKind) -> Self {
101 Self {
102 direct: BTreeMap::from([(source, kind)]),
103 }
104 }
105
106 fn merge(&mut self, other: &ColumnSources) {
107 for (source, kind) in &other.direct {
108 self.direct
109 .entry(source.clone())
110 .and_modify(|k| *k = (*k).max(*kind))
111 .or_insert(*kind);
112 }
113 }
114
115 fn upgraded(mut self, min: DirectKind) -> Self {
119 for kind in self.direct.values_mut() {
120 *kind = (*kind).max(min);
121 }
122 self
123 }
124}
125
126pub type IndirectSources = BTreeMap<SourceColumn, BTreeSet<IndirectKind>>;
128
129#[derive(Debug, Clone, Default)]
134pub(crate) struct NodeLineage {
135 pub columns: Vec<ColumnSources>,
138 pub indirect: IndirectSources,
141}
142
143#[derive(Debug, Clone, Default)]
149pub struct ResolvedColumns {
150 pub fields: BTreeMap<String, ColumnSources>,
152 pub indirect: IndirectSources,
154}
155
156pub(crate) fn resolve_output_columns(
158 plan: &LogicalPlan,
159 config: &OpenLineageConfig,
160) -> Option<ResolvedColumns> {
161 match plan {
162 LogicalPlan::Dml(dml) => match dml.op {
163 WriteOp::Insert(_) | WriteOp::Ctas | WriteOp::Update => {
168 let resolved = resolve(&dml.input, config)?;
169 let target = dml.target.schema();
170 if resolved.columns.len() != target.fields().len() {
171 return degrade(plan, "DML input not aligned with target schema");
172 }
173 Some(keyed_by(resolved, |i| target.field(i).name().clone()))
174 }
175 WriteOp::Delete | WriteOp::Truncate => None,
177 },
178 LogicalPlan::Ddl(
182 DdlStatement::CreateMemoryTable(CreateMemoryTable { input, .. })
183 | DdlStatement::CreateView(CreateView { input, .. }),
184 ) => {
185 let resolved = resolve(input, config)?;
186 let schema = input.schema().clone();
187 if resolved.columns.len() != schema.fields().len() {
188 return degrade(plan, "CTAS/view input arity mismatch");
189 }
190 Some(keyed_by(resolved, |i| schema.field(i).name().clone()))
191 }
192 _ => None,
195 }
196}
197
198fn keyed_by(node: NodeLineage, name: impl Fn(usize) -> String) -> ResolvedColumns {
199 let fields = node
200 .columns
201 .into_iter()
202 .enumerate()
203 .filter(|(_, sources)| !sources.direct.is_empty())
206 .map(|(i, sources)| (name(i), sources))
207 .collect();
208 ResolvedColumns {
209 fields,
210 indirect: node.indirect,
211 }
212}
213
214fn peel_aliases(expr: &Expr) -> &Expr {
219 let mut inner = expr;
220 while let Expr::Alias(alias) = inner {
221 inner = &alias.expr;
222 }
223 inner
224}
225
226fn degrade<T>(plan: &LogicalPlan, reason: &str) -> Option<T> {
227 tracing::debug!(
228 target: "openlineage",
229 node = %plan.display(),
230 reason,
231 "column lineage degraded"
232 );
233 None
234}
235
236fn resolve(plan: &LogicalPlan, config: &OpenLineageConfig) -> Option<NodeLineage> {
238 let node = match plan {
239 LogicalPlan::TableScan(scan) => {
240 if scan
243 .table_name
244 .schema()
245 .is_some_and(|s| s.eq_ignore_ascii_case("information_schema"))
246 {
247 return degrade(plan, "information_schema scan");
248 }
249 let dataset = dataset_for(&scan.table_name, config);
250 let source_schema = scan.source.schema();
251 let columns = (0..scan.projected_schema.fields().len())
254 .map(|i| {
255 let src = scan.projection.as_ref().map_or(i, |p| p[i]);
256 ColumnSources::single(
257 SourceColumn {
258 dataset: dataset.clone(),
259 column: source_schema.field(src).name().clone(),
260 },
261 DirectKind::Identity,
262 )
263 })
264 .collect();
265 let mut indirect = IndirectSources::new();
268 for filter in &scan.filters {
269 if has_hidden_sources(filter) {
270 return degrade(plan, "subquery in pushed-down filter");
271 }
272 for col in filter.column_refs() {
273 if source_schema.field_with_name(&col.name).is_err() {
274 return degrade(plan, "pushed-down filter column not in source schema");
275 }
276 indirect
277 .entry(SourceColumn {
278 dataset: dataset.clone(),
279 column: col.name.clone(),
280 })
281 .or_default()
282 .insert(IndirectKind::Filter);
283 }
284 }
285 NodeLineage { columns, indirect }
286 }
287
288 LogicalPlan::Projection(proj) => {
289 let child = resolve(&proj.input, config)?;
290 let columns = proj
291 .expr
292 .iter()
293 .map(|expr| expr_sources(expr, &proj.input, &child, DirectKind::Identity))
294 .collect::<Option<_>>()?;
295 NodeLineage {
296 columns,
297 indirect: child.indirect,
298 }
299 }
300
301 LogicalPlan::SubqueryAlias(alias) => resolve(&alias.input, config)?,
305
306 LogicalPlan::Filter(filter) => {
307 let mut child = resolve(&filter.input, config)?;
308 let sources = referenced_sources(&filter.predicate, &filter.input, &child.columns)?;
309 add_indirect(&mut child.indirect, sources, IndirectKind::Filter);
310 child
311 }
312
313 LogicalPlan::Aggregate(agg) => {
314 let child = resolve(&agg.input, config)?;
315 let group_list = grouping_set_to_exprlist(&agg.group_expr).ok()?;
316 let mut columns = Vec::with_capacity(agg.schema.fields().len());
317 let mut indirect = child.indirect.clone();
318 for expr in &group_list {
319 let sources = expr_sources(expr, &agg.input, &child, DirectKind::Identity)?;
320 add_indirect(
322 &mut indirect,
323 sources.direct.keys().cloned().collect(),
324 IndirectKind::GroupBy,
325 );
326 columns.push(sources);
327 }
328 if matches!(agg.group_expr.as_slice(), [Expr::GroupingSet(_)]) {
331 columns.push(ColumnSources::default());
332 }
333 for expr in &agg.aggr_expr {
334 columns.push(expr_sources(
335 expr,
336 &agg.input,
337 &child,
338 DirectKind::Aggregation,
339 )?);
340 }
341 NodeLineage { columns, indirect }
342 }
343
344 LogicalPlan::Join(join) => {
345 let left = resolve(&join.left, config)?;
346 let right = resolve(&join.right, config)?;
347 let mut indirect = left.indirect.clone();
348 for (source, kinds) in &right.indirect {
349 indirect.entry(source.clone()).or_default().extend(kinds);
350 }
351 for (l, r) in &join.on {
352 add_indirect(
353 &mut indirect,
354 referenced_sources(l, &join.left, &left.columns)?,
355 IndirectKind::Join,
356 );
357 add_indirect(
358 &mut indirect,
359 referenced_sources(r, &join.right, &right.columns)?,
360 IndirectKind::Join,
361 );
362 }
363 if let Some(filter) = &join.filter {
364 if has_hidden_sources(filter) {
366 return degrade(plan, "subquery in join filter");
367 }
368 for col in filter.column_refs() {
369 let sources = if let Some(i) = join.left.schema().maybe_index_of_column(col) {
370 &left.columns[i]
371 } else if let Some(i) = join.right.schema().maybe_index_of_column(col) {
372 &right.columns[i]
373 } else {
374 return degrade(plan, "join filter column not in either side");
375 };
376 add_indirect(
377 &mut indirect,
378 sources.direct.keys().cloned().collect(),
379 IndirectKind::Join,
380 );
381 }
382 }
383 let mark = std::iter::once(ColumnSources::default());
386 let columns: Vec<ColumnSources> = match join.join_type {
387 JoinType::Inner | JoinType::Left | JoinType::Right | JoinType::Full => {
388 left.columns.into_iter().chain(right.columns).collect()
389 }
390 JoinType::LeftSemi | JoinType::LeftAnti => left.columns,
391 JoinType::RightSemi | JoinType::RightAnti => right.columns,
392 JoinType::LeftMark => left.columns.into_iter().chain(mark).collect(),
393 JoinType::RightMark => right.columns.into_iter().chain(mark).collect(),
394 };
395 NodeLineage { columns, indirect }
396 }
397
398 LogicalPlan::Union(union) => {
399 let mut columns = vec![ColumnSources::default(); union.schema.fields().len()];
400 let mut indirect = IndirectSources::new();
401 for input in &union.inputs {
402 let child = resolve(input, config)?;
403 if child.columns.len() != columns.len() {
404 return degrade(plan, "union input arity mismatch");
405 }
406 for (merged, sources) in columns.iter_mut().zip(&child.columns) {
407 merged.merge(sources);
408 }
409 for (source, kinds) in &child.indirect {
410 indirect.entry(source.clone()).or_default().extend(kinds);
411 }
412 }
413 NodeLineage { columns, indirect }
414 }
415
416 LogicalPlan::Sort(sort) => {
417 let mut child = resolve(&sort.input, config)?;
418 for sort_expr in &sort.expr {
419 let sources = referenced_sources(&sort_expr.expr, &sort.input, &child.columns)?;
420 add_indirect(&mut child.indirect, sources, IndirectKind::Sort);
421 }
422 child
423 }
424
425 LogicalPlan::Limit(limit) => resolve(&limit.input, config)?,
428 LogicalPlan::Repartition(repartition) => resolve(&repartition.input, config)?,
429
430 LogicalPlan::Distinct(Distinct::All(input)) => resolve(input, config)?,
431 LogicalPlan::Distinct(Distinct::On(distinct)) => {
432 let child = resolve(&distinct.input, config)?;
433 let mut indirect = child.indirect.clone();
434 for expr in &distinct.on_expr {
435 let sources = referenced_sources(expr, &distinct.input, &child.columns)?;
436 add_indirect(&mut indirect, sources, IndirectKind::GroupBy);
437 }
438 for sort_expr in distinct.sort_expr.iter().flatten() {
439 let sources = referenced_sources(&sort_expr.expr, &distinct.input, &child.columns)?;
440 add_indirect(&mut indirect, sources, IndirectKind::Sort);
441 }
442 let columns = distinct
443 .select_expr
444 .iter()
445 .map(|expr| expr_sources(expr, &distinct.input, &child, DirectKind::Identity))
446 .collect::<Option<_>>()?;
447 NodeLineage { columns, indirect }
448 }
449
450 LogicalPlan::Window(window) => {
451 let child = resolve(&window.input, config)?;
452 let mut columns = child.columns.clone();
453 let mut indirect = child.indirect.clone();
454 for expr in &window.window_expr {
455 let inner = peel_aliases(expr);
456 if let Expr::WindowFunction(wf) = inner {
457 let mut sources = ColumnSources::default();
458 for arg in &wf.params.args {
459 sources.merge(&expr_sources(
460 arg,
461 &window.input,
462 &child,
463 DirectKind::Aggregation,
464 )?);
465 }
466 for key in wf
467 .params
468 .partition_by
469 .iter()
470 .chain(wf.params.order_by.iter().map(|s| &s.expr))
471 {
472 let refs = referenced_sources(key, &window.input, &child.columns)?;
473 add_indirect(&mut indirect, refs, IndirectKind::Window);
474 }
475 columns.push(sources);
476 } else {
477 columns.push(expr_sources(
480 inner,
481 &window.input,
482 &child,
483 DirectKind::Transformation,
484 )?);
485 }
486 }
487 NodeLineage { columns, indirect }
488 }
489
490 LogicalPlan::Unnest(unnest) => {
491 let child = resolve(&unnest.input, config)?;
492 let unnested: BTreeSet<usize> = unnest
493 .list_type_columns
494 .iter()
495 .map(|(i, _)| *i)
496 .chain(unnest.struct_type_columns.iter().copied())
497 .collect();
498 let columns = unnest
499 .dependency_indices
500 .iter()
501 .map(|&dep| {
502 let sources = child.columns.get(dep)?.clone();
503 Some(if unnested.contains(&dep) {
504 sources.upgraded(DirectKind::Transformation)
505 } else {
506 sources
507 })
508 })
509 .collect::<Option<_>>()
510 .or_else(|| degrade(plan, "unnest dependency index out of range"))?;
511 NodeLineage {
512 columns,
513 indirect: child.indirect,
514 }
515 }
516
517 LogicalPlan::Values(values) => NodeLineage {
519 columns: vec![ColumnSources::default(); values.schema.fields().len()],
520 indirect: IndirectSources::new(),
521 },
522 LogicalPlan::EmptyRelation(empty) => NodeLineage {
523 columns: vec![ColumnSources::default(); empty.schema.fields().len()],
524 indirect: IndirectSources::new(),
525 },
526
527 other => return degrade(other, "no column lineage resolution for plan node"),
532 };
533
534 if node.columns.len() != plan.schema().fields().len() {
537 return degrade(plan, "resolved arity does not match node schema");
538 }
539 Some(node)
540}
541
542fn expr_sources(
549 expr: &Expr,
550 child_plan: &LogicalPlan,
551 child: &NodeLineage,
552 min: DirectKind,
553) -> Option<ColumnSources> {
554 if has_hidden_sources(expr) {
555 return degrade(child_plan, "subquery in expression");
556 }
557 let inner = peel_aliases(expr);
558 if let Expr::Column(col) = inner {
559 let i = child_plan.schema().maybe_index_of_column(col)?;
560 return Some(child.columns[i].clone().upgraded(min));
561 }
562 let mut sources = ColumnSources::default();
563 for col in inner.column_refs() {
564 let i = child_plan.schema().maybe_index_of_column(col)?;
565 sources.merge(&child.columns[i]);
566 }
567 Some(sources.upgraded(min.max(DirectKind::Transformation)))
568}
569
570fn referenced_sources(
574 expr: &Expr,
575 child_plan: &LogicalPlan,
576 columns: &[ColumnSources],
577) -> Option<Vec<SourceColumn>> {
578 if has_hidden_sources(expr) {
579 return degrade(child_plan, "subquery in predicate/key expression");
580 }
581 let mut out = Vec::new();
582 for col in expr.column_refs() {
583 let i = child_plan.schema().maybe_index_of_column(col)?;
584 out.extend(columns[i].direct.keys().cloned());
585 }
586 Some(out)
587}
588
589fn add_indirect(indirect: &mut IndirectSources, sources: Vec<SourceColumn>, kind: IndirectKind) {
590 for source in sources {
591 indirect.entry(source).or_default().insert(kind);
592 }
593}
594
595fn has_hidden_sources(expr: &Expr) -> bool {
598 expr.exists(|e| {
599 Ok(matches!(
600 e,
601 Expr::Exists(_)
602 | Expr::InSubquery(_)
603 | Expr::ScalarSubquery(_)
604 | Expr::OuterReferenceColumn(_, _)
605 ))
606 })
607 .unwrap_or(true)
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613
614 fn source(name: &str) -> SourceColumn {
615 SourceColumn {
616 dataset: DatasetName {
617 namespace: "ns".into(),
618 name: "t".into(),
619 },
620 column: name.into(),
621 }
622 }
623
624 #[test]
625 fn merge_keeps_strongest_kind() {
626 let mut a = ColumnSources::single(source("a"), DirectKind::Identity);
627 let b = ColumnSources::single(source("a"), DirectKind::Aggregation);
628 a.merge(&b);
629 assert_eq!(a.direct[&source("a")], DirectKind::Aggregation);
630 let c = ColumnSources::single(source("a"), DirectKind::Identity);
632 a.merge(&c);
633 assert_eq!(a.direct[&source("a")], DirectKind::Aggregation);
634 }
635
636 #[test]
637 fn upgrade_is_a_floor_not_an_override() {
638 let s = ColumnSources::single(source("a"), DirectKind::Aggregation)
639 .upgraded(DirectKind::Transformation);
640 assert_eq!(s.direct[&source("a")], DirectKind::Aggregation);
641 let s = ColumnSources::single(source("a"), DirectKind::Identity)
642 .upgraded(DirectKind::Transformation);
643 assert_eq!(s.direct[&source("a")], DirectKind::Transformation);
644 }
645}