datafusion_openlineage/rule.rs
1//! Plan-carried lineage marker and its lowering into the terminal node.
2//!
3//! OpenLineage instrumentation has three concerns with different needs (see ADR
4//! 0005): lineage *extraction* needs the optimized `LogicalPlan`; the START event
5//! and orchestration context need `&SessionState` and are async; the terminal
6//! COMPLETE/FAIL node needs to sit at the physical root and observe execution.
7//! Only the [`QueryPlanner`] seam has `&SessionState`, so the planning-time work
8//! lives there — but the terminal node is installed the composable, DataFusion-
9//! idiomatic way: a registered [`ExtensionPlanner`] lowers a plan-carried marker
10//! into [`OpenLineageExec`], rather than the planner hand-wrapping the physical
11//! root.
12//!
13//! Flow, all under one `run_id`. First, [`OpenLineageQueryPlanner`] (the
14//! [`QueryPlanner`]) extracts lineage from the optimized logical plan, resolves
15//! the async [`LineageContextProvider`], emits START, builds the COMPLETE
16//! template, and wraps the *logical* plan in a [`LineageMarker`] carrying that
17//! template. Then physical planning lowers the marker via
18//! [`LineageExtensionPlanner`] into an [`OpenLineageExec`] at the root, which
19//! emits COMPLETE/FAIL at end of execution.
20//!
21//! A `LogicalPlan::Extension` requires a registered `ExtensionPlanner` (the
22//! default physical planner errors on unknown extension nodes), so the planner
23//! delegates physical planning to a [`DefaultPhysicalPlanner`] configured with
24//! [`LineageExtensionPlanner`].
25
26use std::cmp::Ordering;
27use std::fmt;
28use std::hash::{Hash, Hasher};
29use std::sync::Arc;
30
31use async_trait::async_trait;
32use datafusion::common::{DFSchemaRef, Result};
33use datafusion::execution::context::{QueryPlanner, SessionState};
34use datafusion::logical_expr::{
35 Expr, Extension, InvariantLevel, LogicalPlan, UserDefinedLogicalNode,
36 UserDefinedLogicalNodeCore,
37};
38use datafusion::physical_plan::ExecutionPlan;
39use datafusion::physical_planner::{DefaultPhysicalPlanner, ExtensionPlanner, PhysicalPlanner};
40use uuid::Uuid;
41
42use crate::builder::{complete_event, fail_event, start_event};
43use crate::client::OpenLineageClient;
44use crate::config::OpenLineageConfig;
45use crate::context::LineageContextProvider;
46use crate::event::RunEvent;
47use crate::exec::OpenLineageExec;
48use crate::extract::extract;
49
50// ---------------------------------------------------------------------------
51// The plan-carried marker.
52// ---------------------------------------------------------------------------
53
54/// A logical no-op wrapping the real plan, carrying the per-query lineage payload
55/// from [`OpenLineageQueryPlanner`] (which has `&SessionState`) to
56/// [`LineageExtensionPlanner`] (which installs the terminal node). Schema-
57/// transparent: it reports its input's schema so optimization and physical
58/// planning treat it as a pass-through.
59#[derive(Clone)]
60pub struct LineageMarker {
61 input: LogicalPlan,
62 /// COMPLETE event template, built at plan time; cloned into the terminal
63 /// [`OpenLineageExec`] at lowering and mutated into FAIL there on error.
64 complete: RunEvent,
65 client: OpenLineageClient,
66 producer: String,
67}
68
69impl fmt::Debug for LineageMarker {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.debug_struct("LineageMarker").finish_non_exhaustive()
72 }
73}
74
75// Identity is the run id plus the wrapped plan: enough to distinguish markers,
76// and the payload (client/template) is behavioral rather than structural.
77impl PartialEq for LineageMarker {
78 fn eq(&self, other: &Self) -> bool {
79 self.complete.run.run_id == other.complete.run.run_id && self.input == other.input
80 }
81}
82impl Eq for LineageMarker {}
83impl PartialOrd for LineageMarker {
84 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
85 self.complete
86 .run
87 .run_id
88 .partial_cmp(&other.complete.run.run_id)
89 }
90}
91impl Hash for LineageMarker {
92 fn hash<H: Hasher>(&self, state: &mut H) {
93 self.complete.run.run_id.hash(state);
94 }
95}
96
97impl UserDefinedLogicalNodeCore for LineageMarker {
98 fn name(&self) -> &str {
99 "LineageMarker"
100 }
101
102 fn inputs(&self) -> Vec<&LogicalPlan> {
103 vec![&self.input]
104 }
105
106 fn schema(&self) -> &DFSchemaRef {
107 self.input.schema()
108 }
109
110 fn check_invariants(&self, _check: InvariantLevel) -> Result<()> {
111 Ok(())
112 }
113
114 fn expressions(&self) -> Vec<Expr> {
115 vec![]
116 }
117
118 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
119 write!(f, "LineageMarker")
120 }
121
122 fn with_exprs_and_inputs(
123 &self,
124 _exprs: Vec<Expr>,
125 mut inputs: Vec<LogicalPlan>,
126 ) -> Result<Self> {
127 Ok(Self {
128 input: inputs.pop().expect("LineageMarker has one input"),
129 complete: self.complete.clone(),
130 client: self.client.clone(),
131 producer: self.producer.clone(),
132 })
133 }
134}
135
136// ---------------------------------------------------------------------------
137// Lowering: marker -> OpenLineageExec.
138// ---------------------------------------------------------------------------
139
140/// Lowers a [`LineageMarker`] into an [`OpenLineageExec`] at physical-planning
141/// time. Register it on the session's physical planner (see
142/// [`crate::session::instrument_session_state`]).
143#[derive(Debug, Default)]
144pub struct LineageExtensionPlanner;
145
146#[async_trait]
147impl ExtensionPlanner for LineageExtensionPlanner {
148 async fn plan_extension(
149 &self,
150 _planner: &dyn PhysicalPlanner,
151 node: &dyn UserDefinedLogicalNode,
152 _logical_inputs: &[&LogicalPlan],
153 physical_inputs: &[Arc<dyn ExecutionPlan>],
154 _session_state: &SessionState,
155 ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
156 // Not our node: let another extension planner handle it.
157 let Some(marker) = node.as_any().downcast_ref::<LineageMarker>() else {
158 return Ok(None);
159 };
160 let inner = physical_inputs
161 .first()
162 .expect("LineageMarker has one physical input")
163 .clone();
164 Ok(Some(OpenLineageExec::new(
165 inner,
166 marker.client.clone(),
167 marker.complete.clone(),
168 marker.producer.clone(),
169 )))
170 }
171}
172
173// ---------------------------------------------------------------------------
174// The query planner: extract + START + inject the marker.
175// ---------------------------------------------------------------------------
176
177/// A [`QueryPlanner`] that emits OpenLineage events around a query.
178///
179/// It does the `&SessionState`-bound, async work — extract lineage, resolve
180/// context, emit START, mint the `run_id`, emit FAIL on a planning error — then
181/// hands off to physical planning by wrapping the logical plan in a
182/// [`LineageMarker`]. The registered [`LineageExtensionPlanner`] lowers that
183/// marker into the terminal [`OpenLineageExec`]. Built by
184/// [`crate::session::instrument_session_state`].
185pub struct OpenLineageQueryPlanner {
186 client: OpenLineageClient,
187 context: Arc<dyn LineageContextProvider>,
188 config: OpenLineageConfig,
189 /// Physical planner that knows how to lower [`LineageMarker`]; composes any
190 /// extension planners the host already had.
191 physical: Arc<DefaultPhysicalPlanner>,
192}
193
194impl OpenLineageQueryPlanner {
195 /// Build a planner whose physical planning lowers our marker plus
196 /// `extra_extension_planners` (any the host session already registered).
197 pub fn new(
198 client: OpenLineageClient,
199 context: Arc<dyn LineageContextProvider>,
200 config: OpenLineageConfig,
201 extra_extension_planners: Vec<Arc<dyn ExtensionPlanner + Send + Sync>>,
202 ) -> Self {
203 let mut planners: Vec<Arc<dyn ExtensionPlanner + Send + Sync>> =
204 vec![Arc::new(LineageExtensionPlanner)];
205 planners.extend(extra_extension_planners);
206 Self {
207 client,
208 context,
209 config,
210 physical: Arc::new(DefaultPhysicalPlanner::with_extension_planners(planners)),
211 }
212 }
213}
214
215impl fmt::Debug for OpenLineageQueryPlanner {
216 // `DefaultPhysicalPlanner` is not `Debug`, so don't try to print it.
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 f.debug_struct("OpenLineageQueryPlanner")
219 .finish_non_exhaustive()
220 }
221}
222
223#[async_trait]
224impl QueryPlanner for OpenLineageQueryPlanner {
225 async fn create_physical_plan(
226 &self,
227 logical_plan: &LogicalPlan,
228 session_state: &SessionState,
229 ) -> Result<Arc<dyn ExecutionPlan>> {
230 let mut lineage = extract(logical_plan, &self.config);
231 let cx = self.context.context(session_state).await;
232 // The SQL text isn't recoverable from the plan; take it from the
233 // host-supplied context (absent on non-SQL paths, e.g. ingest).
234 lineage.sql = cx.sql.clone();
235
236 // Suppress lineage for queries that touch no datasets — information_schema
237 // introspection, `SET`/`SHOW`, metadata-RPC probes. They carry no input or
238 // output, so a START/COMPLETE pair only adds a dangling job node to the
239 // graph. Plan straight through without a marker so no events fire.
240 if lineage.inputs.is_empty() && lineage.outputs.is_empty() {
241 return self
242 .physical
243 .create_physical_plan(logical_plan, session_state)
244 .await;
245 }
246
247 let run_id = cx.run_id.unwrap_or_else(Uuid::now_v7);
248 self.client
249 .emit(start_event(run_id, &lineage, &cx, &self.config));
250
251 // Carry the COMPLETE template into the physical phase via the plan itself;
252 // the extension planner lowers it into an OpenLineageExec at the root that
253 // emits COMPLETE/FAIL at end of execution, under this same run id.
254 let marker = LineageMarker {
255 input: logical_plan.clone(),
256 complete: complete_event(run_id, &lineage, &cx, &self.config),
257 client: self.client.clone(),
258 producer: self.config.producer.clone(),
259 };
260 let wrapped = LogicalPlan::Extension(Extension {
261 node: Arc::new(marker),
262 });
263
264 match self
265 .physical
266 .create_physical_plan(&wrapped, session_state)
267 .await
268 {
269 Ok(plan) => Ok(plan),
270 Err(err) => {
271 // Planning failed outright — no execution to observe, emit FAIL now.
272 self.client.emit(fail_event(
273 run_id,
274 &lineage,
275 &cx,
276 &self.config,
277 &err.to_string(),
278 ));
279 Err(err)
280 }
281 }
282 }
283}