datafusion_physical_plan/proto.rs
1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Serialization hooks for [`ExecutionPlan`], mirroring the
19//! `try_to_proto`/`try_from_proto` pattern used for `PhysicalExpr`.
20//!
21//! # Why the indirection
22//!
23//! An `ExecutionPlan` must be able to (de)serialize its child plans and its
24//! child physical expressions recursively. The concrete recursion lives in
25//! `datafusion-proto` (it owns the extension codec, the session context and the
26//! central converter), but `datafusion-proto` sits *above* `datafusion-physical-plan`
27//! in the crate graph. To let a plan drive that recursion without a dependency
28//! cycle, this module defines:
29//!
30//! * [`ExecutionPlanEncodeCtx`] / [`ExecutionPlanDecodeCtx`] — the stable,
31//! concrete context types a plan author interacts with. New capabilities can
32//! be added here without changing every plan's hook signature.
33//! * [`ExecutionPlanEncode`] / [`ExecutionPlanDecode`] — internal dispatch
34//! traits, *defined* here but *implemented* in `datafusion-proto`, that the
35//! context types delegate to. This is the dependency inversion that keeps the
36//! proto types flowing in one direction only. They are `#[doc(hidden)]`: not
37//! public API, `pub` only because their implementors live in another crate.
38//!
39//! `datafusion-physical-plan` depends on the pure prost types in
40//! `datafusion-proto-models` (feature `proto`), never on `datafusion-proto`.
41//!
42//! # Function-carrying plans
43//!
44//! Plans that reference UD(A/W)Fs (`AggregateExec`, the window execs, …) also
45//! ride the hook: the context exposes typed, *bytes-only* function serde —
46//! [`encode_udaf`](ExecutionPlanEncodeCtx::encode_udaf) /
47//! [`decode_udaf`](ExecutionPlanDecodeCtx::decode_udaf) and the udf/udwf
48//! siblings. These take/return `datafusion-expr` types plus `Vec<u8>` and never
49//! name a proto type, so the `PhysicalExtensionCodec` (which only
50//! `datafusion-proto` can name) stays fully encapsulated behind the adapter that
51//! backs these traits. The lookup-order policy (payload → codec; else registry →
52//! codec fallback) lives once, in that adapter, rather than in every plan.
53//!
54//! This is possible because `datafusion-physical-plan` sits *above*
55//! `datafusion-expr` in the crate graph; the expression-side ctx (in
56//! `physical-expr-common`, *below* `datafusion-expr`) cannot do this, which is
57//! why `ScalarFunctionExpr` remains special-cased there.
58//!
59//! [`ExecutionPlan`]: crate::ExecutionPlan
60
61use std::sync::Arc;
62
63use arrow::datatypes::Schema;
64use datafusion_common::{Result, internal_datafusion_err};
65use datafusion_execution::TaskContext;
66use datafusion_expr::physical_planning_context::ScalarSubqueryResults;
67use datafusion_expr::{AggregateUDF, ScalarUDF, WindowUDF};
68use datafusion_physical_expr::PhysicalExpr;
69use datafusion_physical_expr_common::physical_expr::proto_decode::{
70 PhysicalExprDecode, PhysicalExprDecodeCtx,
71};
72use datafusion_physical_expr_common::physical_expr::proto_encode::{
73 PhysicalExprEncode, PhysicalExprEncodeCtx,
74};
75use datafusion_proto_models::protobuf::{PhysicalExprNode, PhysicalPlanNode};
76
77use crate::ExecutionPlan;
78
79/// Internal dispatch trait backing [`ExecutionPlanEncodeCtx`].
80///
81/// Implemented by `datafusion-proto`. Plan authors never name this trait; they
82/// call methods on [`ExecutionPlanEncodeCtx`] instead.
83///
84/// **Not public API.** `pub` only because the implementors live in another
85/// crate; `#[doc(hidden)]` records that, so encoding primitives can be added
86/// here as the serialization hooks grow without breaking downstream code.
87#[doc(hidden)]
88pub trait ExecutionPlanEncode {
89 /// Serialize a child execution plan (recursing through the central
90 /// serializer, so the child's own `try_to_proto` hook is honored).
91 fn encode_plan(&self, plan: &Arc<dyn ExecutionPlan>) -> Result<PhysicalPlanNode>;
92
93 /// Serialize a physical expression owned by the plan.
94 fn encode_expr(&self, expr: &Arc<dyn PhysicalExpr>) -> Result<PhysicalExprNode>;
95
96 /// Serialize a scalar UDF to an opaque payload. `None` means "decodable by
97 /// name alone" (built-ins). Bytes-only: no proto types cross this boundary.
98 fn encode_udf(&self, udf: &ScalarUDF) -> Result<Option<Vec<u8>>>;
99
100 /// Serialize an aggregate UDF to an opaque payload. `None` means "decodable
101 /// by name alone".
102 fn encode_udaf(&self, udaf: &AggregateUDF) -> Result<Option<Vec<u8>>>;
103
104 /// Serialize a window UDF to an opaque payload. `None` means "decodable by
105 /// name alone".
106 fn encode_udwf(&self, udwf: &WindowUDF) -> Result<Option<Vec<u8>>>;
107}
108
109/// Internal dispatch trait backing [`ExecutionPlanDecodeCtx`].
110///
111/// Implemented by `datafusion-proto`. Plan authors never name this trait; they
112/// call methods on [`ExecutionPlanDecodeCtx`] instead.
113///
114/// **Not public API.** `pub` only because the implementors live in another
115/// crate; `#[doc(hidden)]` records that, so decoding primitives can be added
116/// here as the serialization hooks grow without breaking downstream code.
117#[doc(hidden)]
118pub trait ExecutionPlanDecode {
119 /// Deserialize a child execution plan (recursing through the central
120 /// deserializer, so the child's own `try_from_proto` is honored).
121 fn decode_plan(&self, node: &PhysicalPlanNode) -> Result<Arc<dyn ExecutionPlan>>;
122
123 /// Deserialize a child plan with `results` active for scalar subquery
124 /// expressions in that plan's subtree.
125 fn decode_plan_with_scalar_subquery_results(
126 &self,
127 node: &PhysicalPlanNode,
128 results: ScalarSubqueryResults,
129 ) -> Result<Arc<dyn ExecutionPlan>>;
130
131 /// Deserialize a physical expression against `input_schema`.
132 fn decode_expr(
133 &self,
134 node: &PhysicalExprNode,
135 input_schema: &Schema,
136 ) -> Result<Arc<dyn PhysicalExpr>>;
137
138 /// The session task context, used by plans that need the function registry
139 /// or session configuration. Never exposes the proto extension codec.
140 fn task_ctx(&self) -> &TaskContext;
141
142 /// Reconstruct a scalar UDF from its name and optional payload. Encapsulates
143 /// the lookup-order policy (payload → codec; else registry → codec fallback)
144 /// so no plan re-derives it. Bytes-only: no proto types cross this boundary.
145 fn decode_udf(&self, name: &str, payload: Option<&[u8]>) -> Result<Arc<ScalarUDF>>;
146
147 /// Reconstruct an aggregate UDF from its name and optional payload.
148 fn decode_udaf(
149 &self,
150 name: &str,
151 payload: Option<&[u8]>,
152 ) -> Result<Arc<AggregateUDF>>;
153
154 /// Reconstruct a window UDF from its name and optional payload.
155 fn decode_udwf(&self, name: &str, payload: Option<&[u8]>) -> Result<Arc<WindowUDF>>;
156}
157
158/// Context handed to [`ExecutionPlan::try_to_proto`].
159///
160///
161/// Provides the primitives a plan needs to serialize its children and
162/// expressions without naming `datafusion-proto`.
163pub struct ExecutionPlanEncodeCtx<'a> {
164 encoder: &'a dyn ExecutionPlanEncode,
165}
166
167impl<'a> ExecutionPlanEncodeCtx<'a> {
168 /// Create a new encode context wrapping an [`ExecutionPlanEncode`]
169 /// implementation (supplied by `datafusion-proto`).
170 pub fn new(encoder: &'a dyn ExecutionPlanEncode) -> Self {
171 Self { encoder }
172 }
173
174 /// Serialize a single child plan.
175 pub fn encode_child(
176 &self,
177 plan: &Arc<dyn ExecutionPlan>,
178 ) -> Result<PhysicalPlanNode> {
179 self.encoder.encode_plan(plan)
180 }
181
182 /// Serialize an iterator of child plans.
183 pub fn encode_children<'b, I>(&self, plans: I) -> Result<Vec<PhysicalPlanNode>>
184 where
185 I: IntoIterator<Item = &'b Arc<dyn ExecutionPlan>>,
186 {
187 plans.into_iter().map(|p| self.encode_child(p)).collect()
188 }
189
190 /// Serialize a single physical expression.
191 pub fn encode_expr(&self, expr: &Arc<dyn PhysicalExpr>) -> Result<PhysicalExprNode> {
192 self.encoder.encode_expr(expr)
193 }
194
195 /// Serialize an iterator of physical expressions.
196 pub fn encode_expressions<'b, I>(&self, exprs: I) -> Result<Vec<PhysicalExprNode>>
197 where
198 I: IntoIterator<Item = &'b Arc<dyn PhysicalExpr>>,
199 {
200 exprs.into_iter().map(|e| self.encode_expr(e)).collect()
201 }
202
203 /// Serialize a scalar UDF to an opaque payload (`None` = built-in, decodable
204 /// by name). No proto types cross this boundary.
205 pub fn encode_udf(&self, udf: &ScalarUDF) -> Result<Option<Vec<u8>>> {
206 self.encoder.encode_udf(udf)
207 }
208
209 /// Serialize an aggregate UDF to an opaque payload (`None` = decodable by
210 /// name).
211 pub fn encode_udaf(&self, udaf: &AggregateUDF) -> Result<Option<Vec<u8>>> {
212 self.encoder.encode_udaf(udaf)
213 }
214
215 /// Serialize a window UDF to an opaque payload (`None` = decodable by name).
216 pub fn encode_udwf(&self, udwf: &WindowUDF) -> Result<Option<Vec<u8>>> {
217 self.encoder.encode_udwf(udwf)
218 }
219
220 /// An expression-level encode context backed by this plan context.
221 ///
222 /// Lets a plan hand `ctx` to expression-level conversions that own their own
223 /// wire logic — e.g.
224 /// [`Partitioning::try_to_proto`](datafusion_physical_expr::Partitioning::try_to_proto)
225 /// and
226 /// [`PhysicalSortExpr::try_to_proto`](datafusion_physical_expr::PhysicalSortExpr::try_to_proto).
227 pub fn expr_ctx(&self) -> PhysicalExprEncodeCtx<'_> {
228 PhysicalExprEncodeCtx::new(self)
229 }
230}
231
232/// Lets [`ExecutionPlanEncodeCtx`] back a [`PhysicalExprEncodeCtx`], so
233/// expression-level conversions can be reused from plan hooks.
234impl PhysicalExprEncode for ExecutionPlanEncodeCtx<'_> {
235 fn encode(&self, expr: &Arc<dyn PhysicalExpr>) -> Result<PhysicalExprNode> {
236 self.encode_expr(expr)
237 }
238}
239
240/// Context handed to a plan's `try_from_proto` associated function.
241///
242/// Provides the primitives a plan needs to deserialize its children and
243/// expressions without naming `datafusion-proto`.
244pub struct ExecutionPlanDecodeCtx<'a> {
245 decoder: &'a dyn ExecutionPlanDecode,
246}
247
248impl<'a> ExecutionPlanDecodeCtx<'a> {
249 /// Create a new decode context wrapping an [`ExecutionPlanDecode`]
250 /// implementation (supplied by `datafusion-proto`).
251 pub fn new(decoder: &'a dyn ExecutionPlanDecode) -> Self {
252 Self { decoder }
253 }
254
255 /// Deserialize a single child plan.
256 pub fn decode_child(
257 &self,
258 node: &PhysicalPlanNode,
259 ) -> Result<Arc<dyn ExecutionPlan>> {
260 self.decoder.decode_plan(node)
261 }
262
263 /// Deserialize a child plan with `results` active for scalar subquery
264 /// expressions in that plan's subtree.
265 pub fn decode_child_with_scalar_subquery_results(
266 &self,
267 node: &PhysicalPlanNode,
268 results: ScalarSubqueryResults,
269 ) -> Result<Arc<dyn ExecutionPlan>> {
270 self.decoder
271 .decode_plan_with_scalar_subquery_results(node, results)
272 }
273
274 /// Deserialize a required child plan, producing a uniform "missing required
275 /// field" error when the optional wire field is absent.
276 pub fn decode_required_child(
277 &self,
278 node: Option<&PhysicalPlanNode>,
279 plan_name: &str,
280 field: &str,
281 ) -> Result<Arc<dyn ExecutionPlan>> {
282 let node = node.ok_or_else(|| {
283 internal_datafusion_err!("{plan_name} is missing required field '{field}'")
284 })?;
285 self.decode_child(node)
286 }
287
288 /// Deserialize a physical expression against `input_schema`.
289 pub fn decode_expr(
290 &self,
291 node: &PhysicalExprNode,
292 input_schema: &Schema,
293 ) -> Result<Arc<dyn PhysicalExpr>> {
294 self.decoder.decode_expr(node, input_schema)
295 }
296
297 /// Deserialize a required physical expression against `input_schema`.
298 pub fn decode_required_expr(
299 &self,
300 node: Option<&PhysicalExprNode>,
301 input_schema: &Schema,
302 plan_name: &str,
303 field: &str,
304 ) -> Result<Arc<dyn PhysicalExpr>> {
305 let node = node.ok_or_else(|| {
306 internal_datafusion_err!("{plan_name} is missing required field '{field}'")
307 })?;
308 self.decode_expr(node, input_schema)
309 }
310
311 /// The session task context (function registry + session config). Never
312 /// exposes the proto extension codec.
313 pub fn task_ctx(&self) -> &TaskContext {
314 self.decoder.task_ctx()
315 }
316
317 /// Reconstruct a scalar UDF from its name and optional payload. The
318 /// lookup-order policy is owned by `datafusion-proto`; no proto types cross
319 /// this boundary.
320 pub fn decode_udf(
321 &self,
322 name: &str,
323 payload: Option<&[u8]>,
324 ) -> Result<Arc<ScalarUDF>> {
325 self.decoder.decode_udf(name, payload)
326 }
327
328 /// Reconstruct an aggregate UDF from its name and optional payload.
329 pub fn decode_udaf(
330 &self,
331 name: &str,
332 payload: Option<&[u8]>,
333 ) -> Result<Arc<AggregateUDF>> {
334 self.decoder.decode_udaf(name, payload)
335 }
336
337 /// Reconstruct a window UDF from its name and optional payload.
338 pub fn decode_udwf(
339 &self,
340 name: &str,
341 payload: Option<&[u8]>,
342 ) -> Result<Arc<WindowUDF>> {
343 self.decoder.decode_udwf(name, payload)
344 }
345
346 /// An expression-level decode context backed by this plan context, bound to
347 /// `input_schema`.
348 ///
349 /// The decode counterpart of
350 /// [`ExecutionPlanEncodeCtx::expr_ctx`], for calling conversions such as
351 /// [`Partitioning::try_from_proto`](datafusion_physical_expr::Partitioning::try_from_proto).
352 pub fn expr_ctx<'s>(&'s self, input_schema: &'s Schema) -> PhysicalExprDecodeCtx<'s> {
353 PhysicalExprDecodeCtx::new(input_schema, self)
354 }
355}
356
357/// Lets [`ExecutionPlanDecodeCtx`] back a [`PhysicalExprDecodeCtx`], so
358/// expression-level conversions can be reused from plan hooks.
359impl PhysicalExprDecode for ExecutionPlanDecodeCtx<'_> {
360 fn decode(
361 &self,
362 node: &PhysicalExprNode,
363 schema: &Schema,
364 ) -> Result<Arc<dyn PhysicalExpr>> {
365 self.decode_expr(node, schema)
366 }
367}
368
369/// Assert that a [`PhysicalPlanNode`] carries the expected `PhysicalPlanType`
370/// variant, returning a reference to the inner payload, else an `internal_err!`.
371/// Mirrors `expect_expr_variant!` on the expression side. Field access on the
372/// result auto-derefs through the `Box` that boxed variants use.
373#[macro_export]
374macro_rules! expect_plan_variant {
375 ($node:expr, $variant:path, $plan_name:literal $(,)?) => {{
376 match &$node.physical_plan_type {
377 Some($variant(inner)) => inner,
378 _ => {
379 return ::datafusion_common::internal_err!(concat!(
380 "PhysicalPlanNode is not a ",
381 $plan_name
382 ));
383 }
384 }
385 }};
386}