Skip to main content

datafusion_physical_expr/
planner.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 std::sync::Arc;
19
20use crate::scalar_subquery::ScalarSubqueryExpr;
21use crate::{HigherOrderFunctionExpr, ScalarFunctionExpr};
22use crate::{
23    PhysicalExpr,
24    expressions::{self, Column, Literal, binary, like, similar_to},
25};
26
27use arrow::datatypes::Schema;
28use datafusion_common::config::ConfigOptions;
29use datafusion_common::datatype::FieldExt;
30use datafusion_common::metadata::FieldMetadata;
31use datafusion_common::{
32    DFSchema, Result, ScalarValue, TableReference, ToDFSchema, exec_err,
33    internal_datafusion_err, not_impl_err, plan_datafusion_err, plan_err,
34};
35use datafusion_expr::execution_props::ExecutionProps;
36use datafusion_expr::expr::{
37    Alias, Cast, HigherOrderFunction, InList, Lambda, LambdaVariable, Placeholder,
38    ScalarFunction,
39};
40use datafusion_expr::physical_planning_context::PhysicalPlanningContext;
41use datafusion_expr::var_provider::VarType;
42use datafusion_expr::var_provider::is_system_variables;
43use datafusion_expr::{
44    Between, BinaryExpr, Expr, Like, Operator, TryCast, binary_expr, lit,
45};
46
47/// [PhysicalExpr] evaluate DataFusion expressions such as `A + 1`, or `CAST(c1
48/// AS int)`.
49///
50/// [PhysicalExpr] are the physical counterpart to [Expr] used in logical
51/// planning, and can be evaluated directly on a [RecordBatch]. They are
52/// normally created from [Expr] by a [PhysicalPlanner] and can be created
53/// directly using [create_physical_expr].
54///
55/// A Physical expression knows its type, nullability and how to evaluate itself.
56///
57/// [PhysicalPlanner]: https://docs.rs/datafusion/latest/datafusion/physical_planner/trait.PhysicalPlanner.html
58/// [RecordBatch]: https://docs.rs/arrow/latest/arrow/record_batch/struct.RecordBatch.html
59///
60/// # Example: Create `PhysicalExpr` from `Expr`
61/// ```
62/// # use arrow::datatypes::{DataType, Field, Schema};
63/// # use datafusion_common::DFSchema;
64/// # use datafusion_expr::{Expr, col, lit};
65/// # use datafusion_physical_expr::create_physical_expr;
66/// # use datafusion_expr::execution_props::ExecutionProps;
67/// # use datafusion_expr::physical_planning_context::PhysicalPlanningContext;
68/// // For a logical expression `a = 1`, we can create a physical expression
69/// let expr = col("a").eq(lit(1));
70/// // To create a PhysicalExpr we need 1. a schema
71/// let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
72/// let df_schema = DFSchema::try_from(schema).unwrap();
73/// // 2. ExecutionProps
74/// let props = ExecutionProps::new();
75/// // We can now create a PhysicalExpr. Expressions with no scalar
76/// // subqueries use an empty `PhysicalPlanningContext`:
77/// let physical_expr =
78///     create_physical_expr(&expr, &df_schema, &props, &PhysicalPlanningContext::default())
79///         .unwrap();
80/// ```
81///
82/// # Example: Executing a PhysicalExpr to obtain [ColumnarValue]
83/// ```
84/// # use std::sync::Arc;
85/// # use arrow::array::{cast::AsArray, BooleanArray, Int32Array, RecordBatch};
86/// # use arrow::datatypes::{DataType, Field, Schema};
87/// # use datafusion_common::{assert_batches_eq, DFSchema};
88/// # use datafusion_expr::{Expr, col, lit, ColumnarValue};
89/// # use datafusion_physical_expr::create_physical_expr;
90/// # use datafusion_expr::execution_props::ExecutionProps;
91/// # use datafusion_expr::physical_planning_context::PhysicalPlanningContext;
92/// # let expr = col("a").eq(lit(1));
93/// # let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
94/// # let df_schema = DFSchema::try_from(schema.clone()).unwrap();
95/// # let props = ExecutionProps::new();
96/// // Given a PhysicalExpr, for `a = 1` we can evaluate it against a RecordBatch like this:
97/// let physical_expr =
98///     create_physical_expr(&expr, &df_schema, &props, &PhysicalPlanningContext::default())
99///         .unwrap();
100/// // Input of [1,2,3]
101/// let input_batch = RecordBatch::try_from_iter(vec![
102///   ("a", Arc::new(Int32Array::from(vec![1, 2, 3])) as _)
103/// ]).unwrap();
104/// // The result is a ColumnarValue (either an Array or a Scalar)
105/// let result = physical_expr.evaluate(&input_batch).unwrap();
106/// // In this case, a BooleanArray with the result of the comparison
107/// let ColumnarValue::Array(arr) = result else {
108///  panic!("Expected an array")
109/// };
110/// assert_eq!(arr.as_boolean(), &BooleanArray::from(vec![true, false, false]));
111/// ```
112///
113/// [ColumnarValue]: datafusion_expr::ColumnarValue
114///
115/// Create a physical expression from a logical expression ([Expr]).
116///
117/// # Arguments
118///
119/// * `e` - The logical expression
120/// * `input_dfschema` - The DataFusion schema for the input, used to resolve `Column` references
121///   to qualified or unqualified fields by name.
122/// * `execution_props` - Per-execution properties such as the query start time.
123/// * `planning_ctx` - The [`PhysicalPlanningContext`] used to resolve
124///   `Expr::ScalarSubquery` and `Expr::LambdaVariable` nodes. The physical
125///   planner threads the subquery index map and shared results container from
126///   its `ScalarSubqueryExec` construction into calls to
127///   `create_physical_expr`; the lambda variable qualifiers are added by this
128///   function itself as it descends into lambda bodies. Callers creating
129///   physical expressions outside of physical planning should pass
130///   `&PhysicalPlanningContext::default()`; converting a scalar subquery then returns a
131///   planning error.
132#[cfg_attr(feature = "recursive_protection", recursive::recursive)]
133pub fn create_physical_expr(
134    e: &Expr,
135    input_dfschema: &DFSchema,
136    execution_props: &ExecutionProps,
137    planning_ctx: &PhysicalPlanningContext,
138) -> Result<Arc<dyn PhysicalExpr>> {
139    let input_schema = input_dfschema.as_arrow();
140
141    match e {
142        Expr::Alias(Alias { expr, metadata, .. }) => {
143            if let Expr::Literal(v, prior_metadata) = expr.as_ref() {
144                let new_metadata = FieldMetadata::merge_options(
145                    prior_metadata.as_ref(),
146                    metadata.as_ref(),
147                );
148                Ok(Arc::new(Literal::new_with_metadata(
149                    v.clone(),
150                    new_metadata,
151                )))
152            } else {
153                Ok(create_physical_expr(
154                    expr,
155                    input_dfschema,
156                    execution_props,
157                    planning_ctx,
158                )?)
159            }
160        }
161        Expr::Column(c) => {
162            let idx = input_dfschema.index_of_column(c)?;
163            Ok(Arc::new(Column::new(&c.name, idx)))
164        }
165        Expr::Literal(value, metadata) => Ok(Arc::new(Literal::new_with_metadata(
166            value.clone(),
167            metadata.clone(),
168        ))),
169        Expr::ScalarVariable(_, variable_names) => {
170            if is_system_variables(variable_names) {
171                match execution_props.get_var_provider(VarType::System) {
172                    Some(provider) => {
173                        let scalar_value = provider.get_value(variable_names.clone())?;
174                        Ok(Arc::new(Literal::new(scalar_value)))
175                    }
176                    _ => plan_err!("No system variable provider found"),
177                }
178            } else {
179                match execution_props.get_var_provider(VarType::UserDefined) {
180                    Some(provider) => {
181                        let scalar_value = provider.get_value(variable_names.clone())?;
182                        Ok(Arc::new(Literal::new(scalar_value)))
183                    }
184                    _ => plan_err!("No user defined variable provider found"),
185                }
186            }
187        }
188        Expr::IsTrue(expr) => {
189            let binary_op = binary_expr(
190                expr.as_ref().clone(),
191                Operator::IsNotDistinctFrom,
192                lit(true),
193            );
194            create_physical_expr(
195                &binary_op,
196                input_dfschema,
197                execution_props,
198                planning_ctx,
199            )
200        }
201        Expr::IsNotTrue(expr) => {
202            let binary_op =
203                binary_expr(expr.as_ref().clone(), Operator::IsDistinctFrom, lit(true));
204            create_physical_expr(
205                &binary_op,
206                input_dfschema,
207                execution_props,
208                planning_ctx,
209            )
210        }
211        Expr::IsFalse(expr) => {
212            let binary_op = binary_expr(
213                expr.as_ref().clone(),
214                Operator::IsNotDistinctFrom,
215                lit(false),
216            );
217            create_physical_expr(
218                &binary_op,
219                input_dfschema,
220                execution_props,
221                planning_ctx,
222            )
223        }
224        Expr::IsNotFalse(expr) => {
225            let binary_op =
226                binary_expr(expr.as_ref().clone(), Operator::IsDistinctFrom, lit(false));
227            create_physical_expr(
228                &binary_op,
229                input_dfschema,
230                execution_props,
231                planning_ctx,
232            )
233        }
234        Expr::IsUnknown(expr) => {
235            let binary_op = binary_expr(
236                expr.as_ref().clone(),
237                Operator::IsNotDistinctFrom,
238                Expr::Literal(ScalarValue::Boolean(None), None),
239            );
240            create_physical_expr(
241                &binary_op,
242                input_dfschema,
243                execution_props,
244                planning_ctx,
245            )
246        }
247        Expr::IsNotUnknown(expr) => {
248            let binary_op = binary_expr(
249                expr.as_ref().clone(),
250                Operator::IsDistinctFrom,
251                Expr::Literal(ScalarValue::Boolean(None), None),
252            );
253            create_physical_expr(
254                &binary_op,
255                input_dfschema,
256                execution_props,
257                planning_ctx,
258            )
259        }
260        Expr::BinaryExpr(BinaryExpr { left, op, right }) => {
261            // Create physical expressions for left and right operands
262            let lhs = create_physical_expr(
263                left,
264                input_dfschema,
265                execution_props,
266                planning_ctx,
267            )?;
268            let rhs = create_physical_expr(
269                right,
270                input_dfschema,
271                execution_props,
272                planning_ctx,
273            )?;
274            // Note that the logical planner is responsible
275            // for type coercion on the arguments (e.g. if one
276            // argument was originally Int32 and one was
277            // Int64 they will both be coerced to Int64).
278            //
279            // There should be no coercion during physical
280            // planning.
281            binary(lhs, *op, rhs, input_schema)
282        }
283        Expr::Like(Like {
284            negated,
285            expr,
286            pattern,
287            escape_char,
288            case_insensitive,
289        }) => {
290            // `\` is the implicit escape, see https://github.com/apache/datafusion/issues/13291
291            if escape_char.unwrap_or('\\') != '\\' {
292                return exec_err!(
293                    "LIKE does not support escape_char other than the backslash (\\)"
294                );
295            }
296            let physical_expr = create_physical_expr(
297                expr,
298                input_dfschema,
299                execution_props,
300                planning_ctx,
301            )?;
302            let physical_pattern = create_physical_expr(
303                pattern,
304                input_dfschema,
305                execution_props,
306                planning_ctx,
307            )?;
308            like(
309                *negated,
310                *case_insensitive,
311                physical_expr,
312                physical_pattern,
313                input_schema,
314            )
315        }
316        Expr::SimilarTo(Like {
317            negated,
318            expr,
319            pattern,
320            escape_char,
321            case_insensitive,
322        }) => {
323            if escape_char.is_some() {
324                return exec_err!("SIMILAR TO does not support escape_char yet");
325            }
326            let physical_expr = create_physical_expr(
327                expr,
328                input_dfschema,
329                execution_props,
330                planning_ctx,
331            )?;
332            let physical_pattern = create_physical_expr(
333                pattern,
334                input_dfschema,
335                execution_props,
336                planning_ctx,
337            )?;
338            similar_to(*negated, *case_insensitive, physical_expr, physical_pattern)
339        }
340        Expr::Case(case) => {
341            let expr: Option<Arc<dyn PhysicalExpr>> = if let Some(e) = &case.expr {
342                Some(create_physical_expr(
343                    e.as_ref(),
344                    input_dfschema,
345                    execution_props,
346                    planning_ctx,
347                )?)
348            } else {
349                None
350            };
351            let (when_expr, then_expr): (Vec<&Expr>, Vec<&Expr>) = case
352                .when_then_expr
353                .iter()
354                .map(|(w, t)| (w.as_ref(), t.as_ref()))
355                .unzip();
356            let when_expr = create_physical_exprs(
357                when_expr,
358                input_dfschema,
359                execution_props,
360                planning_ctx,
361            )?;
362            let then_expr = create_physical_exprs(
363                then_expr,
364                input_dfschema,
365                execution_props,
366                planning_ctx,
367            )?;
368            let when_then_expr: Vec<(Arc<dyn PhysicalExpr>, Arc<dyn PhysicalExpr>)> =
369                when_expr
370                    .iter()
371                    .zip(then_expr.iter())
372                    .map(|(w, t)| (Arc::clone(w), Arc::clone(t)))
373                    .collect();
374            let else_expr: Option<Arc<dyn PhysicalExpr>> =
375                if let Some(e) = &case.else_expr {
376                    Some(create_physical_expr(
377                        e.as_ref(),
378                        input_dfschema,
379                        execution_props,
380                        planning_ctx,
381                    )?)
382                } else {
383                    None
384                };
385            Ok(expressions::case(expr, when_then_expr, else_expr)?)
386        }
387        Expr::Cast(Cast { expr, field }) => expressions::cast_with_target_field(
388            create_physical_expr(expr, input_dfschema, execution_props, planning_ctx)?,
389            input_schema,
390            field,
391            None,
392        ),
393        Expr::TryCast(TryCast { expr, field }) => {
394            expressions::try_cast_with_target_field(
395                create_physical_expr(
396                    expr,
397                    input_dfschema,
398                    execution_props,
399                    planning_ctx,
400                )?,
401                input_schema,
402                field,
403            )
404        }
405        Expr::Not(expr) => expressions::not(create_physical_expr(
406            expr,
407            input_dfschema,
408            execution_props,
409            planning_ctx,
410        )?),
411        Expr::Negative(expr) => expressions::negative(
412            create_physical_expr(expr, input_dfschema, execution_props, planning_ctx)?,
413            input_schema,
414        ),
415        Expr::IsNull(expr) => expressions::is_null(create_physical_expr(
416            expr,
417            input_dfschema,
418            execution_props,
419            planning_ctx,
420        )?),
421        Expr::IsNotNull(expr) => expressions::is_not_null(create_physical_expr(
422            expr,
423            input_dfschema,
424            execution_props,
425            planning_ctx,
426        )?),
427        Expr::ScalarFunction(ScalarFunction { func, args }) => {
428            let physical_args = create_physical_exprs(
429                args,
430                input_dfschema,
431                execution_props,
432                planning_ctx,
433            )?;
434            let config_options = match execution_props.config_options.as_ref() {
435                Some(config_options) => Arc::clone(config_options),
436                None => Arc::new(ConfigOptions::default()),
437            };
438
439            Ok(Arc::new(ScalarFunctionExpr::try_new(
440                Arc::clone(func),
441                physical_args,
442                input_schema,
443                config_options,
444            )?))
445        }
446        Expr::Between(Between {
447            expr,
448            negated,
449            low,
450            high,
451        }) => {
452            let value_expr = create_physical_expr(
453                expr,
454                input_dfschema,
455                execution_props,
456                planning_ctx,
457            )?;
458            let low_expr =
459                create_physical_expr(low, input_dfschema, execution_props, planning_ctx)?;
460            let high_expr = create_physical_expr(
461                high,
462                input_dfschema,
463                execution_props,
464                planning_ctx,
465            )?;
466
467            // rewrite the between into the two binary operators
468            let binary_expr = binary(
469                binary(
470                    Arc::clone(&value_expr),
471                    Operator::GtEq,
472                    low_expr,
473                    input_schema,
474                )?,
475                Operator::And,
476                binary(
477                    Arc::clone(&value_expr),
478                    Operator::LtEq,
479                    high_expr,
480                    input_schema,
481                )?,
482                input_schema,
483            );
484
485            if *negated {
486                expressions::not(binary_expr?)
487            } else {
488                binary_expr
489            }
490        }
491        Expr::InList(InList {
492            expr,
493            list,
494            negated,
495        }) => match expr.as_ref() {
496            Expr::Literal(ScalarValue::Utf8(None), _) => {
497                Ok(expressions::lit(ScalarValue::Boolean(None)))
498            }
499            _ => {
500                let value_expr = create_physical_expr(
501                    expr,
502                    input_dfschema,
503                    execution_props,
504                    planning_ctx,
505                )?;
506
507                let list_exprs = create_physical_exprs(
508                    list,
509                    input_dfschema,
510                    execution_props,
511                    planning_ctx,
512                )?;
513                expressions::in_list(value_expr, list_exprs, negated, input_schema)
514            }
515        },
516        Expr::ScalarSubquery(sq) => {
517            match planning_ctx.index_of(sq) {
518                Some(index) => {
519                    let schema = sq.subquery.schema();
520                    if schema.fields().len() != 1 {
521                        return plan_err!(
522                            "Scalar subquery must return exactly one column, got {}",
523                            schema.fields().len()
524                        );
525                    }
526                    let dt = schema.field(0).data_type().clone();
527                    let nullable = schema.field(0).is_nullable();
528                    Ok(Arc::new(ScalarSubqueryExpr::new(
529                        dt,
530                        nullable,
531                        index,
532                        planning_ctx.results().clone(),
533                    )))
534                }
535                None => {
536                    // Not found: either a correlated subquery that wasn't
537                    // rewritten to a join, or an uncorrelated one that wasn't
538                    // registered by the physical planner.
539                    not_impl_err!(
540                        "Physical plan does not support logical expression {e:?}"
541                    )
542                }
543            }
544        }
545        Expr::Placeholder(Placeholder { id, .. }) => {
546            exec_err!("Placeholder '{id}' was not provided a value for execution.")
547        }
548        Expr::HigherOrderFunction(invocation @ HigherOrderFunction { func, args }) => {
549            let num_lambdas = args
550                .iter()
551                .filter(|arg| matches!(arg, Expr::Lambda(_)))
552                .count();
553
554            let mut lambda_parameters =
555                invocation.lambda_parameters(input_dfschema)?.into_iter();
556
557            if num_lambdas > lambda_parameters.len() {
558                return plan_err!(
559                    "{} lambda_parameters returned only {} values for {num_lambdas} lambdas",
560                    func.name(),
561                    lambda_parameters.len()
562                );
563            }
564
565            let lambda_qualifier = 1 + input_dfschema
566                .iter()
567                .filter_map(|(qualifier, _field)| {
568                    qualifier.and_then(|tbl| {
569                        tbl.table().strip_prefix("lambda_")?.parse::<usize>().ok()
570                    })
571                })
572                .max()
573                .unwrap_or_default();
574
575            let qualifier = TableReference::bare(format!("lambda_{lambda_qualifier}"));
576
577            let physical_args = args
578                .iter()
579                .map(|arg| match arg {
580                    Expr::Lambda(lambda) => {
581                        let lambda_parameters = lambda_parameters
582                            .next()
583                            .ok_or_else(|| {
584                                internal_datafusion_err!(
585                                    "lambda_parameters len should have been checked above"
586                                )
587                            })?
588                            .into_iter()
589                            .zip(&lambda.params)
590                            .map(|(field, name)| {
591                                (Some(qualifier.clone()), field.renamed(name.as_str()))
592                            });
593
594                        let new_fields = input_dfschema
595                            .iter()
596                            .map(|(tbl, field)| (tbl.cloned(), Arc::clone(field)))
597                            .chain(lambda_parameters)
598                            .collect();
599
600                        let lambda_schema = DFSchema::new_with_metadata(
601                            new_fields,
602                            input_dfschema.metadata().clone(),
603                        )?;
604
605                        let planning_ctx = planning_ctx
606                            .clone()
607                            .with_qualified_lambda_variables(&qualifier, &lambda.params);
608
609                        create_physical_expr(
610                            arg,
611                            &lambda_schema,
612                            execution_props,
613                            &planning_ctx,
614                        )
615                    }
616                    _ => create_physical_expr(
617                        arg,
618                        input_dfschema,
619                        execution_props,
620                        planning_ctx,
621                    ),
622                })
623                .collect::<Result<_>>()?;
624
625            let config_options = match execution_props.config_options.as_ref() {
626                Some(config_options) => Arc::clone(config_options),
627                None => Arc::new(ConfigOptions::default()),
628            };
629
630            Ok(Arc::new(HigherOrderFunctionExpr::try_new_with_schema(
631                Arc::clone(func),
632                physical_args,
633                input_schema,
634                config_options,
635            )?))
636        }
637        Expr::Lambda(Lambda { params, body }) => expressions::lambda(
638            params,
639            create_physical_expr(body, input_dfschema, execution_props, planning_ctx)?,
640        ),
641        Expr::LambdaVariable(LambdaVariable {
642            name,
643            field,
644            spans: _,
645        }) => {
646            let field = field.as_ref().ok_or_else(|| {
647                plan_datafusion_err!("unresolved LambdaVariable {name}")
648            })?;
649
650            let qualifier =
651                planning_ctx
652                    .lambda_variable_qualifier(name)
653                    .ok_or_else(|| {
654                        plan_datafusion_err!(
655                            "qualifier for lambda variable {name} not found"
656                        )
657                    })?;
658
659            let index = input_dfschema
660                .index_of_column_by_name(Some(qualifier), name)
661                .ok_or_else(|| {
662                    plan_datafusion_err!(
663                        "lambda variable {qualifier}.{name} not found in planning schema"
664                    )
665                })?;
666
667            let schema_field = input_dfschema.field(index);
668
669            // LambdaVariable.field will be made optional as in Expr::Placeholder
670            // and only LambdaVariable.name used, and field.name ignored,
671            // so they're not enforced to match for logical expressions
672            // Rename the field to match the schema one and use it's PartialEq impl instead
673            // of checking property by property and fail if new properties get's added to it.
674            // While not necessary, the sql planner does create lambda vars with matching names,
675            // so this shouldn't allocate with a lambda var from it
676            let renamed_field = Arc::clone(field).renamed(name);
677
678            if &renamed_field != schema_field {
679                return plan_err!(
680                    "LambdaVariable field and schema field mismatch {} != {}",
681                    renamed_field,
682                    schema_field
683                );
684            }
685
686            Ok(Arc::new(expressions::LambdaVariable::new(
687                index,
688                Arc::clone(schema_field),
689            )))
690        }
691        other => {
692            not_impl_err!("Physical plan does not support logical expression {other:?}")
693        }
694    }
695}
696
697/// Create vector of Physical Expression from a vector of logical expression
698///
699/// See [`create_physical_expr`] for details on the `planning_ctx` argument.
700pub fn create_physical_exprs<'a, I>(
701    exprs: I,
702    input_dfschema: &DFSchema,
703    execution_props: &ExecutionProps,
704    planning_ctx: &PhysicalPlanningContext,
705) -> Result<Vec<Arc<dyn PhysicalExpr>>>
706where
707    I: IntoIterator<Item = &'a Expr>,
708{
709    exprs
710        .into_iter()
711        .map(|expr| {
712            create_physical_expr(expr, input_dfschema, execution_props, planning_ctx)
713        })
714        .collect()
715}
716
717/// Convert a logical expression to a physical expression (without any simplification, etc)
718pub fn logical2physical(expr: &Expr, schema: &Schema) -> Arc<dyn PhysicalExpr> {
719    // TODO this makes a deep copy of the Schema. Should take SchemaRef instead and avoid deep copy
720    let df_schema = schema.clone().to_dfschema().unwrap();
721    let execution_props = ExecutionProps::new();
722    create_physical_expr(
723        expr,
724        &df_schema,
725        &execution_props,
726        &PhysicalPlanningContext::default(),
727    )
728    .unwrap()
729}
730
731#[cfg(test)]
732mod tests {
733    use arrow::array::{ArrayRef, BooleanArray, RecordBatch, StringArray};
734    use arrow::datatypes::{DataType, Field};
735    use arrow_schema::extension::{EXTENSION_TYPE_METADATA_KEY, EXTENSION_TYPE_NAME_KEY};
736    use datafusion_expr::col;
737
738    use super::*;
739
740    fn test_cast_schema() -> Schema {
741        Schema::new(vec![Field::new("a", DataType::Int32, false)])
742    }
743
744    fn lower_cast_expr(expr: &Expr, schema: &Schema) -> Result<Arc<dyn PhysicalExpr>> {
745        let df_schema = DFSchema::try_from(schema.clone())?;
746        create_physical_expr(
747            expr,
748            &df_schema,
749            &ExecutionProps::new(),
750            &PhysicalPlanningContext::default(),
751        )
752    }
753
754    fn as_planner_cast(physical: &Arc<dyn PhysicalExpr>) -> &expressions::CastExpr {
755        physical
756            .downcast_ref::<expressions::CastExpr>()
757            .expect("planner should lower logical CAST to CastExpr")
758    }
759
760    fn as_planner_try_cast(
761        physical: &Arc<dyn PhysicalExpr>,
762    ) -> &expressions::TryCastExpr {
763        physical
764            .downcast_ref::<expressions::TryCastExpr>()
765            .expect("planner should lower logical TRY_CAST to TryCastExpr")
766    }
767
768    #[test]
769    fn test_create_physical_expr_scalar_input_output() -> Result<()> {
770        let expr = col("letter").eq(lit("A"));
771
772        let schema = Schema::new(vec![Field::new("letter", DataType::Utf8, false)]);
773        let df_schema = DFSchema::try_from_qualified_schema("data", &schema)?;
774        let p = create_physical_expr(
775            &expr,
776            &df_schema,
777            &ExecutionProps::new(),
778            &PhysicalPlanningContext::default(),
779        )?;
780
781        let batch = RecordBatch::try_new(
782            Arc::new(schema),
783            vec![Arc::new(StringArray::from_iter_values(vec![
784                "A", "B", "C", "D",
785            ]))],
786        )?;
787        let result = p.evaluate(&batch)?;
788        let result = result.into_array(4).expect("Failed to convert to array");
789
790        assert_eq!(
791            &result,
792            &(Arc::new(BooleanArray::from(vec![true, false, false, false,])) as ArrayRef)
793        );
794
795        Ok(())
796    }
797
798    #[test]
799    fn test_cast_lowering_preserves_target_field_metadata() -> Result<()> {
800        let schema = test_cast_schema();
801
802        // Target field with both extension metadata and custom metadata.
803        // With exact target metadata semantics, all target metadata should propagate.
804        let target_field = Arc::new(
805            Field::new("cast_target", DataType::Int64, true).with_metadata(
806                [
807                    (
808                        EXTENSION_TYPE_NAME_KEY.to_string(),
809                        "arrow.json".to_string(),
810                    ),
811                    (EXTENSION_TYPE_METADATA_KEY.to_string(), "{}".to_string()),
812                    ("custom_target_meta".to_string(), "custom_value".to_string()),
813                ]
814                .into(),
815            ),
816        );
817        let cast_expr = Expr::Cast(Cast::new_from_field(
818            Box::new(col("a")),
819            Arc::clone(&target_field),
820        ));
821
822        let physical = lower_cast_expr(&cast_expr, &schema)?;
823        let cast = as_planner_cast(&physical);
824
825        // The CastExpr stores the target type and all target metadata
826        assert_eq!(cast.cast_type(), &DataType::Int64);
827        let target_metadata = cast.target_metadata().expect("should have metadata");
828        assert_eq!(
829            target_metadata.get(EXTENSION_TYPE_NAME_KEY),
830            Some(&"arrow.json".to_string())
831        );
832        assert_eq!(
833            target_metadata.get(EXTENSION_TYPE_METADATA_KEY),
834            Some(&"{}".to_string())
835        );
836        assert_eq!(cast.target_nullable(), Some(true));
837
838        // return_field should have all target metadata (exact semantics)
839        let returned = physical.return_field(&schema)?;
840        assert_eq!(
841            returned.metadata().get(EXTENSION_TYPE_NAME_KEY),
842            Some(&"arrow.json".to_string())
843        );
844        assert_eq!(
845            returned.metadata().get(EXTENSION_TYPE_METADATA_KEY),
846            Some(&"{}".to_string())
847        );
848        // All target metadata should propagate with exact semantics
849        assert_eq!(
850            returned.metadata().get("custom_target_meta"),
851            Some(&"custom_value".to_string()),
852            "All target metadata should propagate with exact semantics"
853        );
854        assert!(physical.nullable(&schema)?);
855
856        Ok(())
857    }
858
859    #[test]
860    fn test_cast_lowering_preserves_standard_cast_semantics() -> Result<()> {
861        let schema = test_cast_schema();
862        let cast_expr = Expr::Cast(Cast::new(Box::new(col("a")), DataType::Int64));
863
864        let physical = lower_cast_expr(&cast_expr, &schema)?;
865        let cast = as_planner_cast(&physical);
866        let returned_field = physical.return_field(&schema)?;
867
868        assert_eq!(cast.cast_type(), &DataType::Int64);
869        assert_eq!(returned_field.name(), "a");
870        assert_eq!(returned_field.data_type(), &DataType::Int64);
871        assert!(!physical.nullable(&schema)?);
872
873        Ok(())
874    }
875
876    #[test]
877    fn test_cast_lowering_preserves_same_type_field_semantics() -> Result<()> {
878        let schema = test_cast_schema();
879
880        // Same-type cast with extension metadata on target.
881        // With exact target metadata semantics, all target metadata should propagate.
882        let target_field = Arc::new(
883            Field::new("same_type_cast", DataType::Int32, true).with_metadata(
884                [
885                    (
886                        EXTENSION_TYPE_NAME_KEY.to_string(),
887                        "arrow.opaque".to_string(),
888                    ),
889                    ("custom_meta".to_string(), "custom_value".to_string()),
890                ]
891                .into(),
892            ),
893        );
894
895        for use_try_cast in [false, true] {
896            // For error labelling
897            let cast_name = if use_try_cast { "TRY_CAST" } else { "CAST" };
898
899            let cast_expr = if use_try_cast {
900                Expr::TryCast(TryCast::new_from_field(
901                    Box::new(col("a")),
902                    Arc::clone(&target_field),
903                ))
904            } else {
905                Expr::Cast(Cast::new_from_field(
906                    Box::new(col("a")),
907                    Arc::clone(&target_field),
908                ))
909            };
910
911            let physical = lower_cast_expr(&cast_expr, &schema)?;
912
913            // Extract common fields - both CastExpr and TryCastExpr have these
914            let (cast_type, target_metadata, target_nullable) = if use_try_cast {
915                let cast = as_planner_try_cast(&physical);
916                (cast.cast_type(), cast.target_metadata(), None)
917            } else {
918                let cast = as_planner_cast(&physical);
919                (
920                    cast.cast_type(),
921                    cast.target_metadata(),
922                    cast.target_nullable(),
923                )
924            };
925
926            // Verify the physical expression stores correct metadata (same for both)
927            assert_eq!(cast_type, &DataType::Int32, "{cast_name}: cast_type");
928            let target_metadata = target_metadata.expect("should have metadata");
929            assert_eq!(
930                target_metadata.get(EXTENSION_TYPE_NAME_KEY),
931                Some(&"arrow.opaque".to_string()),
932                "{cast_name}: extension type name"
933            );
934
935            // Only CastExpr tracks target_nullable (TryCast is always nullable)
936            if !use_try_cast {
937                assert_eq!(target_nullable, Some(true), "{cast_name}: target_nullable");
938            }
939
940            // return_field should have all target metadata (exact semantics)
941            let returned = physical.return_field(&schema)?;
942            assert_eq!(
943                returned.metadata().get(EXTENSION_TYPE_NAME_KEY),
944                Some(&"arrow.opaque".to_string()),
945                "{cast_name}: return_field extension type name"
946            );
947            // All target metadata should propagate with exact semantics
948            assert_eq!(
949                returned.metadata().get("custom_meta"),
950                Some(&"custom_value".to_string()),
951                "{cast_name}: All target metadata should propagate with exact semantics"
952            );
953            assert!(
954                physical.nullable(&schema)?,
955                "{cast_name}: should be nullable"
956            );
957        }
958
959        Ok(())
960    }
961
962    /// Test that deeply nested expressions do not cause a stack overflow.
963    ///
964    /// This test only runs when the `recursive_protection` feature is enabled,
965    /// as it would overflow the stack otherwise.
966    #[test]
967    #[cfg_attr(not(feature = "recursive_protection"), ignore)]
968    fn test_deeply_nested_binary_expr() -> Result<()> {
969        // Create a deeply nested binary expression tree: ((((a + a) + a) + a) + ... )
970        // With 1000 levels of nesting, this would overflow the stack without recursion protection.
971        let depth = 1000;
972
973        let mut expr = col("a");
974        for _ in 0..depth {
975            expr = Expr::BinaryExpr(BinaryExpr {
976                left: Box::new(expr),
977                op: Operator::Plus,
978                right: Box::new(col("a")),
979            });
980        }
981
982        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
983        let df_schema = DFSchema::try_from(schema)?;
984
985        // This should not stack overflow
986        let _physical_expr = create_physical_expr(
987            &expr,
988            &df_schema,
989            &ExecutionProps::new(),
990            &PhysicalPlanningContext::default(),
991        )?;
992
993        Ok(())
994    }
995}