use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use async_trait::async_trait;
use datafusion::common::{DFSchemaRef, Result};
use datafusion::execution::context::{QueryPlanner, SessionState};
use datafusion::logical_expr::{
Expr, Extension, InvariantLevel, LogicalPlan, UserDefinedLogicalNode,
UserDefinedLogicalNodeCore,
};
use datafusion::physical_plan::ExecutionPlan;
use datafusion::physical_planner::{DefaultPhysicalPlanner, ExtensionPlanner, PhysicalPlanner};
use uuid::Uuid;
use crate::builder::{complete_event, fail_event, start_event};
use crate::client::OpenLineageClient;
use crate::config::OpenLineageConfig;
use crate::context::LineageContextProvider;
use crate::event::RunEvent;
use crate::exec::OpenLineageExec;
use crate::extract::extract;
#[derive(Clone)]
pub struct LineageMarker {
input: LogicalPlan,
complete: RunEvent,
client: OpenLineageClient,
producer: String,
}
impl fmt::Debug for LineageMarker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LineageMarker").finish_non_exhaustive()
}
}
impl PartialEq for LineageMarker {
fn eq(&self, other: &Self) -> bool {
self.complete.run.run_id == other.complete.run.run_id && self.input == other.input
}
}
impl Eq for LineageMarker {}
impl PartialOrd for LineageMarker {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.complete
.run
.run_id
.partial_cmp(&other.complete.run.run_id)
}
}
impl Hash for LineageMarker {
fn hash<H: Hasher>(&self, state: &mut H) {
self.complete.run.run_id.hash(state);
}
}
impl UserDefinedLogicalNodeCore for LineageMarker {
fn name(&self) -> &str {
"LineageMarker"
}
fn inputs(&self) -> Vec<&LogicalPlan> {
vec![&self.input]
}
fn schema(&self) -> &DFSchemaRef {
self.input.schema()
}
fn check_invariants(&self, _check: InvariantLevel) -> Result<()> {
Ok(())
}
fn expressions(&self) -> Vec<Expr> {
vec![]
}
fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "LineageMarker")
}
fn with_exprs_and_inputs(
&self,
_exprs: Vec<Expr>,
mut inputs: Vec<LogicalPlan>,
) -> Result<Self> {
Ok(Self {
input: inputs.pop().expect("LineageMarker has one input"),
complete: self.complete.clone(),
client: self.client.clone(),
producer: self.producer.clone(),
})
}
}
#[derive(Debug, Default)]
pub struct LineageExtensionPlanner;
#[async_trait]
impl ExtensionPlanner for LineageExtensionPlanner {
async fn plan_extension(
&self,
_planner: &dyn PhysicalPlanner,
node: &dyn UserDefinedLogicalNode,
_logical_inputs: &[&LogicalPlan],
physical_inputs: &[Arc<dyn ExecutionPlan>],
_session_state: &SessionState,
) -> Result<Option<Arc<dyn ExecutionPlan>>> {
let Some(marker) = node.as_any().downcast_ref::<LineageMarker>() else {
return Ok(None);
};
let inner = physical_inputs
.first()
.expect("LineageMarker has one physical input")
.clone();
Ok(Some(OpenLineageExec::new(
inner,
marker.client.clone(),
marker.complete.clone(),
marker.producer.clone(),
)))
}
}
pub struct OpenLineageQueryPlanner {
client: OpenLineageClient,
context: Arc<dyn LineageContextProvider>,
config: OpenLineageConfig,
physical: Arc<DefaultPhysicalPlanner>,
}
impl OpenLineageQueryPlanner {
pub fn new(
client: OpenLineageClient,
context: Arc<dyn LineageContextProvider>,
config: OpenLineageConfig,
extra_extension_planners: Vec<Arc<dyn ExtensionPlanner + Send + Sync>>,
) -> Self {
let mut planners: Vec<Arc<dyn ExtensionPlanner + Send + Sync>> =
vec![Arc::new(LineageExtensionPlanner)];
planners.extend(extra_extension_planners);
Self {
client,
context,
config,
physical: Arc::new(DefaultPhysicalPlanner::with_extension_planners(planners)),
}
}
}
impl fmt::Debug for OpenLineageQueryPlanner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OpenLineageQueryPlanner")
.finish_non_exhaustive()
}
}
#[async_trait]
impl QueryPlanner for OpenLineageQueryPlanner {
async fn create_physical_plan(
&self,
logical_plan: &LogicalPlan,
session_state: &SessionState,
) -> Result<Arc<dyn ExecutionPlan>> {
let mut lineage = extract(logical_plan, &self.config);
let cx = self.context.context(session_state).await;
lineage.sql = cx.sql.clone();
if lineage.inputs.is_empty() && lineage.outputs.is_empty() {
return self
.physical
.create_physical_plan(logical_plan, session_state)
.await;
}
let run_id = cx.run_id.unwrap_or_else(Uuid::now_v7);
self.client
.emit(start_event(run_id, &lineage, &cx, &self.config));
let marker = LineageMarker {
input: logical_plan.clone(),
complete: complete_event(run_id, &lineage, &cx, &self.config),
client: self.client.clone(),
producer: self.config.producer.clone(),
};
let wrapped = LogicalPlan::Extension(Extension {
node: Arc::new(marker),
});
match self
.physical
.create_physical_plan(&wrapped, session_state)
.await
{
Ok(plan) => Ok(plan),
Err(err) => {
self.client.emit(fail_event(
run_id,
&lineage,
&cx,
&self.config,
&err.to_string(),
));
Err(err)
}
}
}
}
#[cfg(test)]
mod tests {
use std::collections::hash_map::DefaultHasher;
use datafusion::logical_expr::LogicalPlanBuilder;
use super::*;
use crate::QueryLineage;
use crate::context::LineageContext;
use crate::transport::NoopTransport;
use datafusion::logical_expr::UserDefinedLogicalNodeCore as NodeCore;
fn marker(run_id: Uuid) -> LineageMarker {
let input = LogicalPlanBuilder::empty(false).build().unwrap();
let config = OpenLineageConfig::default();
let complete = complete_event(
run_id,
&QueryLineage::default(),
&LineageContext::default(),
&config,
);
LineageMarker {
input,
complete,
client: OpenLineageClient::new(Arc::new(NoopTransport)),
producer: config.producer,
}
}
fn hash_of(m: &LineageMarker) -> u64 {
let mut h = DefaultHasher::new();
m.hash(&mut h);
h.finish()
}
#[tokio::test]
async fn node_core_is_schema_transparent_and_expr_free() {
let m = marker(Uuid::now_v7());
assert_eq!(NodeCore::name(&m), "LineageMarker");
assert_eq!(NodeCore::inputs(&m).len(), 1);
assert_eq!(NodeCore::schema(&m), NodeCore::inputs(&m)[0].schema());
assert!(NodeCore::expressions(&m).is_empty());
assert!(NodeCore::check_invariants(&m, InvariantLevel::Always).is_ok());
assert_eq!(format!("{m:?}"), "LineageMarker { .. }");
}
#[tokio::test]
async fn with_exprs_and_inputs_rebuilds_preserving_payload() {
let run_id = Uuid::now_v7();
let m = marker(run_id);
let new_input = LogicalPlanBuilder::empty(true).build().unwrap();
let rebuilt = NodeCore::with_exprs_and_inputs(&m, vec![], vec![new_input.clone()]).unwrap();
assert_eq!(NodeCore::inputs(&rebuilt)[0], &new_input);
assert_eq!(rebuilt.complete.run.run_id, run_id);
}
#[tokio::test]
async fn identity_keys_on_run_id_and_input() {
let run_id = Uuid::now_v7();
let a = marker(run_id);
let b = marker(run_id);
assert_eq!(a, b);
assert_eq!(hash_of(&a), hash_of(&b));
assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
let c = marker(Uuid::now_v7());
assert_ne!(a, c);
assert_eq!(
a.partial_cmp(&c),
a.complete.run.run_id.partial_cmp(&c.complete.run.run_id)
);
}
}