use datafusion::common::tree_node::{TreeNode, TreeNodeRecursion, TreeNodeVisitor};
use datafusion::error::Result;
use datafusion::logical_expr::dml::InsertOp;
use datafusion::logical_expr::{DdlStatement, LogicalPlan, WriteOp};
use datafusion::sql::TableReference;
use crate::column::{ResolvedColumns, resolve_output_columns};
use crate::config::OpenLineageConfig;
use crate::facets::{
BaseFacet, ColumnLineageDatasetFacet, DataSourceDatasetFacet, DatasetFacets, FieldLineage,
InputField, LifecycleStateChangeDatasetFacet, SchemaDatasetFacet, SchemaField, Transformation,
TransformationType,
};
use crate::naming::DatasetName;
const SCHEMA_FACET: &str = "1-2-0/SchemaDatasetFacet.json";
const COLUMN_LINEAGE_FACET: &str = "1-2-0/ColumnLineageDatasetFacet.json";
const DATA_SOURCE_FACET: &str = "1-0-1/DatasourceDatasetFacet.json";
const LIFECYCLE_FACET: &str = "1-0-1/LifecycleStateChangeDatasetFacet.json";
#[derive(Debug, Default)]
pub struct QueryLineage {
pub inputs: Vec<InputTable>,
pub outputs: Vec<OutputTable>,
pub sql: Option<String>,
}
#[derive(Debug)]
pub struct InputTable {
pub name: DatasetName,
pub fields: Vec<SchemaField>,
}
#[derive(Debug)]
pub struct OutputTable {
pub name: DatasetName,
pub fields: Vec<SchemaField>,
pub column_lineage: Option<ResolvedColumns>,
pub lifecycle: Option<&'static str>,
}
pub fn extract(plan: &LogicalPlan, config: &OpenLineageConfig) -> QueryLineage {
let mut visitor = LineageVisitor {
config,
inputs: Vec::new(),
outputs: Vec::new(),
};
let _ = plan.visit(&mut visitor);
let mut outputs = visitor.outputs;
if !outputs.is_empty()
&& let Some(resolved) = resolve_output_columns(plan, config)
{
for output in &mut outputs {
output.column_lineage = Some(resolved.clone());
}
}
QueryLineage {
inputs: visitor.inputs,
outputs,
sql: None,
}
}
pub(crate) fn dataset_for(table_ref: &TableReference, config: &OpenLineageConfig) -> DatasetName {
DatasetName::from_table_ref(&config.job_namespace, &table_ref.to_string())
}
fn lifecycle_for(op: &WriteOp) -> Option<&'static str> {
match op {
WriteOp::Ctas => Some("CREATE"),
WriteOp::Insert(InsertOp::Overwrite | InsertOp::Replace) => Some("OVERWRITE"),
WriteOp::Insert(InsertOp::Append) | WriteOp::Update | WriteOp::Delete => None,
WriteOp::Truncate => Some("TRUNCATE"),
}
}
fn data_source_facet(name: &DatasetName, config: &OpenLineageConfig) -> DataSourceDatasetFacet {
DataSourceDatasetFacet {
base: BaseFacet::new(&config.producer, DATA_SOURCE_FACET),
name: name.name.clone(),
uri: format!("datafusion:{}/{}", name.namespace, name.name),
}
}
pub(crate) fn schema_fields(fields: &datafusion::arrow::datatypes::Fields) -> Vec<SchemaField> {
fields
.iter()
.map(|f| SchemaField {
name: f.name().to_string(),
type_: f.data_type().to_string(),
description: None,
})
.collect()
}
struct LineageVisitor<'a> {
config: &'a OpenLineageConfig,
inputs: Vec<InputTable>,
outputs: Vec<OutputTable>,
}
impl LineageVisitor<'_> {
fn dataset_for(&self, table_ref: &TableReference) -> DatasetName {
dataset_for(table_ref, self.config)
}
}
impl TreeNodeVisitor<'_> for LineageVisitor<'_> {
type Node = LogicalPlan;
fn f_down(&mut self, node: &Self::Node) -> Result<TreeNodeRecursion> {
match node {
LogicalPlan::TableScan(scan)
if scan
.table_name
.schema()
.is_some_and(|s| s.eq_ignore_ascii_case("information_schema")) => {}
LogicalPlan::TableScan(scan) => {
let dataset = self.dataset_for(&scan.table_name);
let fields = schema_fields(scan.source.schema().fields());
if !self.inputs.iter().any(|i| i.name == dataset) {
self.inputs.push(InputTable {
name: dataset,
fields,
});
}
}
LogicalPlan::Dml(dml) => match dml.op {
WriteOp::Insert(_) | WriteOp::Update | WriteOp::Delete | WriteOp::Ctas => {
self.outputs.push(OutputTable {
name: self.dataset_for(&dml.table_name),
fields: schema_fields(dml.target.schema().fields()),
column_lineage: None,
lifecycle: lifecycle_for(&dml.op),
});
}
WriteOp::Truncate => {}
},
LogicalPlan::Ddl(ddl) => match ddl {
DdlStatement::CreateExternalTable(cmd) => {
self.outputs.push(OutputTable {
name: self.dataset_for(&cmd.name),
fields: schema_fields(cmd.schema.as_arrow().fields()),
column_lineage: None,
lifecycle: Some("CREATE"),
});
}
DdlStatement::CreateMemoryTable(cmd) => {
self.outputs.push(OutputTable {
name: self.dataset_for(&cmd.name),
fields: schema_fields(cmd.input.schema().as_arrow().fields()),
column_lineage: None,
lifecycle: Some("CREATE"),
});
}
DdlStatement::CreateView(cmd) => {
self.outputs.push(OutputTable {
name: self.dataset_for(&cmd.name),
fields: schema_fields(cmd.input.schema().as_arrow().fields()),
column_lineage: None,
lifecycle: Some(if cmd.or_replace {
"OVERWRITE"
} else {
"CREATE"
}),
});
}
_ => {}
},
other => {
tracing::trace!(
target: "openlineage",
node = other.display().to_string(),
"no lineage extraction for plan node"
);
}
}
Ok(TreeNodeRecursion::Continue)
}
}
pub(crate) fn input_dataset_facets(
input: &InputTable,
config: &OpenLineageConfig,
) -> DatasetFacets {
let schema = SchemaDatasetFacet {
base: BaseFacet::new(&config.producer, SCHEMA_FACET),
fields: input.fields.clone(),
};
DatasetFacets {
schema: Some(schema),
data_source: Some(data_source_facet(&input.name, config)),
..Default::default()
}
}
pub(crate) fn output_dataset_facets(
output: &OutputTable,
config: &OpenLineageConfig,
) -> DatasetFacets {
let schema = (!output.fields.is_empty()).then(|| SchemaDatasetFacet {
base: BaseFacet::new(&config.producer, SCHEMA_FACET),
fields: output.fields.clone(),
});
let data_source = Some(data_source_facet(&output.name, config));
let lifecycle_state_change = output
.lifecycle
.map(|state| LifecycleStateChangeDatasetFacet {
base: BaseFacet::new(&config.producer, LIFECYCLE_FACET),
lifecycle_state_change: state.to_string(),
});
let resolved = match &output.column_lineage {
Some(resolved) if !resolved.fields.is_empty() => resolved,
_ => {
return DatasetFacets {
schema,
data_source,
lifecycle_state_change,
..Default::default()
};
}
};
let direct = |subtype: &str| Transformation {
type_: TransformationType::Direct,
subtype: Some(subtype.to_string()),
description: String::new(),
masking: false,
};
let indirect = |subtype: &str| Transformation {
type_: TransformationType::Indirect,
..direct(subtype)
};
let fields = resolved
.fields
.iter()
.map(|(field, sources)| {
let mut input_fields: Vec<InputField> = sources
.direct
.iter()
.map(|(source, kind)| {
let mut transformations = vec![direct(kind.subtype())];
if let Some(kinds) = resolved.indirect.get(source) {
transformations.extend(kinds.iter().map(|k| indirect(k.subtype())));
}
InputField {
namespace: source.dataset.namespace.clone(),
name: source.dataset.name.clone(),
field: Some(source.column.clone()),
transformations,
}
})
.collect();
input_fields.extend(
resolved
.indirect
.iter()
.filter(|(source, _)| !sources.direct.contains_key(source))
.map(|(source, kinds)| InputField {
namespace: source.dataset.namespace.clone(),
name: source.dataset.name.clone(),
field: Some(source.column.clone()),
transformations: kinds.iter().map(|k| indirect(k.subtype())).collect(),
}),
);
(field.clone(), FieldLineage { input_fields })
})
.collect();
DatasetFacets {
schema,
data_source,
lifecycle_state_change,
column_lineage: Some(ColumnLineageDatasetFacet {
base: BaseFacet::new(&config.producer, COLUMN_LINEAGE_FACET),
fields,
dataset: Vec::new(),
}),
..Default::default()
}
}