jayzhan211 commented on code in PR #27143:
URL: https://github.com/apache/datafusion/pull/27143#discussion_r4538661857


##########
datafusion/expr/src/expr_schema.rs:
##########
@@ -685,11 +688,26 @@ impl ExprSchemable for Expr {
                     ))),
                 }
             }
+            Expr::Case(case) => {
+                let fields = case
+                    .when_then_expr
+                    .iter()
+                    .map(|(_, then)| then)
+                    .chain(case.else_expr.iter())
+                    .map(|expr| expr.to_field(schema).map(|(_, field)| field))
+                    .collect::<Result<Vec<_>>>()?;
+                let metadata =
+                    coerced_fields_metadata(fields.iter().map(|f| 
f.as_ref()))?;
+                Ok(Arc::new(metadata.add_to_field(Field::new(
+                    &schema_name,
+                    self.get_type(schema)?,
+                    self.nullable(schema)?,
+                ))))

Review Comment:
   Planning a CASE nested inside function calls got much slower: with 12 levels 
of `upper(CASE WHEN a > k THEN <inner> ELSE s END)`, `EXPLAIN` takes 19–21 s 
here vs 0.34–0.44 s on main (debug build). Each level now walks its branches 
three times: `to_field` on each branch, then `self.get_type` and 
`self.nullable`, and for function calls those two go back through `to_field`. 
So the cost grows as 3^depth instead of main's 2^depth. Taking the type from 
the branch fields you already have brings it back to 0.38–0.43 s with the same 
plan. The expr and optimizer lib tests and the case/metadata/coalesce/nvl slt 
files still pass:
   
   ```suggestion
                   let metadata =
                       coerced_fields_metadata(fields.iter().map(|f| 
f.as_ref()))?;
                   // Same as `get_type`: the first non-NULL branch type. 
Reading it from
                   // `fields` avoids a second walk of the branches, which 
multiplies with
                   // the depth of nested CASE and function calls.
                   let data_type = fields
                       .iter()
                       .map(|f| f.data_type())
                       .find(|t| !t.is_null())
                       .cloned()
                       .unwrap_or(DataType::Null);
                   Ok(Arc::new(metadata.add_to_field(Field::new(
                       &schema_name,
                       data_type,
                       self.nullable(schema)?,
                   ))))
   ```
   
   <details>
   <summary>Script that generates the query, and the timings</summary>
   
   ```sh
   python3 - 12 > q.sql <<'PY'
   import sys
   d = int(sys.argv[1]); e = "s"
   for k in range(d):
       e = f"upper(CASE WHEN a > {k} THEN {e} ELSE s END)"
   print("CREATE TABLE t AS VALUES (1, 'x');")
   print("CREATE VIEW v AS SELECT column1 AS a, column2 AS s FROM t;")
   print(f"EXPLAIN SELECT {e} AS r FROM v;")
   PY
   time datafusion-cli -q -f q.sql > /dev/null
   ```
   
   | depth | main | this PR | with the suggestion |
   |---|---|---|---|
   | 10 | 0.15–0.31 s | 1.7–2.7 s | 0.12 s |
   | 12 | 0.34–0.44 s | 18.8–21.4 s | 0.38–0.43 s |
   | 14 | 1.3–1.9 s | not run | 1.5 s |
   
   </details>
   



##########
datafusion/sqllogictest/test_files/metadata.slt:
##########
@@ -687,6 +687,173 @@ NULL
 l_bar
 baz
 
+# CASE merges the metadata of its value branches with the same rules as
+# coalesce: the earlier branch wins for each key, and an extension name is kept
+# together with its parameters. The operand of a simple CASE and the WHEN
+# conditions do not contribute.
+query TTT
+select
+  arrow_metadata(case when id > 1 then name else l_name end, 'metadata_key'),
+  arrow_metadata(case id when 1 then name else l_name end, 'metadata_key'),
+  arrow_metadata(
+    case when id > 1 then cast(name as json) else cast(l_name as json) end,
+    'ARROW:extension:name')
+from table_with_metadata;
+----
+the name field the name field arrow.json
+the name field the name field arrow.json
+the name field the name field arrow.json
+
+statement error Error during planning: Cannot merge field metadata with 
differing values of 'ARROW:extension:name': 'arrow\.json' and 'b\.type'
+select case when id > 1
+  then cast(name as json)
+  else with_metadata(l_name, 'ARROW:extension:name', 'b.type') end
+from table_with_metadata;

Review Comment:
   Optional cleanup: the plain-text form of this error is easier to read than 
the escaped regex. I ran the file with this change applied, including the blank 
line that follows it:
   
   ```suggestion
   statement error
   select case when id > 1
     then cast(name as json)
     else with_metadata(l_name, 'ARROW:extension:name', 'b.type') end
   from table_with_metadata;
   ----
   DataFusion error: Error during planning: Cannot merge field metadata with 
differing values of 'ARROW:extension:name': 'arrow.json' and 'b.type'
   
   ```
   



##########
datafusion/physical-expr/src/expressions/case.rs:
##########
@@ -1307,6 +1325,24 @@ impl PhysicalExpr for CaseExpr {
         }
     }
 
