Skip to main content

datafusion_substrait/logical_plan/consumer/
substrait_consumer.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
18use super::{
19    from_aggregate_rel, from_cast, from_cross_rel, from_exchange_rel, from_fetch_rel,
20    from_field_reference, from_filter_rel, from_if_then, from_join_rel, from_literal,
21    from_nested, from_project_rel, from_read_rel, from_scalar_function, from_set_rel,
22    from_singular_or_list, from_sort_rel, from_subquery, from_substrait_rel,
23    from_substrait_rex, from_window_function,
24};
25use crate::extensions::Extensions;
26use crate::logical_plan::consumer::{
27    field_from_substrait_type_without_names, from_lambda,
28};
29use async_trait::async_trait;
30use datafusion::arrow::datatypes::{DataType, FieldRef};
31use datafusion::catalog::TableProvider;
32use datafusion::common::datatype::FieldExt;
33use datafusion::common::{
34    DFSchema, ScalarValue, TableReference, not_impl_err, substrait_err,
35};
36use datafusion::execution::{FunctionRegistry, SessionState};
37use datafusion::logical_expr::expr::LambdaVariable;
38use datafusion::logical_expr::{Expr, Extension, LogicalPlan};
39use std::collections::VecDeque;
40use std::sync::{Arc, RwLock};
41use substrait::proto::expression as substrait_expression;
42use substrait::proto::expression::{
43    Enum, FieldReference, IfThen, Literal, MultiOrList, Nested, ScalarFunction,
44    SingularOrList, SwitchExpression, WindowFunction,
45};
46use substrait::proto::{self, Type};
47use substrait::proto::{
48    AggregateRel, ConsistentPartitionWindowRel, CrossRel, DynamicParameter, ExchangeRel,
49    Expression, ExtensionLeafRel, ExtensionMultiRel, ExtensionSingleRel, FetchRel,
50    FilterRel, JoinRel, ProjectRel, ReadRel, Rel, SetRel, SortRel, r#type,
51};
52
53#[async_trait]
54/// This trait is used to consume Substrait plans, converting them into DataFusion Logical Plans.
55/// It can be implemented by users to allow for custom handling of relations, expressions, etc.
56///
57/// Combined with the [crate::logical_plan::producer::SubstraitProducer] this allows for fully
58/// customizable Substrait serde.
59///
60/// # Example Usage
61///
62/// ```
63/// # use async_trait::async_trait;
64/// # use datafusion::catalog::TableProvider;
65/// # use datafusion::common::{not_impl_err, substrait_err, DFSchema, ScalarValue, TableReference};
66/// # use datafusion::error::Result;
67/// # use datafusion::execution::{FunctionRegistry, SessionState};
68/// # use datafusion::logical_expr::{Expr, LogicalPlan, LogicalPlanBuilder};
69/// # use std::sync::Arc;
70/// # use substrait::proto;
71/// # use substrait::proto::{ExtensionLeafRel, FilterRel, ProjectRel, Type};
72/// # use datafusion::arrow::datatypes::DataType;
73/// # use datafusion::logical_expr::expr::ScalarFunction;
74/// # use datafusion_substrait::extensions::Extensions;
75/// # use datafusion_substrait::logical_plan::consumer::{
76/// #     from_project_rel, from_substrait_rel, from_substrait_rex, SubstraitConsumer, DefaultSubstraitLambdaConsumer
77/// # };
78///
79/// struct CustomSubstraitConsumer {
80///     extensions: Arc<Extensions>,
81///     state: Arc<SessionState>,
82///     // You can reuse existing consumer code related to lambdas
83///     lambda_consumer: DefaultSubstraitLambdaConsumer,
84/// }
85///
86/// #[async_trait]
87/// impl SubstraitConsumer for CustomSubstraitConsumer {
88///     async fn resolve_table_ref(
89///         &self,
90///         table_ref: &TableReference,
91///     ) -> Result<Option<Arc<dyn TableProvider>>> {
92///         let table = table_ref.table().to_string();
93///         let schema = self.state.schema_for_ref(table_ref.clone())?;
94///         let table_provider = schema.table(&table).await?;
95///         Ok(table_provider)
96///     }
97///
98///     fn get_extensions(&self) -> &Extensions {
99///         self.extensions.as_ref()
100///     }
101///
102///     fn get_function_registry(&self) -> &impl FunctionRegistry {
103///         self.state.as_ref()
104///     }
105///
106///     fn push_lambda_parameters(
107///        &self,
108///        lambda_parameters: &[Type],
109///        input_schema: &DFSchema,
110///    ) -> datafusion::common::Result<Vec<String>> {
111///        self.lambda_consumer.push_lambda_parameters(
112///            self,
113///            lambda_parameters,
114///            input_schema,
115///        )
116///    }
117///
118///     fn pop_lambda_parameters(&self) {
119///        self.lambda_consumer.pop_lambda_parameters();
120///    }
121///
122///    fn lambda_variable(
123///        &self,
124///        steps_out: usize,
125///        field_idx: usize,
126///    ) -> datafusion::common::Result<Expr> {
127///        self.lambda_consumer.lambda_variable(steps_out, field_idx)
128///    }
129///
130///     // You can reuse existing consumer code to assist in handling advanced extensions
131///     async fn consume_project(&self, rel: &ProjectRel) -> Result<LogicalPlan> {
132///         let df_plan = from_project_rel(self, rel).await?;
133///         if let Some(advanced_extension) = rel.advanced_extension.as_ref() {
134///             not_impl_err!(
135///                 "decode and handle an advanced extension: {:?}",
136///                 advanced_extension
137///             )
138///         } else {
139///             Ok(df_plan)
140///         }
141///     }
142///
143///     // You can implement a fully custom consumer method if you need special handling
144///     async fn consume_filter(&self, rel: &FilterRel) -> Result<LogicalPlan> {
145///         let input = self.consume_rel(rel.input.as_ref().unwrap()).await?;
146///         let expression =
147///             self.consume_expression(rel.condition.as_ref().unwrap(), input.schema())
148///                 .await?;
149///         // though this one is quite boring
150///         LogicalPlanBuilder::from(input).filter(expression)?.build()
151///     }
152///
153///     // You can add handlers for extension relations
154///     async fn consume_extension_leaf(
155///         &self,
156///         rel: &ExtensionLeafRel,
157///     ) -> Result<LogicalPlan> {
158///         not_impl_err!(
159///             "handle protobuf Any {} as you need",
160///             rel.detail.as_ref().unwrap().type_url
161///         )
162///     }
163///
164///     // and handlers for user-define types
165///     fn consume_user_defined_type(&self, typ: &proto::r#type::UserDefined) -> Result<DataType> {
166///         let type_string = self.extensions.types.get(&typ.type_reference).unwrap();
167///         match type_string.as_str() {
168///             "u!foo" => not_impl_err!("handle foo conversion"),
169///             "u!bar" => not_impl_err!("handle bar conversion"),
170///             _ => substrait_err!("unexpected type")
171///         }
172///     }
173///
174///     // and user-defined literals
175///     fn consume_user_defined_literal(&self, literal: &proto::expression::literal::UserDefined) -> Result<ScalarValue> {
176///         // extract type_reference from the new TypeAnchorType oneof
177///         let type_ref = match literal.type_anchor_type {
178///             Some(proto::expression::literal::user_defined::TypeAnchorType::TypeReference(r)) => r,
179///             Some(proto::expression::literal::user_defined::TypeAnchorType::TypeAliasReference(_)) => {
180///                 return not_impl_err!("Type alias references are not yet supported")
181///             }
182///             None => 0,
183///         };
184///         let type_string = self.extensions.types.get(&type_ref).unwrap();
185///         match type_string.as_str() {
186///             "u!foo" => not_impl_err!("handle foo conversion"),
187///             "u!bar" => not_impl_err!("handle bar conversion"),
188///             _ => substrait_err!("unexpected type")
189///         }
190///     }
191/// }
192/// ```
193pub trait SubstraitConsumer: Send + Sync + Sized {
194    async fn resolve_table_ref(
195        &self,
196        table_ref: &TableReference,
197    ) -> datafusion::common::Result<Option<Arc<dyn TableProvider>>>;
198
199    // TODO: Remove these two methods
200    //   Ideally, the abstract consumer should not place any constraints on implementations.
201    //   The functionality for which the Extensions and FunctionRegistry is needed should be abstracted
202    //   out into methods on the trait. As an example, resolve_table_reference is such a method.
203    //   See: https://github.com/apache/datafusion/issues/13863
204    fn get_extensions(&self) -> &Extensions;
205    fn get_function_registry(&self) -> &impl FunctionRegistry;
206
207    // Relation Methods
208    // There is one method per Substrait relation to allow for easy overriding of consumer behaviour.
209    // These methods have default implementations calling the common handler code, to allow for users
210    // to re-use common handling logic.
211
212    /// All [Rel]s to be converted pass through this method.
213    /// You can provide your own implementation if you wish to customize the conversion behaviour.
214    async fn consume_rel(&self, rel: &Rel) -> datafusion::common::Result<LogicalPlan> {
215        from_substrait_rel(self, rel).await
216    }
217
218    async fn consume_read(
219        &self,
220        rel: &ReadRel,
221    ) -> datafusion::common::Result<LogicalPlan> {
222        from_read_rel(self, rel).await
223    }
224
225    async fn consume_filter(
226        &self,
227        rel: &FilterRel,
228    ) -> datafusion::common::Result<LogicalPlan> {
229        from_filter_rel(self, rel).await
230    }
231
232    async fn consume_fetch(
233        &self,
234        rel: &FetchRel,
235    ) -> datafusion::common::Result<LogicalPlan> {
236        from_fetch_rel(self, rel).await
237    }
238
239    async fn consume_aggregate(
240        &self,
241        rel: &AggregateRel,
242    ) -> datafusion::common::Result<LogicalPlan> {
243        from_aggregate_rel(self, rel).await
244    }
245
246    async fn consume_sort(
247        &self,
248        rel: &SortRel,
249    ) -> datafusion::common::Result<LogicalPlan> {
250        from_sort_rel(self, rel).await
251    }
252
253    async fn consume_join(
254        &self,
255        rel: &JoinRel,
256    ) -> datafusion::common::Result<LogicalPlan> {
257        from_join_rel(self, rel).await
258    }
259
260    async fn consume_project(
261        &self,
262        rel: &ProjectRel,
263    ) -> datafusion::common::Result<LogicalPlan> {
264        from_project_rel(self, rel).await
265    }
266
267    async fn consume_set(&self, rel: &SetRel) -> datafusion::common::Result<LogicalPlan> {
268        from_set_rel(self, rel).await
269    }
270
271    async fn consume_cross(
272        &self,
273        rel: &CrossRel,
274    ) -> datafusion::common::Result<LogicalPlan> {
275        from_cross_rel(self, rel).await
276    }
277
278    async fn consume_consistent_partition_window(
279        &self,
280        _rel: &ConsistentPartitionWindowRel,
281    ) -> datafusion::common::Result<LogicalPlan> {
282        not_impl_err!("Consistent Partition Window Rel not supported")
283    }
284
285    async fn consume_exchange(
286        &self,
287        rel: &ExchangeRel,
288    ) -> datafusion::common::Result<LogicalPlan> {
289        from_exchange_rel(self, rel).await
290    }
291
292    // Expression Methods
293    // There is one method per Substrait expression to allow for easy overriding of consumer behaviour
294    // These methods have default implementations calling the common handler code, to allow for users
295    // to re-use common handling logic.
296
297    /// All [Expression]s to be converted pass through this method.
298    /// You can provide your own implementation if you wish to customize the conversion behaviour.
299    async fn consume_expression(
300        &self,
301        expr: &Expression,
302        input_schema: &DFSchema,
303    ) -> datafusion::common::Result<Expr> {
304        from_substrait_rex(self, expr, input_schema).await
305    }
306
307    async fn consume_literal(&self, expr: &Literal) -> datafusion::common::Result<Expr> {
308        from_literal(self, expr).await
309    }
310
311    async fn consume_field_reference(
312        &self,
313        expr: &FieldReference,
314        input_schema: &DFSchema,
315    ) -> datafusion::common::Result<Expr> {
316        from_field_reference(self, expr, input_schema).await
317    }
318
319    async fn consume_scalar_function(
320        &self,
321        expr: &ScalarFunction,
322        input_schema: &DFSchema,
323    ) -> datafusion::common::Result<Expr> {
324        from_scalar_function(self, expr, input_schema).await
325    }
326
327    async fn consume_window_function(
328        &self,
329        expr: &WindowFunction,
330        input_schema: &DFSchema,
331    ) -> datafusion::common::Result<Expr> {
332        from_window_function(self, expr, input_schema).await
333    }
334
335    async fn consume_if_then(
336        &self,
337        expr: &IfThen,
338        input_schema: &DFSchema,
339    ) -> datafusion::common::Result<Expr> {
340        from_if_then(self, expr, input_schema).await
341    }
342
343    async fn consume_switch(
344        &self,
345        _expr: &SwitchExpression,
346        _input_schema: &DFSchema,
347    ) -> datafusion::common::Result<Expr> {
348        not_impl_err!("Switch expression not supported")
349    }
350
351    async fn consume_singular_or_list(
352        &self,
353        expr: &SingularOrList,
354        input_schema: &DFSchema,
355    ) -> datafusion::common::Result<Expr> {
356        from_singular_or_list(self, expr, input_schema).await
357    }
358
359    async fn consume_multi_or_list(
360        &self,
361        _expr: &MultiOrList,
362        _input_schema: &DFSchema,
363    ) -> datafusion::common::Result<Expr> {
364        not_impl_err!("Multi Or List expression not supported")
365    }
366
367    async fn consume_cast(
368        &self,
369        expr: &substrait_expression::Cast,
370        input_schema: &DFSchema,
371    ) -> datafusion::common::Result<Expr> {
372        from_cast(self, expr, input_schema).await
373    }
374
375    async fn consume_subquery(
376        &self,
377        expr: &substrait_expression::Subquery,
378        input_schema: &DFSchema,
379    ) -> datafusion::common::Result<Expr> {
380        from_subquery(self, expr, input_schema).await
381    }
382
383    async fn consume_nested(
384        &self,
385        expr: &Nested,
386        input_schema: &DFSchema,
387    ) -> datafusion::common::Result<Expr> {
388        from_nested(self, expr, input_schema).await
389    }
390
391    async fn consume_enum(
392        &self,
393        _expr: &Enum,
394        _input_schema: &DFSchema,
395    ) -> datafusion::common::Result<Expr> {
396        not_impl_err!("Enum expression not supported")
397    }
398
399    async fn consume_dynamic_parameter(
400        &self,
401        expr: &DynamicParameter,
402        _input_schema: &DFSchema,
403    ) -> datafusion::common::Result<Expr> {
404        let id = format!("${}", expr.parameter_reference + 1);
405        let field = expr
406            .r#type
407            .as_ref()
408            .map(|t| {
409                super::from_substrait_type_without_names(self, t).map(|dt| {
410                    Arc::new(datafusion::arrow::datatypes::Field::new(&id, dt, true))
411                })
412            })
413            .transpose()?;
414        Ok(Expr::Placeholder(
415            datafusion::logical_expr::expr::Placeholder::new_with_field(id, field),
416        ))
417    }
418
419    async fn consume_lambda(
420        &self,
421        expr: &proto::expression::Lambda,
422        input_schema: &DFSchema,
423    ) -> datafusion::common::Result<Expr> {
424        from_lambda(self, expr, input_schema).await
425    }
426
427    // Outer Schema Stack
428    // These methods manage a stack of outer schemas for correlated subquery support.
429    // When entering a subquery, the enclosing query's schema is pushed onto the stack.
430    // Field references with OuterReference root_type use these to resolve columns.
431
432    /// Push an outer schema onto the stack when entering a subquery.
433    fn push_outer_schema(&self, _schema: Arc<DFSchema>) {}
434
435    /// Pop an outer schema from the stack when leaving a subquery.
436    fn pop_outer_schema(&self) {}
437
438    /// Get the outer schema at the given nesting depth.
439    /// `steps_out = 1` is the immediately enclosing query, `steps_out = 2`
440    /// is two levels out, etc. Returns `None` if `steps_out` is 0 or
441    /// exceeds the current nesting depth (the caller should treat this as
442    /// an error in the Substrait plan).
443    fn get_outer_schema(&self, _steps_out: usize) -> Option<Arc<DFSchema>> {
444        None
445    }
446
447    // User-Defined Functionality
448
449    // The details of extension relations, and how to handle them, are fully up to users to specify.
450    // The following methods allow users to customize the consumer behaviour
451
452    async fn consume_extension_leaf(
453        &self,
454        rel: &ExtensionLeafRel,
455    ) -> datafusion::common::Result<LogicalPlan> {
456        if let Some(detail) = rel.detail.as_ref() {
457            return substrait_err!(
458                "Missing handler for ExtensionLeafRel: {}",
459                detail.type_url
460            );
461        }
462        substrait_err!("Missing handler for ExtensionLeafRel")
463    }
464
465    async fn consume_extension_single(
466        &self,
467        rel: &ExtensionSingleRel,
468    ) -> datafusion::common::Result<LogicalPlan> {
469        if let Some(detail) = rel.detail.as_ref() {
470            return substrait_err!(
471                "Missing handler for ExtensionSingleRel: {}",
472                detail.type_url
473            );
474        }
475        substrait_err!("Missing handler for ExtensionSingleRel")
476    }
477
478    async fn consume_extension_multi(
479        &self,
480        rel: &ExtensionMultiRel,
481    ) -> datafusion::common::Result<LogicalPlan> {
482        if let Some(detail) = rel.detail.as_ref() {
483            return substrait_err!(
484                "Missing handler for ExtensionMultiRel: {}",
485                detail.type_url
486            );
487        }
488        substrait_err!("Missing handler for ExtensionMultiRel")
489    }
490
491    // Users can bring their own types to Substrait which require custom handling
492
493    fn consume_user_defined_type(
494        &self,
495        user_defined_type: &r#type::UserDefined,
496    ) -> datafusion::common::Result<DataType> {
497        substrait_err!(
498            "Missing handler for user-defined type: {}",
499            user_defined_type.type_reference
500        )
501    }
502
503    fn consume_user_defined_literal(
504        &self,
505        user_defined_literal: &proto::expression::literal::UserDefined,
506    ) -> datafusion::common::Result<ScalarValue> {
507        let type_ref = match user_defined_literal.type_anchor_type {
508            Some(
509                proto::expression::literal::user_defined::TypeAnchorType::TypeReference(
510                    ref_val,
511                ),
512            ) => ref_val,
513            Some(
514                proto::expression::literal::user_defined::TypeAnchorType::TypeAliasReference(_),
515            ) => {
516                return not_impl_err!(
517                    "Type alias references in user-defined literals are not yet supported"
518                )
519            }
520            None => 0,
521        };
522        substrait_err!("Missing handler for user-defined literals {}", type_ref)
523    }
524
525    // Lambda related methods
526
527    /// Push the given lambda parameters onto the stack when entering a lambda and
528    /// returns the names they got assigned
529    ///
530    /// Note for custom implementations it's possible to embed a [DefaultSubstraitLambdaConsumer] and forward this method to it
531    fn push_lambda_parameters(
532        &self,
533        _lambda_parameters: &[Type],
534        _input_schema: &DFSchema,
535    ) -> datafusion::common::Result<Vec<String>> {
536        not_impl_err!("SubstraitConsumer::push_lambda_parameters")
537    }
538
539    /// Pop lambda parameters from the stack when leaving a lambda.
540    fn pop_lambda_parameters(&self) {}
541
542    /// Returns an expression corresponding to the lambda variable with the given field_idx within the lambda it originates from,
543    /// at the lambda `step_outs` of the current scope
544    ///
545    /// Note for custom implementations it's possible to embed a [DefaultSubstraitLambdaConsumer] and forward this method to it
546    fn lambda_variable(
547        &self,
548        _steps_out: usize,
549        _field_idx: usize,
550    ) -> datafusion::common::Result<Expr> {
551        not_impl_err!("SubstraitConsumer::lambda_variable")
552    }
553}
554
555/// Default SubstraitConsumer for converting standard Substrait without user-defined extensions.
556///
557/// Used as the consumer in [crate::logical_plan::consumer::from_substrait_plan]
558pub struct DefaultSubstraitConsumer<'a> {
559    pub(super) extensions: &'a Extensions,
560    pub(super) state: &'a SessionState,
561    outer_schemas: RwLock<Vec<Arc<DFSchema>>>,
562    lambda_consumer: DefaultSubstraitLambdaConsumer,
563}
564
565impl<'a> DefaultSubstraitConsumer<'a> {
566    pub fn new(extensions: &'a Extensions, state: &'a SessionState) -> Self {
567        DefaultSubstraitConsumer {
568            extensions,
569            state,
570            outer_schemas: RwLock::new(Vec::new()),
571            lambda_consumer: DefaultSubstraitLambdaConsumer::new(),
572        }
573    }
574}
575
576#[async_trait]
577impl SubstraitConsumer for DefaultSubstraitConsumer<'_> {
578    async fn resolve_table_ref(
579        &self,
580        table_ref: &TableReference,
581    ) -> datafusion::common::Result<Option<Arc<dyn TableProvider>>> {
582        let table = table_ref.table().to_string();
583        let schema = self.state.schema_for_ref(table_ref.clone())?;
584        let table_provider = schema.table(&table).await?;
585        Ok(table_provider)
586    }
587
588    fn get_extensions(&self) -> &Extensions {
589        self.extensions
590    }
591
592    fn get_function_registry(&self) -> &impl FunctionRegistry {
593        self.state
594    }
595
596    fn push_outer_schema(&self, schema: Arc<DFSchema>) {
597        self.outer_schemas.write().unwrap().push(schema);
598    }
599
600    fn pop_outer_schema(&self) {
601        self.outer_schemas.write().unwrap().pop();
602    }
603
604    fn get_outer_schema(&self, steps_out: usize) -> Option<Arc<DFSchema>> {
605        let schemas = self.outer_schemas.read().unwrap();
606        // steps_out=1 → last element, steps_out=2 → second-to-last, etc.
607        // Returns None for steps_out=0 or steps_out > stack depth.
608        schemas
609            .len()
610            .checked_sub(steps_out)
611            .and_then(|idx| schemas.get(idx).cloned())
612    }
613
614    async fn consume_extension_leaf(
615        &self,
616        rel: &ExtensionLeafRel,
617    ) -> datafusion::common::Result<LogicalPlan> {
618        let Some(ext_detail) = &rel.detail else {
619            return substrait_err!("Unexpected empty detail in ExtensionLeafRel");
620        };
621        let plan = self
622            .state
623            .serializer_registry()
624            .deserialize_logical_plan(&ext_detail.type_url, &ext_detail.value)?;
625        Ok(LogicalPlan::Extension(Extension { node: plan }))
626    }
627
628    async fn consume_extension_single(
629        &self,
630        rel: &ExtensionSingleRel,
631    ) -> datafusion::common::Result<LogicalPlan> {
632        let Some(ext_detail) = &rel.detail else {
633            return substrait_err!("Unexpected empty detail in ExtensionSingleRel");
634        };
635        let plan = self
636            .state
637            .serializer_registry()
638            .deserialize_logical_plan(&ext_detail.type_url, &ext_detail.value)?;
639        let Some(input_rel) = &rel.input else {
640            return substrait_err!(
641                "ExtensionSingleRel missing input rel, try using ExtensionLeafRel instead"
642            );
643        };
644        let input_plan = self.consume_rel(input_rel).await?;
645        let plan = plan.with_exprs_and_inputs(plan.expressions(), vec![input_plan])?;
646        Ok(LogicalPlan::Extension(Extension { node: plan }))
647    }
648
649    async fn consume_extension_multi(
650        &self,
651        rel: &ExtensionMultiRel,
652    ) -> datafusion::common::Result<LogicalPlan> {
653        let Some(ext_detail) = &rel.detail else {
654            return substrait_err!("Unexpected empty detail in ExtensionMultiRel");
655        };
656        let plan = self
657            .state
658            .serializer_registry()
659            .deserialize_logical_plan(&ext_detail.type_url, &ext_detail.value)?;
660        let mut inputs = Vec::with_capacity(rel.inputs.len());
661        for input in &rel.inputs {
662            let input_plan = self.consume_rel(input).await?;
663            inputs.push(input_plan);
664        }
665        let plan = plan.with_exprs_and_inputs(plan.expressions(), inputs)?;
666        Ok(LogicalPlan::Extension(Extension { node: plan }))
667    }
668
669    fn push_lambda_parameters(
670        &self,
671        lambda_parameters: &[Type],
672        input_schema: &DFSchema,
673    ) -> datafusion::common::Result<Vec<String>> {
674        self.lambda_consumer
675            .push_lambda_parameters(self, lambda_parameters, input_schema)
676    }
677
678    fn pop_lambda_parameters(&self) {
679        self.lambda_consumer.pop_lambda_parameters()
680    }
681
682    fn lambda_variable(
683        &self,
684        steps_out: usize,
685        field_idx: usize,
686    ) -> datafusion::common::Result<Expr> {
687        self.lambda_consumer.lambda_variable(steps_out, field_idx)
688    }
689}
690
691/// Default implementation of lambda related methods of the [SubstraitConsumer] trait
692///
693/// Can be embedded into a custom [SubstraitConsumer] to implement them
694pub struct DefaultSubstraitLambdaConsumer {
695    inner: RwLock<DefaultSubstraitLambdaConsumerInner>,
696}
697
698struct DefaultSubstraitLambdaConsumerInner {
699    /// Parameters of the lambdas currently in scope, ordered from innermost
700    /// to outermost. Index 0 is the lambda being consumed; higher indices
701    /// are enclosing lambdas, matching the `steps_out` value used by
702    /// [`DefaultSubstraitLambdaConsumer::lambda_variable`] and `LambdaParameterReference`.
703    lambda_parameters: VecDeque<Vec<FieldRef>>,
704    next_lambda_parameter: usize,
705}
706
707impl Default for DefaultSubstraitLambdaConsumer {
708    fn default() -> Self {
709        Self::new()
710    }
711}
712
713impl DefaultSubstraitLambdaConsumer {
714    pub fn new() -> Self {
715        Self {
716            inner: RwLock::new(DefaultSubstraitLambdaConsumerInner {
717                lambda_parameters: VecDeque::new(),
718                next_lambda_parameter: 0,
719            }),
720        }
721    }
722
723    pub fn push_lambda_parameters(
724        &self,
725        consumer: &impl SubstraitConsumer,
726        lambda_parameters: &[Type],
727        input_schema: &DFSchema,
728    ) -> datafusion::common::Result<Vec<String>> {
729        let mut inner = self.inner.write().unwrap();
730
731        let lambda_parameters = lambda_parameters
732            .iter()
733            .map(|ty| {
734                let (assigned_number, default_name) =
735                    next_lambda_parameter_name(inner.next_lambda_parameter, input_schema);
736
737                inner.next_lambda_parameter = assigned_number + 1;
738
739                Ok(field_from_substrait_type_without_names(consumer, ty)?
740                    .renamed(&default_name))
741            })
742            .collect::<datafusion::common::Result<Vec<_>>>()?;
743
744        let names = lambda_parameters.iter().map(|f| f.name().clone()).collect();
745
746        inner.lambda_parameters.push_front(lambda_parameters);
747
748        Ok(names)
749    }
750
751    pub fn pop_lambda_parameters(&self) {
752        self.inner.write().unwrap().lambda_parameters.pop_front();
753    }
754
755    pub fn lambda_variable(
756        &self,
757        steps_out: usize,
758        field_idx: usize,
759    ) -> datafusion::common::Result<Expr> {
760        let lambda_parameters = &self.inner.read().unwrap().lambda_parameters;
761
762        let Some(lambda_parameters) = lambda_parameters.get(steps_out) else {
763            return substrait_err!(
764                "No lambda at {steps_out} steps out, got only {}",
765                lambda_parameters.len()
766            );
767        };
768
769        let Some(var) = lambda_parameters.get(field_idx) else {
770            return substrait_err!(
771                "At lambda {steps_out} steps out, no field at index {field_idx}, got only {}",
772                lambda_parameters.len()
773            );
774        };
775
776        Ok(Expr::LambdaVariable(LambdaVariable::new(
777            var.name().clone(),
778            Some(Arc::clone(var)),
779        )))
780    }
781}
782
783/// Returns the next available lambda parameter name and the index it was assigned.
784///
785/// Names follow the pattern `pN` where `N` starts at `next_lambda_parameter`. If `pN`
786/// conflicts with an existing column name in `input_schema`, `N` is incremented until
787/// a free name is found.
788fn next_lambda_parameter_name(
789    mut next_lambda_parameter: usize,
790    input_schema: &DFSchema,
791) -> (usize, String) {
792    loop {
793        let name = format!("p{next_lambda_parameter}");
794
795        // avoid conflicts with column names
796        if !input_schema.has_column_with_unqualified_name(&name) {
797            return (next_lambda_parameter, name);
798        }
799
800        next_lambda_parameter += 1;
801    }
802}
803
804#[cfg(test)]
805mod tests {
806    use super::*;
807    use crate::logical_plan::consumer::utils::tests::test_consumer;
808    use datafusion::arrow::datatypes::{Field, Schema};
809
810    fn make_schema(fields: &[(&str, DataType)]) -> Arc<DFSchema> {
811        let arrow_fields: Vec<Field> = fields
812            .iter()
813            .map(|(name, dt)| Field::new(*name, dt.clone(), true))
814            .collect();
815        Arc::new(
816            DFSchema::try_from(Schema::new(arrow_fields))
817                .expect("failed to create schema"),
818        )
819    }
820
821    #[test]
822    fn test_get_outer_schema_empty_stack() {
823        let consumer = test_consumer();
824
825        // No schemas pushed — any steps_out should return None
826        assert!(consumer.get_outer_schema(0).is_none());
827        assert!(consumer.get_outer_schema(1).is_none());
828        assert!(consumer.get_outer_schema(2).is_none());
829    }
830
831    #[test]
832    fn test_get_outer_schema_single_level() {
833        let consumer = test_consumer();
834
835        let schema_a = make_schema(&[("a", DataType::Int64)]);
836        consumer.push_outer_schema(Arc::clone(&schema_a));
837
838        // steps_out=1 returns the one pushed schema
839        let result = consumer.get_outer_schema(1).unwrap();
840        assert_eq!(result.fields().len(), 1);
841        assert_eq!(result.fields()[0].name(), "a");
842
843        // steps_out=0 and steps_out=2 are out of range
844        assert!(consumer.get_outer_schema(0).is_none());
845        assert!(consumer.get_outer_schema(2).is_none());
846
847        consumer.pop_outer_schema();
848        assert!(consumer.get_outer_schema(1).is_none());
849    }
850
851    #[test]
852    fn test_get_outer_schema_nested() {
853        let consumer = test_consumer();
854
855        let schema_a = make_schema(&[("a", DataType::Int64)]);
856        let schema_b = make_schema(&[("b", DataType::Utf8)]);
857
858        consumer.push_outer_schema(Arc::clone(&schema_a));
859        consumer.push_outer_schema(Arc::clone(&schema_b));
860
861        // steps_out=1 returns the most recent (schema_b)
862        let result = consumer.get_outer_schema(1).unwrap();
863        assert_eq!(result.fields()[0].name(), "b");
864
865        // steps_out=2 returns the grandparent (schema_a)
866        let result = consumer.get_outer_schema(2).unwrap();
867        assert_eq!(result.fields()[0].name(), "a");
868
869        // steps_out=3 exceeds depth
870        assert!(consumer.get_outer_schema(3).is_none());
871
872        // Pop one level — now steps_out=1 returns schema_a
873        consumer.pop_outer_schema();
874        let result = consumer.get_outer_schema(1).unwrap();
875        assert_eq!(result.fields()[0].name(), "a");
876        assert!(consumer.get_outer_schema(2).is_none());
877    }
878}