This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new e40c8a8915 fix: Support Substrait's compound names also for window 
functions (#11163)
e40c8a8915 is described below

commit e40c8a8915d5017951207864886892b3327b47b5
Author: Arttu <[email protected]>
AuthorDate: Mon Jul 1 15:37:41 2024 +0200

    fix: Support Substrait's compound names also for window functions (#11163)
    
    * simplify and deduplicate scalar/aggregate/window function handling
    
    * simplify window function handling and error out faster if function is not 
found
    
    * fix window function name
    
    * simplify scalar function handling
    
    * fix
    
    * add a test and fix consuming bound types
---
 datafusion/substrait/src/logical_plan/consumer.rs  | 239 ++++++++-------------
 datafusion/substrait/tests/cases/logical_plans.rs  |  34 ++-
 .../non_nullable_lists.substrait.json              |   0
 .../select_not_bool.substrait.json                 |   0
 .../test_plans/select_window.substrait.json        | 153 +++++++++++++
 5 files changed, 269 insertions(+), 157 deletions(-)

diff --git a/datafusion/substrait/src/logical_plan/consumer.rs 
b/datafusion/substrait/src/logical_plan/consumer.rs
index f7a64bd90c..af8dd60f65 100644
--- a/datafusion/substrait/src/logical_plan/consumer.rs
+++ b/datafusion/substrait/src/logical_plan/consumer.rs
@@ -20,7 +20,8 @@ use datafusion::arrow::datatypes::{
     DataType, Field, FieldRef, Fields, IntervalUnit, Schema, TimeUnit,
 };
 use datafusion::common::{
-    not_impl_err, substrait_datafusion_err, substrait_err, DFSchema, 
DFSchemaRef,
+    not_impl_datafusion_err, not_impl_err, plan_datafusion_err, plan_err,
+    substrait_datafusion_err, substrait_err, DFSchema, DFSchemaRef,
 };
 use substrait::proto::expression::literal::IntervalDayToSecond;
 use substrait::proto::read_rel::local_files::file_or_files::PathType::UriFile;
@@ -30,8 +31,7 @@ use arrow_buffer::{IntervalDayTime, IntervalMonthDayNano};
 use datafusion::execution::FunctionRegistry;
 use datafusion::logical_expr::{
     aggregate_function, expr::find_df_window_func, Aggregate, BinaryExpr, Case,
-    EmptyRelation, Expr, ExprSchemable, LogicalPlan, Operator, Projection, 
ScalarUDF,
-    Values,
+    EmptyRelation, Expr, ExprSchemable, LogicalPlan, Operator, Projection, 
Values,
 };
 
 use datafusion::logical_expr::{
@@ -57,7 +57,7 @@ use substrait::proto::{
         reference_segment::ReferenceType::StructField,
         window_function::bound as SubstraitBound,
         window_function::bound::Kind as BoundKind, window_function::Bound,
-        MaskExpression, RexType,
+        window_function::BoundsType, MaskExpression, RexType,
     },
     extensions::simple_extension_declaration::MappingType,
     function_argument::ArgType,
@@ -71,7 +71,6 @@ use substrait::proto::{
 use substrait::proto::{FunctionArgument, SortField};
 
 use datafusion::arrow::array::GenericListArray;
-use datafusion::common::plan_err;
 use datafusion::common::scalar::ScalarStructBuilder;
 use datafusion::logical_expr::expr::{InList, InSubquery, Sort};
 use std::collections::HashMap;
@@ -89,12 +88,6 @@ use crate::variation_const::{
     UNSIGNED_INTEGER_TYPE_VARIATION_REF,
 };
 
-enum ScalarFunctionType {
-    Op(Operator),
-    Expr(BuiltinExprBuilder),
-    Udf(Arc<ScalarUDF>),
-}
-
 pub fn name_to_op(name: &str) -> Result<Operator> {
     match name {
         "equal" => Ok(Operator::Eq),
@@ -128,28 +121,6 @@ pub fn name_to_op(name: &str) -> Result<Operator> {
     }
 }
 
-fn scalar_function_type_from_str(
-    ctx: &SessionContext,
-    name: &str,
-) -> Result<ScalarFunctionType> {
-    let s = ctx.state();
-    let name = substrait_fun_name(name);
-
-    if let Some(func) = s.scalar_functions().get(name) {
-        return Ok(ScalarFunctionType::Udf(func.to_owned()));
-    }
-
-    if let Ok(op) = name_to_op(name) {
-        return Ok(ScalarFunctionType::Op(op));
-    }
-
-    if let Some(builder) = BuiltinExprBuilder::try_from_name(name) {
-        return Ok(ScalarFunctionType::Expr(builder));
-    }
-
-    not_impl_err!("Unsupported function name: {name:?}")
-}
-
 pub fn substrait_fun_name(name: &str) -> &str {
     let name = match name.rsplit_once(':') {
         // Since 0.32.0, Substrait requires the function names to be in a 
compound format
@@ -972,7 +943,7 @@ pub async fn from_substrait_rex_vec(
 }
 
 /// Convert Substrait FunctionArguments to DataFusion Exprs
-pub async fn from_substriat_func_args(
+pub async fn from_substrait_func_args(
     ctx: &SessionContext,
     arguments: &Vec<FunctionArgument>,
     input_schema: &DFSchema,
@@ -984,9 +955,7 @@ pub async fn from_substriat_func_args(
             Some(ArgType::Value(e)) => {
                 from_substrait_rex(ctx, e, input_schema, extensions).await
             }
-            _ => {
-                not_impl_err!("Aggregated function argument non-Value type not 
supported")
-            }
+            _ => not_impl_err!("Function argument non-Value type not 
supported"),
         };
         args.push(arg_expr?.as_ref().clone());
     }
@@ -1003,18 +972,8 @@ pub async fn from_substrait_agg_func(
     order_by: Option<Vec<Expr>>,
     distinct: bool,
 ) -> Result<Arc<Expr>> {
-    let mut args: Vec<Expr> = vec![];
-    for arg in &f.arguments {
-        let arg_expr = match &arg.arg_type {
-            Some(ArgType::Value(e)) => {
-                from_substrait_rex(ctx, e, input_schema, extensions).await
-            }
-            _ => {
-                not_impl_err!("Aggregated function argument non-Value type not 
supported")
-            }
-        };
-        args.push(arg_expr?.as_ref().clone());
-    }
+    let args =
+        from_substrait_func_args(ctx, &f.arguments, input_schema, 
extensions).await?;
 
     let Some(function_name) = extensions.get(&f.function_reference) else {
         return plan_err!(
@@ -1022,14 +981,16 @@ pub async fn from_substrait_agg_func(
             f.function_reference
         );
     };
-    // function_name.split(':').next().unwrap_or(function_name);
+
     let function_name = substrait_fun_name((**function_name).as_str());
     // try udaf first, then built-in aggr fn.
     if let Ok(fun) = ctx.udaf(function_name) {
         // deal with situation that count(*) got no arguments
-        if fun.name() == "count" && args.is_empty() {
-            args.push(Expr::Literal(ScalarValue::Int64(Some(1))));
-        }
+        let args = if fun.name() == "count" && args.is_empty() {
+            vec![Expr::Literal(ScalarValue::Int64(Some(1)))]
+        } else {
+            args
+        };
 
         Ok(Arc::new(Expr::AggregateFunction(
             expr::AggregateFunction::new_udf(fun, args, distinct, filter, 
order_by, None),
@@ -1041,7 +1002,7 @@ pub async fn from_substrait_agg_func(
         )))
     } else {
         not_impl_err!(
-            "Aggregated function {} is not supported: function anchor = {:?}",
+            "Aggregate function {} is not supported: function anchor = {:?}",
             function_name,
             f.function_reference
         )
@@ -1145,84 +1106,40 @@ pub async fn from_substrait_rex(
             })))
         }
         Some(RexType::ScalarFunction(f)) => {
-            let fn_name = extensions.get(&f.function_reference).ok_or_else(|| {
-                DataFusionError::NotImplemented(format!(
-                    "Aggregated function not found: function reference = {:?}",
+            let Some(fn_name) = extensions.get(&f.function_reference) else {
+                return plan_err!(
+                    "Scalar function not found: function reference = {:?}",
                     f.function_reference
-                ))
-            })?;
-
-            // Convert function arguments from Substrait to DataFusion
-            async fn decode_arguments(
-                ctx: &SessionContext,
-                input_schema: &DFSchema,
-                extensions: &HashMap<u32, &String>,
-                function_args: &[FunctionArgument],
-            ) -> Result<Vec<Expr>> {
-                let mut args = Vec::with_capacity(function_args.len());
-                for arg in function_args {
-                    let arg_expr = match &arg.arg_type {
-                        Some(ArgType::Value(e)) => {
-                            from_substrait_rex(ctx, e, input_schema, 
extensions).await
-                        }
-                        _ => not_impl_err!(
-                            "Aggregated function argument non-Value type not 
supported"
-                        ),
-                    }?;
-                    args.push(arg_expr.as_ref().clone());
-                }
-                Ok(args)
-            }
+                );
+            };
+            let fn_name = substrait_fun_name(fn_name);
 
-            let fn_type = scalar_function_type_from_str(ctx, fn_name)?;
-            match fn_type {
-                ScalarFunctionType::Udf(fun) => {
-                    let args = decode_arguments(
-                        ctx,
-                        input_schema,
-                        extensions,
-                        f.arguments.as_slice(),
-                    )
+            let args =
+                from_substrait_func_args(ctx, &f.arguments, input_schema, 
extensions)
                     .await?;
-                    Ok(Arc::new(Expr::ScalarFunction(
-                        expr::ScalarFunction::new_udf(fun, args),
-                    )))
-                }
-                ScalarFunctionType::Op(op) => {
-                    if f.arguments.len() != 2 {
-                        return not_impl_err!(
-                            "Expect two arguments for binary operator {op:?}"
-                        );
-                    }
-                    let lhs = &f.arguments[0].arg_type;
-                    let rhs = &f.arguments[1].arg_type;
-
-                    match (lhs, rhs) {
-                        (Some(ArgType::Value(l)), Some(ArgType::Value(r))) => {
-                            Ok(Arc::new(Expr::BinaryExpr(BinaryExpr {
-                                left: Box::new(
-                                    from_substrait_rex(ctx, l, input_schema, 
extensions)
-                                        .await?
-                                        .as_ref()
-                                        .clone(),
-                                ),
-                                op,
-                                right: Box::new(
-                                    from_substrait_rex(ctx, r, input_schema, 
extensions)
-                                        .await?
-                                        .as_ref()
-                                        .clone(),
-                                ),
-                            })))
-                        }
-                        (l, r) => not_impl_err!(
-                            "Invalid arguments for binary expression: {l:?} 
and {r:?}"
-                        ),
-                    }
-                }
-                ScalarFunctionType::Expr(builder) => {
-                    builder.build(ctx, f, input_schema, extensions).await
+
+            // try to first match the requested function into registered udfs, 
then built-in ops
+            // and finally built-in expressions
+            if let Some(func) = ctx.state().scalar_functions().get(fn_name) {
+                Ok(Arc::new(Expr::ScalarFunction(
+                    expr::ScalarFunction::new_udf(func.to_owned(), args),
+                )))
+            } else if let Ok(op) = name_to_op(fn_name) {
+                if args.len() != 2 {
+                    return not_impl_err!(
+                        "Expect two arguments for binary operator {op:?}"
+                    );
                 }
+
+                Ok(Arc::new(Expr::BinaryExpr(BinaryExpr {
+                    left: Box::new(args[0].to_owned()),
+                    op,
+                    right: Box::new(args[1].to_owned()),
+                })))
+            } else if let Some(builder) = 
BuiltinExprBuilder::try_from_name(fn_name) {
+                builder.build(ctx, f, input_schema, extensions).await
+            } else {
+                not_impl_err!("Unsupported function name: {fn_name:?}")
             }
         }
         Some(RexType::Literal(lit)) => {
@@ -1247,36 +1164,50 @@ pub async fn from_substrait_rex(
             None => substrait_err!("Cast expression without output type is not 
allowed"),
         },
         Some(RexType::WindowFunction(window)) => {
-            let fun = match extensions.get(&window.function_reference) {
-                Some(function_name) => {
-                    // check udaf
-                    match ctx.udaf(function_name) {
-                        Ok(udaf) => {
-                            
Ok(Some(WindowFunctionDefinition::AggregateUDF(udaf)))
-                        }
-                        Err(_) => Ok(find_df_window_func(function_name)),
-                    }
-                }
-                None => not_impl_err!(
-                    "Window function not found: function anchor = {:?}",
-                    &window.function_reference
-                ),
+            let Some(fn_name) = extensions.get(&window.function_reference) 
else {
+                return plan_err!(
+                    "Window function not found: function reference = {:?}",
+                    window.function_reference
+                );
             };
+            let fn_name = substrait_fun_name(fn_name);
+
+            // check udaf first, then built-in functions
+            let fun = match ctx.udaf(fn_name) {
+                Ok(udaf) => Ok(WindowFunctionDefinition::AggregateUDF(udaf)),
+                Err(_) => find_df_window_func(fn_name).ok_or_else(|| {
+                    not_impl_datafusion_err!(
+                        "Window function {} is not supported: function anchor 
= {:?}",
+                        fn_name,
+                        window.function_reference
+                    )
+                }),
+            }?;
+
             let order_by =
                 from_substrait_sorts(ctx, &window.sorts, input_schema, 
extensions)
                     .await?;
-            // Substrait does not encode WindowFrameUnits so we're using a 
simple logic to determine the units
-            // If there is no `ORDER BY`, then by default, the frame counts 
each row from the lower up to upper boundary
-            // If there is `ORDER BY`, then by default, each frame is a range 
starting from unbounded preceding to current row
-            // TODO: Consider the cases where window frame is specified in 
query and is different from default
-            let units = if order_by.is_empty() {
-                WindowFrameUnits::Rows
-            } else {
-                WindowFrameUnits::Range
-            };
+
+            let bound_units =
+                match BoundsType::try_from(window.bounds_type).map_err(|e| {
+                    plan_datafusion_err!("Invalid bound type {}: {e}", 
window.bounds_type)
+                })? {
+                    BoundsType::Rows => WindowFrameUnits::Rows,
+                    BoundsType::Range => WindowFrameUnits::Range,
+                    BoundsType::Unspecified => {
+                        // If the plan does not specify the bounds type, then 
we use a simple logic to determine the units
+                        // If there is no `ORDER BY`, then by default, the 
frame counts each row from the lower up to upper boundary
+                        // If there is `ORDER BY`, then by default, each frame 
is a range starting from unbounded preceding to current row
+                        if order_by.is_empty() {
+                            WindowFrameUnits::Rows
+                        } else {
+                            WindowFrameUnits::Range
+                        }
+                    }
+                };
             Ok(Arc::new(Expr::WindowFunction(expr::WindowFunction {
-                fun: fun?.unwrap(),
-                args: from_substriat_func_args(
+                fun,
+                args: from_substrait_func_args(
                     ctx,
                     &window.arguments,
                     input_schema,
@@ -1292,7 +1223,7 @@ pub async fn from_substrait_rex(
                 .await?,
                 order_by,
                 window_frame: 
datafusion::logical_expr::WindowFrame::new_bounds(
-                    units,
+                    bound_units,
                     from_substrait_bound(&window.lower_bound, true)?,
                     from_substrait_bound(&window.upper_bound, false)?,
                 ),
diff --git a/datafusion/substrait/tests/cases/logical_plans.rs 
b/datafusion/substrait/tests/cases/logical_plans.rs
index 94572e098b..6492febc93 100644
--- a/datafusion/substrait/tests/cases/logical_plans.rs
+++ b/datafusion/substrait/tests/cases/logical_plans.rs
@@ -28,7 +28,7 @@ mod tests {
     use substrait::proto::Plan;
 
     #[tokio::test]
-    async fn function_compound_signature() -> Result<()> {
+    async fn scalar_function_compound_signature() -> Result<()> {
         // DataFusion currently produces Substrait that refers to functions 
only by their name.
         // However, the Substrait spec requires that functions be identified 
by their compound signature.
         // This test confirms that DataFusion is able to consume plans 
following the spec, even though
@@ -39,7 +39,7 @@ mod tests {
 
         // File generated with substrait-java's Isthmus:
         // ./isthmus-cli/build/graal/isthmus "select not d from data" -c 
"create table data (d boolean)"
-        let proto = read_json("tests/testdata/select_not_bool.substrait.json");
+        let proto = 
read_json("tests/testdata/test_plans/select_not_bool.substrait.json");
 
         let plan = from_substrait_plan(&ctx, &proto).await?;
 
@@ -51,13 +51,41 @@ mod tests {
         Ok(())
     }
 
+    // Aggregate function compound signature is tested through TPCH plans
+
+    #[tokio::test]
+    async fn window_function_compound_signature() -> Result<()> {
+        // DataFusion currently produces Substrait that refers to functions 
only by their name.
+        // However, the Substrait spec requires that functions be identified 
by their compound signature.
+        // This test confirms that DataFusion is able to consume plans 
following the spec, even though
+        // we don't yet produce such plans.
+        // Once we start producing plans with compound signatures, this test 
can be replaced by the roundtrip tests.
+
+        let ctx = create_context().await?;
+
+        // File generated with substrait-java's Isthmus:
+        // ./isthmus-cli/build/graal/isthmus "select sum(d) OVER (PARTITION BY 
part ORDER BY ord ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) AS 
lead_expr from data" -c "create table data (d int, part int, ord int)"
+        let proto = 
read_json("tests/testdata/test_plans/select_window.substrait.json");
+
+        let plan = from_substrait_plan(&ctx, &proto).await?;
+
+        assert_eq!(
+            format!("{:?}", plan),
+            "Projection: sum(DATA.a) PARTITION BY [DATA.b] ORDER BY [DATA.c 
ASC NULLS LAST] ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING AS LEAD_EXPR\
+            \n  WindowAggr: windowExpr=[[sum(DATA.a) PARTITION BY [DATA.b] 
ORDER BY [DATA.c ASC NULLS LAST] ROWS BETWEEN 1 PRECEDING AND UNBOUNDED 
FOLLOWING]]\
+            \n    TableScan: DATA projection=[a, b, c, d, e, f]"
+        );
+        Ok(())
+    }
+
     #[tokio::test]
     async fn non_nullable_lists() -> Result<()> {
         // DataFusion's Substrait consumer treats all lists as nullable, even 
if the Substrait plan specifies them as non-nullable.
         // That's because implementing the non-nullability consistently is 
non-trivial.
         // This test confirms that reading a plan with non-nullable lists 
works as expected.
         let ctx = create_context().await?;
-        let proto = 
read_json("tests/testdata/non_nullable_lists.substrait.json");
+        let proto =
+            
read_json("tests/testdata/test_plans/non_nullable_lists.substrait.json");
 
         let plan = from_substrait_plan(&ctx, &proto).await?;
 
diff --git 
a/datafusion/substrait/tests/testdata/non_nullable_lists.substrait.json 
b/datafusion/substrait/tests/testdata/test_plans/non_nullable_lists.substrait.json
similarity index 100%
rename from 
datafusion/substrait/tests/testdata/non_nullable_lists.substrait.json
rename to 
datafusion/substrait/tests/testdata/test_plans/non_nullable_lists.substrait.json
diff --git a/datafusion/substrait/tests/testdata/select_not_bool.substrait.json 
b/datafusion/substrait/tests/testdata/test_plans/select_not_bool.substrait.json
similarity index 100%
rename from datafusion/substrait/tests/testdata/select_not_bool.substrait.json
rename to 
datafusion/substrait/tests/testdata/test_plans/select_not_bool.substrait.json
diff --git 
a/datafusion/substrait/tests/testdata/test_plans/select_window.substrait.json 
b/datafusion/substrait/tests/testdata/test_plans/select_window.substrait.json
new file mode 100644
index 0000000000..3082c4258f
--- /dev/null
+++ 
b/datafusion/substrait/tests/testdata/test_plans/select_window.substrait.json
@@ -0,0 +1,153 @@
+{
+  "extensionUris": [
+    {
+      "extensionUriAnchor": 1,
+      "uri": "/functions_arithmetic.yaml"
+    }
+  ],
+  "extensions": [
+    {
+      "extensionFunction": {
+        "extensionUriReference": 1,
+        "functionAnchor": 0,
+        "name": "sum:i32"
+      }
+    }
+  ],
+  "relations": [
+    {
+      "root": {
+        "input": {
+          "project": {
+            "common": {
+              "emit": {
+                "outputMapping": [
+                  3
+                ]
+              }
+            },
+            "input": {
+              "read": {
+                "common": {
+                  "direct": {
+                  }
+                },
+                "baseSchema": {
+                  "names": [
+                    "D",
+                    "PART",
+                    "ORD"
+                  ],
+                  "struct": {
+                    "types": [
+                      {
+                        "i32": {
+                          "typeVariationReference": 0,
+                          "nullability": "NULLABILITY_NULLABLE"
+                        }
+                      },
+                      {
+                        "i32": {
+                          "typeVariationReference": 0,
+                          "nullability": "NULLABILITY_NULLABLE"
+                        }
+                      },
+                      {
+                        "i32": {
+                          "typeVariationReference": 0,
+                          "nullability": "NULLABILITY_NULLABLE"
+                        }
+                      }
+                    ],
+                    "typeVariationReference": 0,
+                    "nullability": "NULLABILITY_REQUIRED"
+                  }
+                },
+                "namedTable": {
+                  "names": [
+                    "DATA"
+                  ]
+                }
+              }
+            },
+            "expressions": [
+              {
+                "windowFunction": {
+                  "functionReference": 0,
+                  "partitions": [
+                    {
+                      "selection": {
+                        "directReference": {
+                          "structField": {
+                            "field": 1
+                          }
+                        },
+                        "rootReference": {
+                        }
+                      }
+                    }
+                  ],
+                  "sorts": [
+                    {
+                      "expr": {
+                        "selection": {
+                          "directReference": {
+                            "structField": {
+                              "field": 2
+                            }
+                          },
+                          "rootReference": {
+                          }
+                        }
+                      },
+                      "direction": "SORT_DIRECTION_ASC_NULLS_LAST"
+                    }
+                  ],
+                  "upperBound": {
+                    "unbounded": {
+                    }
+                  },
+                  "lowerBound": {
+                    "preceding": {
+                      "offset": "1"
+                    }
+                  },
+                  "phase": "AGGREGATION_PHASE_INITIAL_TO_RESULT",
+                  "outputType": {
+                    "i32": {
+                      "typeVariationReference": 0,
+                      "nullability": "NULLABILITY_NULLABLE"
+                    }
+                  },
+                  "args": [],
+                  "arguments": [
+                    {
+                      "value": {
+                        "selection": {
+                          "directReference": {
+                            "structField": {
+                              "field": 0
+                            }
+                          },
+                          "rootReference": {
+                          }
+                        }
+                      }
+                    }
+                  ],
+                  "invocation": "AGGREGATION_INVOCATION_ALL",
+                  "options": [],
+                  "boundsType": "BOUNDS_TYPE_ROWS"
+                }
+              }
+            ]
+          }
+        },
+        "names": [
+          "LEAD_EXPR"
+        ]
+      }
+    }
+  ],
+  "expectedTypeUrls": []
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to