+    fn return_field(&self, input_schema: &Schema) -> Result<FieldRef> {

Review Comment:
   `ScalarFunctionExpr::evaluate` calls `return_field` on its arguments for 
every batch, and here each nested CASE level formats the name of its whole 
subtree and recomputes its nullability. That makes it O(depth²): for a CASE 
nested in the ELSE, the extra cost over main is 11% of the CASE's evaluation 
time at depth 4 and 22% at depth 8 (release build, 8192 rows). If nested CASE 
branches compute only their metadata, it drops to 1.8% and 2.8%, and the 
physical-expr lib tests and the metadata/case slt files still pass:
   
   ```diff
   -use datafusion_common::metadata::coerced_fields_metadata;
   +use datafusion_common::metadata::{FieldMetadata, coerced_fields_metadata};
   @@ impl CaseExpr {
            self.body.else_expr.as_ref()
        }
   +
   +    /// The metadata of [`PhysicalExpr::return_field`]. A nested CASE 
branch only
   +    /// computes its metadata, not its name and nullability, which would 
walk its
   +    /// whole subtree again at every level.
   +    fn return_metadata(&self, input_schema: &Schema) -> 
Result<FieldMetadata> {
   +        let fields = self
   +            .body
   +            .when_then_expr
   +            .iter()
   +            .map(|(_, then)| then)
   +            .chain(self.body.else_expr.iter())
   +            .map(|expr| match expr.downcast_ref::<CaseExpr>() {
   +                Some(case) => {
   +                    let field = Field::new("", 
case.data_type(input_schema)?, true);
   +                    Ok(Arc::new(
   +                        
case.return_metadata(input_schema)?.add_to_field(field),
   +                    ))
   +                }
   +                None => expr.return_field(input_schema),
   +            })
   +            .collect::<Result<Vec<_>>>()?;
   +        coerced_fields_metadata(fields.iter().map(|f| f.as_ref()))
   +    }
    }
   @@ impl PhysicalExpr for CaseExpr {
        fn return_field(&self, input_schema: &Schema) -> Result<FieldRef> {
   -        let fields = self
   -            .body
   -            .when_then_expr
   -            .iter()
   -            .map(|(_, then)| then)
   -            .chain(self.body.else_expr.iter())
   -            .map(|expr| expr.return_field(input_schema))
   -            .collect::<Result<Vec<_>>>()?;
   -        let metadata = coerced_fields_metadata(fields.iter().map(|f| 
f.as_ref()))?;
            let field = Field::new(
                format!("{self}"),
                self.data_type(input_schema)?,
                self.nullable(input_schema)?,
            );
   -        Ok(metadata.add_to_field(field).into())
   +        Ok(self
   +            .return_metadata(input_schema)?
   +            .add_to_field(field)
   +            .into())
        }
   ```
   
   <details>
   <summary>Extra return_field cost as a share of CASE evaluation (release, 
8192 rows)</summary>
   
   | nesting | this PR | with the diff |
   |---|---|---|
   | ELSE, depth 1 | 2.2% | 2.3% |
   | ELSE, depth 4 | 11.1% | 1.8% |
   | ELSE, depth 8 | 22.2% | 2.8% |
   | ELSE, depth 32 | 137% | 7.3% |
   | THEN, depth 32 | 49.5% | 2.4% |
   
   </details>
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to