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::dataframe::DataFrame;
use datafusion::execution::context::{QueryPlanner, SessionContext, 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::{LineageContext, LineageContextProvider};
use crate::event::RunEvent;
use crate::exec::OpenLineageExec;
use crate::extract::{QueryLineage, extract};
tokio::task_local! {
static SUPPRESS_NESTED_LINEAGE: ();
}
fn nested_lineage_suppressed() -> bool {
SUPPRESS_NESTED_LINEAGE.try_with(|()| ()).is_ok()
}
#[derive(Clone)]
pub struct LineageMarker {
input: LogicalPlan,
complete: RunEvent,
client: OpenLineageClient,
producer: String,
}
impl LineageMarker {
pub fn new(
input: LogicalPlan,
complete: RunEvent,
client: OpenLineageClient,
producer: String,
) -> Self {
Self {
input,
complete,
client,
producer,
}
}
}
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 LineageHandle {
run_id: Uuid,
lineage: QueryLineage,
context: LineageContext,
}
impl LineageHandle {
pub fn run_id(&self) -> Uuid {
self.run_id
}
pub fn to_marker(
&self,
plan: LogicalPlan,
client: OpenLineageClient,
config: &OpenLineageConfig,
) -> LogicalPlan {
let complete = complete_event(self.run_id, &self.lineage, &self.context, config);
LogicalPlan::Extension(Extension {
node: Arc::new(LineageMarker::new(
plan,
complete,
client,
config.producer.clone(),
)),
})
}
pub fn emit_fail(&self, client: &OpenLineageClient, config: &OpenLineageConfig, err: &str) {
client.emit(fail_event(
self.run_id,
&self.lineage,
&self.context,
config,
err,
));
}
pub fn emit_complete(&self, client: &OpenLineageClient, config: &OpenLineageConfig) {
let mut event = complete_event(self.run_id, &self.lineage, &self.context, config);
event.event_time = chrono::Utc::now().to_rfc3339();
client.emit(event);
}
pub fn set_sql_if_absent(&mut self, sql: &str) {
if self.lineage.sql.is_none() {
self.lineage.sql = Some(sql.to_string());
}
}
}
pub async fn begin_lineage(
client: &OpenLineageClient,
context: &dyn LineageContextProvider,
config: &OpenLineageConfig,
plan: &LogicalPlan,
session_state: &SessionState,
) -> Option<LineageHandle> {
if nested_lineage_suppressed() {
return None;
}
let mut lineage = extract(plan, config);
let cx = context.context(session_state).await;
lineage.sql = cx.sql.clone();
if lineage.inputs.is_empty() && lineage.outputs.is_empty() {
return None;
}
let run_id = cx.run_id.unwrap_or_else(Uuid::now_v7);
client.emit(start_event(run_id, &lineage, &cx, config));
Some(LineageHandle {
run_id,
lineage,
context: cx,
})
}
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)),
}
}
async fn begin_lineage(
&self,
plan: &LogicalPlan,
session_state: &SessionState,
) -> Option<LineageHandle> {
begin_lineage(
&self.client,
self.context.as_ref(),
&self.config,
plan,
session_state,
)
.await
}
pub(crate) async fn execute_ddl_with_lineage(
&self,
ctx: &SessionContext,
plan: LogicalPlan,
raw_sql: &str,
) -> Result<DataFrame> {
let Some(mut handle) = self.begin_lineage(&plan, &ctx.state()).await else {
return ctx.execute_logical_plan(plan).await;
};
handle.set_sql_if_absent(raw_sql);
let result = SUPPRESS_NESTED_LINEAGE
.scope((), ctx.execute_logical_plan(plan))
.await;
match result {
Ok(df) => {
handle.emit_complete(&self.client, &self.config);
Ok(df)
}
Err(err) => {
handle.emit_fail(&self.client, &self.config, &err.to_string());
Err(err)
}
}
}
}
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 Some(handle) = self.begin_lineage(logical_plan, session_state).await else {
return self
.physical
.create_physical_plan(logical_plan, session_state)
.await;
};
let wrapped = handle.to_marker(logical_plan.clone(), self.client.clone(), &self.config);
match self
.physical
.create_physical_plan(&wrapped, session_state)
.await
{
Ok(plan) => Ok(plan),
Err(err) => {
handle.emit_fail(&self.client, &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)
);
}
use std::sync::Mutex;
use datafusion::execution::context::SessionContext;
use crate::context::StaticContextProvider;
use crate::event::{RunEvent, RunEventType};
use crate::transport::{Transport, TransportError};
#[derive(Clone, Default, Debug)]
struct Recording {
events: std::sync::Arc<Mutex<Vec<RunEvent>>>,
}
#[async_trait]
impl Transport for Recording {
async fn emit(&self, event: &RunEvent) -> std::result::Result<(), TransportError> {
self.events.lock().unwrap().push(event.clone());
Ok(())
}
}
async fn table_select() -> (SessionState, LogicalPlan) {
let ctx = SessionContext::new();
ctx.sql("CREATE TABLE t AS VALUES (1)").await.unwrap();
let plan = ctx
.state()
.create_logical_plan("SELECT * FROM t")
.await
.unwrap();
(ctx.state(), plan)
}
#[tokio::test]
async fn begin_lineage_emits_start_and_yields_a_marker() {
let (state, plan) = table_select().await;
let rec = Recording::default();
let client = OpenLineageClient::new(std::sync::Arc::new(rec.clone()));
let config = OpenLineageConfig::default();
let ctx = StaticContextProvider::default();
let handle = begin_lineage(&client, &ctx, &config, &plan, &state)
.await
.expect("query touches a dataset -> a run begins");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let events = rec.events.lock().unwrap();
let start = events
.iter()
.find(|e| e.event_type == RunEventType::Start)
.expect("START emitted");
assert_eq!(start.run.run_id, handle.run_id());
let wrapped = handle.to_marker(plan.clone(), client.clone(), &config);
assert!(matches!(&wrapped, LogicalPlan::Extension(e) if e.node.name() == "LineageMarker"));
}
#[tokio::test]
async fn begin_lineage_returns_none_for_no_dataset_query() {
let ctx_df = SessionContext::new();
let plan = ctx_df
.state()
.create_logical_plan("SET a = 1")
.await
.unwrap();
let rec = Recording::default();
let client = OpenLineageClient::new(std::sync::Arc::new(rec.clone()));
let config = OpenLineageConfig::default();
let ctx = StaticContextProvider::default();
let handle = begin_lineage(&client, &ctx, &config, &plan, &ctx_df.state()).await;
assert!(handle.is_none(), "no datasets -> no run begins");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(
rec.events.lock().unwrap().is_empty(),
"no events for a no-dataset query"
);
}
}