Skip to main content

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}