Repository: apache/datafusion (language: rust)

Issue:
Fix the null handling for `to_char` function
### Describe the bug

Currenlty, if we input a null value to `to_char`, we will get an empty string instead of a null value.
```
> select to_char(NULL, '%Y-%m-%d %H:%M:%S') is null;
+-------------------------------------------------+
| to_char(NULL,Utf8("%Y-%m-%d %H:%M:%S")) IS NULL |
+-------------------------------------------------+
| false                                           |
+-------------------------------------------------+
1 row(s) fetched. 
Elapsed 0.001 seconds.

> select to_char(NULL, '%Y-%m-%d %H:%M:%S');
+-----------------------------------------+
| to_char(NULL,Utf8("%Y-%m-%d %H:%M:%S")) |
+-----------------------------------------+
|                                         |
+-----------------------------------------+
1 row(s) fetched. 
Elapsed 0.001 seconds.
```

However, the postgres behavior is returing a null value if got a null input.
```
psql=# select to_char(null::timestamp, '%Y-%m-%d %H:%M:%S') is null;
 ?column? 
----------
 t
(1 row)

psql=# select to_char(null::timestamp, '%Y-%m-%d %H:%M:%S');
 to_char 
---------
 
(1 row)
```

I think we should follow the behavior of postgres.

### To Reproduce

Execute the SQL mentioned above.

### Expected behavior

`to_char` should return a null if got a null input.

### Additional context

_No response_


Discussion / hints:

Fix patch:
diff --git a/datafusion/functions/src/datetime/to_char.rs b/datafusion/functions/src/datetime/to_char.rs
index a9630ea4f9ba..034bbb705070 100644
--- a/datafusion/functions/src/datetime/to_char.rs
+++ b/datafusion/functions/src/datetime/to_char.rs
@@ -212,6 +212,14 @@ fn _to_char_scalar(
     let is_scalar_expression = matches!(&expression, ColumnarValue::Scalar(_));
     let array = expression.into_array(1)?;
 
+    // fix https://github.com/apache/datafusion/issues/14884
+    // If the input date/time is null, return a null Utf8 result.
+    if array.is_null(0) {
+        return Ok(match is_scalar_expression {
+            true => ColumnarValue::Scalar(ScalarValue::Utf8(None)),
+            false => ColumnarValue::Array(new_null_array(&Utf8, array.len())),
+        });
+    }
     if format.is_none() {
         if is_scalar_expression {
             return Ok(ColumnarValue::Scalar(ScalarValue::Utf8(None)));
@@ -252,6 +260,13 @@ fn _to_char_array(args: &[ColumnarValue]) -> Result<ColumnarValue> {
     let data_type = arrays[0].data_type();
 
     for idx in 0..arrays[0].len() {
+        // fix https://github.com/apache/datafusion/issues/14884
+        // If the date/time value is null, push None.
+        if arrays[0].is_null(idx) {
+            results.push(None);
+            continue;
+        }
+
         let format = if format_array.is_null(idx) {
             None
         } else {
@@ -663,4 +678,31 @@ mod tests {
             "Execution error: Format for `to_char` must be non-null Utf8, received Timestamp(Nanosecond, None)"
         );
     }
+
+    #[test]
+    fn test_to_char_input_none_array() {
+        let date_array = Arc::new(Date32Array::from(vec![Some(18506), None])) as ArrayRef;
+        let format_array =
+            StringArray::from(vec!["%Y-%m-%d".to_string(), "%Y-%m-%d".to_string()]);
+        let args = datafusion_expr::ScalarFunctionArgs {
+            args: vec![
+                ColumnarValue::Array(date_array),
+                ColumnarValue::Array(Arc::new(format_array) as ArrayRef),
+            ],
+            number_rows: 2,
+            return_type: &DataType::Utf8,
+        };
+        let result = ToCharFunc::new()
+            .invoke_with_args(args)
+            .expect("Expected no error");
+        if let ColumnarValue::Array(result) = result {
+            let result = result.as_any().downcast_ref::<StringArray>().unwrap();
+            assert_eq!(result.len(), 2);
+            // The first element is valid, second is null.
+            assert!(!result.is_null(0));
+            assert!(result.is_null(1));
+        } else {
+            panic!("Expected an array value");
+        }
+    }
 }


Test patch:
diff --git a/datafusion/sqllogictest/test_files/timestamps.slt b/datafusion/sqllogictest/test_files/timestamps.slt
index 846a052ae36f..7eadb3c89dac 100644
--- a/datafusion/sqllogictest/test_files/timestamps.slt
+++ b/datafusion/sqllogictest/test_files/timestamps.slt
@@ -2845,7 +2845,7 @@ NULL
 query T
 SELECT to_char(null, '%d-%m-%Y');
 ----
-(empty)
+NULL
 
 query T
 SELECT to_char(column1, column2)