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

github-merge-queue[bot] 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 d42cd854df feat(spark): add equal_null (#24827)
d42cd854df is described below

commit d42cd854df0f78d95e95946831077db3c7808ced
Author: Advit Arora <[email protected]>
AuthorDate: Tue Sep 22 08:48:42 2026 +0000

    feat(spark): add equal_null (#24827)
    
    ## Which issue does this PR close?
    
    No issue filed. Part of #15914, which tracks the Spark function library.
    
    ## Rationale for this change
    
    `equal_null` is missing, and the `misc` module it belongs in had no
    functions in it at all. In
    Spark it is an alias: `EqualNull(l, r)` is rewritten to `EqualNullSafe`,
    the `<=>` operator, so
    it is null-safe equality. Both NULL is true, and the result is never
    NULL:
    
    ```sql
    SELECT equal_null(NULL, NULL);              -- true
    SELECT equal_null(NULL::int, 1::int);       -- false
    ```
    
    DataFusion already has that operator as `IS NOT DISTINCT FROM`, so this
    mirrors Spark's own
    structure instead of writing a second comparison kernel.
    
    ## What changes are included in this PR?
    
    `misc/equal_null.rs` and its registration. `simplify()` rewrites to the
    operator, which is
    Spark's `replacement` field, and `invoke_with_args` calls `apply_cmp`
    with the same operator so
    the function also works with the logical optimizer disabled, as the
    crate README requires for
    Comet.
    
    Two of the stub's commented-out queries were malformed, the porting
    script wrote one cast per
    distinct `typeof()` key, so a call with two identical literals lost an
    argument. The same
    artifact affects 10 more pairs in 6 other spark files, left alone here.
    
    Two divergences from Spark are left alone because they belong to the
    operator, not to this
    function. Spark treats `-NaN` and `NaN` as equal and DataFusion does
    not, which is true of
    `IS NOT DISTINCT FROM` generally. Spark also rejects maps at analysis
    since `MapType` is not
    orderable, while `comparison_coercion` here accepts them.
    
    `return_field_from_args` is overridden so the field is not nullable.
    `normalize_float_zero` now
    recurses into nested children. `compare_op_for_nested` and
    `GroupValuesColumn` both normalize
    through it, so the scalar kernels and the two group-by paths agree on
    nested float keys.
    
    ## Are these changes tested?
    
    Yes, `spark/misc/equal_null.slt` goes from a skipped stub to 27
    assertions covering the truth
    table, float ordering, columns, arrays, structs, decimals and the arity
    errors.
    
    Reverting `simplify()` to plain `Eq` fails 9 of them, so the file is not
    passing on constant
    folding. Putting `invoke_with_args` back to a stub fails the two queries
    that run with the
    optimizer off, and nothing else.
    
    ## Are there any user-facing changes?
    
    Yes. `equal_null` is new in `datafusion-spark`, and `-0.0` now matches
    `+0.0` inside nested
    values, in comparisons and in `GROUP BY`, matching the scalar kernels.
---
 Cargo.lock                                         |   1 +
 datafusion/common/src/utils/mod.rs                 |  42 ++++-
 datafusion/physical-expr-common/src/datum.rs       |  15 +-
 .../aggregates/group_values/multi_group_by/mod.rs  |   7 +
 datafusion/spark/Cargo.toml                        |   1 +
 datafusion/spark/src/function/misc/equal_null.rs   |  99 ++++++++++
 datafusion/spark/src/function/misc/mod.rs          |  17 +-
 .../sqllogictest/test_files/negative_zero.slt      |  25 +++
 .../test_files/spark/misc/equal_null.slt           | 208 ++++++++++++++++++++-
 9 files changed, 400 insertions(+), 15 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index bc8f497624..2f2ea87c91 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2677,6 +2677,7 @@ dependencies = [
  "datafusion-functions-aggregate",
  "datafusion-functions-aggregate-common",
  "datafusion-functions-nested",
+ "datafusion-physical-expr-common",
  "datafusion-session",
  "log",
  "num-traits",
diff --git a/datafusion/common/src/utils/mod.rs 
b/datafusion/common/src/utils/mod.rs
index bf87b9a788..84a8a295a6 100644
--- a/datafusion/common/src/utils/mod.rs
+++ b/datafusion/common/src/utils/mod.rs
@@ -1447,7 +1447,7 @@ fn fsl_values_row_number(list_size: i32, array_len: 
usize) -> Result<Int32Array>
 /// OR-reduction) decides whether to fall through to the rewriting path.
 /// Only arrays that actually contain `-0.0` pay for a new buffer.
 pub fn normalize_float_zero(array: &ArrayRef) -> ArrayRef {
-    use arrow::array::{Float16Array, Float32Array, Float64Array};
+    use arrow::array::{Float16Array, Float32Array, Float64Array, make_array};
     use arrow::datatypes::{Float16Type, Float32Type, Float64Type};
     // -0.0 has only the sign bit set; no other finite or NaN value shares
     // this bit pattern, so a strict-equality scan reliably gates the rewrite.
@@ -1499,10 +1499,50 @@ pub fn normalize_float_zero(array: &ArrayRef) -> 
ArrayRef {
             });
             Arc::new(normalized)
         }
+        dt if has_float_leaf(dt) => {
+            let data = array.to_data();
+            let children = data
+                .child_data()
+                .iter()
+                .map(|child| 
normalize_float_zero(&make_array(child.clone())).to_data())
+                .collect::<Vec<_>>();
+            if children
+                .iter()
+                .zip(data.child_data())
+                .all(|(new, old)| new.ptr_eq(old))
+            {
+                return Arc::clone(array);
+            }
+            make_array(
+                data.into_builder()
+                    .child_data(children)
+                    .build()
+                    .expect("rewriting float leaves preserves the array 
layout"),
+            )
+        }
         _ => Arc::clone(array),
     }
 }
 
+pub fn has_float_leaf(data_type: &DataType) -> bool {
+    match data_type {
+        DataType::Float16 | DataType::Float32 | DataType::Float64 => true,
+        DataType::List(f)
+        | DataType::LargeList(f)
+        | DataType::ListView(f)
+        | DataType::LargeListView(f)
+        | DataType::FixedSizeList(f, _)
+        | DataType::Map(f, _)
+        | DataType::RunEndEncoded(_, f) => has_float_leaf(f.data_type()),
+        DataType::Struct(fields) => fields.iter().any(|f| 
has_float_leaf(f.data_type())),
+        DataType::Union(fields, _) => {
+            fields.iter().any(|(_, f)| has_float_leaf(f.data_type()))
+        }
+        DataType::Dictionary(_, values) => has_float_leaf(values),
+        _ => false,
+    }
+}
+
 /// Replace `-0.0` with `+0.0` in `Float16`, `Float32`, or `Float64` scalar
 /// values. Other variants are returned unchanged. See [`normalize_float_zero`]
 /// for context.
diff --git a/datafusion/physical-expr-common/src/datum.rs 
b/datafusion/physical-expr-common/src/datum.rs
index d23fb30db6..416507c618 100644
--- a/datafusion/physical-expr-common/src/datum.rs
+++ b/datafusion/physical-expr-common/src/datum.rs
@@ -16,14 +16,16 @@
 // under the License.
 
 use arrow::array::BooleanArray;
-use arrow::array::{ArrayRef, Datum, make_comparator};
+use arrow::array::{Array, ArrayRef, Datum, make_array, make_comparator};
 use arrow::buffer::{BooleanBuffer, NullBuffer};
 use arrow::compute::kernels::cmp::{
     distinct, eq, gt, gt_eq, lt, lt_eq, neq, not_distinct,
 };
 use arrow::compute::{SortOptions, ilike, like, nilike, nlike};
 use arrow::error::ArrowError;
-use datafusion_common::utils::{normalize_float_zero, 
normalize_float_zero_scalar};
+use datafusion_common::utils::{
+    has_float_leaf, normalize_float_zero, normalize_float_zero_scalar,
+};
 use datafusion_common::{Result, ScalarValue};
 use datafusion_common::{arrow_datafusion_err, assert_or_internal_err, 
internal_err};
 use datafusion_expr_common::columnar_value::ColumnarValue;
@@ -147,6 +149,11 @@ pub fn compare_with_eq(
     }
 }
 
+fn normalize_nested_float_zero(array: &dyn Array) -> Option<ArrayRef> {
+    has_float_leaf(array.data_type())
+        .then(|| normalize_float_zero(&make_array(array.to_data())))
+}
+
 /// Compare on nested type List, Struct, and so on
 pub fn compare_op_for_nested(
     op: Operator,
@@ -155,6 +162,10 @@ pub fn compare_op_for_nested(
 ) -> Result<BooleanArray> {
     let (l, is_l_scalar) = lhs.get();
     let (r, is_r_scalar) = rhs.get();
+    let l_norm = normalize_nested_float_zero(l);
+    let r_norm = normalize_nested_float_zero(r);
+    let l = l_norm.as_deref().unwrap_or(l);
+    let r = r_norm.as_deref().unwrap_or(r);
     let l_len = l.len();
     let r_len = r.len();
 
diff --git 
a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs 
b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs
index 5c33abd41d..f10990a73a 100644
--- a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs
+++ b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs
@@ -50,6 +50,7 @@ use arrow::datatypes::{
 };
 use datafusion_common::hash_utils::RandomState;
 use datafusion_common::hash_utils::create_hashes;
+use datafusion_common::utils::{has_float_leaf, normalize_float_zero};
 use datafusion_common::{Result, not_impl_err};
 use datafusion_execution::memory_pool::proxy::{HashTableAllocExt, VecAllocExt};
 use datafusion_expr::{EmitTo, GroupSelection};
@@ -1131,6 +1132,12 @@ fn make_group_column(field: &Field) -> Result<Box<dyn 
GroupColumn>> {
 
 impl<const STREAMING: bool> GroupValues for GroupValuesColumn<STREAMING> {
     fn intern(&mut self, cols: &[ArrayRef], groups: &mut Vec<usize>) -> 
Result<()> {
+        let normalized: Option<Vec<ArrayRef>> = cols
+            .iter()
+            .any(|col| col.data_type().is_nested() && 
has_float_leaf(col.data_type()))
+            .then(|| cols.iter().map(normalize_float_zero).collect());
+        let cols = normalized.as_deref().unwrap_or(cols);
+
         // `try_new` and the reset points in `emit` / `clear_shrink` keep
         // `self.group_values` populated with one builder per schema field,
         // so no lazy initialization is needed here.
diff --git a/datafusion/spark/Cargo.toml b/datafusion/spark/Cargo.toml
index f40708863d..08309a931d 100644
--- a/datafusion/spark/Cargo.toml
+++ b/datafusion/spark/Cargo.toml
@@ -56,6 +56,7 @@ datafusion-functions = { workspace = true }
 datafusion-functions-aggregate = { workspace = true }
 datafusion-functions-aggregate-common = { workspace = true }
 datafusion-functions-nested = { workspace = true }
+datafusion-physical-expr-common = { workspace = true }
 datafusion-session = { workspace = true }
 log = { workspace = true }
 num-traits = { workspace = true }
diff --git a/datafusion/spark/src/function/misc/equal_null.rs 
b/datafusion/spark/src/function/misc/equal_null.rs
new file mode 100644
index 0000000000..b1604a7162
--- /dev/null
+++ b/datafusion/spark/src/function/misc/equal_null.rs
@@ -0,0 +1,99 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use arrow::datatypes::{DataType, Field, FieldRef};
+use datafusion_common::utils::take_function_args;
+use datafusion_common::{Result, plan_err};
+use datafusion_expr::simplify::{ExprSimplifyResult, SimplifyContext};
+use datafusion_expr::type_coercion::binary::comparison_coercion;
+use datafusion_expr::{
+    ColumnarValue, Expr, Operator, ReturnFieldArgs, ScalarFunctionArgs, 
ScalarUDFImpl,
+    Signature, Volatility, binary_expr,
+};
+use datafusion_physical_expr_common::datum::apply_cmp;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Hash)]
+pub struct SparkEqualNull {
+    signature: Signature,
+}
+
+impl Default for SparkEqualNull {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl SparkEqualNull {
+    pub fn new() -> Self {
+        Self {
+            signature: Signature::user_defined(Volatility::Immutable),
+        }
+    }
+}
+
+impl ScalarUDFImpl for SparkEqualNull {
+    fn name(&self) -> &str {
+        "equal_null"
+    }
+
+    fn signature(&self) -> &Signature {
+        &self.signature
+    }
+
+    fn coerce_types(&self, arg_types: &[DataType]) -> Result<Vec<DataType>> {
+        let [lhs, rhs] = arg_types else {
+            return plan_err!(
+                "Function 'equal_null' expects 2 arguments but received {}",
+                arg_types.len()
+            );
+        };
+        // simplify() emits a comparison, and the type coercion pass has 
already run by then
+        let Some(common) = comparison_coercion(lhs, rhs) else {
+            return plan_err!(
+                "For function 'equal_null' {lhs} and {rhs} are not comparable"
+            );
+        };
+        Ok(vec![common.clone(), common])
+    }
+
+    fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
+        Ok(DataType::Boolean)
+    }
+
+    fn return_field_from_args(&self, _args: ReturnFieldArgs) -> 
Result<FieldRef> {
+        Ok(Arc::new(Field::new(self.name(), DataType::Boolean, false)))
+    }
+
+    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> 
Result<ColumnarValue> {
+        let [lhs, rhs] = take_function_args(self.name(), args.args)?;
+        apply_cmp(Operator::IsNotDistinctFrom, &lhs, &rhs)
+    }
+
+    fn simplify(
+        &self,
+        args: Vec<Expr>,
+        _info: &SimplifyContext,
+    ) -> Result<ExprSimplifyResult> {
+        let [lhs, rhs] = take_function_args(self.name(), args)?;
+        Ok(ExprSimplifyResult::Simplified(binary_expr(
+            lhs,
+            Operator::IsNotDistinctFrom,
+            rhs,
+        )))
+    }
+}
diff --git a/datafusion/spark/src/function/misc/mod.rs 
b/datafusion/spark/src/function/misc/mod.rs
index a87df9a2c8..8739a53f40 100644
--- a/datafusion/spark/src/function/misc/mod.rs
+++ b/datafusion/spark/src/function/misc/mod.rs
@@ -16,10 +16,23 @@
 // under the License.
 
 use datafusion_expr::ScalarUDF;
+use datafusion_functions::make_udf_function;
 use std::sync::Arc;
 
-pub mod expr_fn {}
+mod equal_null;
+
+make_udf_function!(equal_null::SparkEqualNull, equal_null);
+
+pub mod expr_fn {
+    use datafusion_functions::export_functions;
+
+    export_functions!((
+        equal_null,
+        "Returns true if arg1 equals arg2, or if both are NULL; false 
otherwise",
+        arg1 arg2
+    ));
+}
 
 pub fn functions() -> Vec<Arc<ScalarUDF>> {
-    vec![]
+    vec![equal_null()]
 }
diff --git a/datafusion/sqllogictest/test_files/negative_zero.slt 
b/datafusion/sqllogictest/test_files/negative_zero.slt
index 8ea1122880..fe4df57e17 100644
--- a/datafusion/sqllogictest/test_files/negative_zero.slt
+++ b/datafusion/sqllogictest/test_files/negative_zero.slt
@@ -229,3 +229,28 @@ JOIN (SELECT arrow_cast(-0.0, 'Float32') AS b) t2 ON t1.a 
= t2.b;
 
 statement ok
 reset datafusion.optimizer.prefer_hash_join;
+
+#####
+## Nested values holding +0.0 / -0.0
+#####
+
+query BBB
+SELECT [0.0] = [-0.0] AS eq,
+       [0.0] IS DISTINCT FROM [-0.0] AS is_distinct,
+       {a: 0.0} = {a: -0.0} AS struct_eq;
+----
+true false true
+
+statement ok
+CREATE TABLE nested_zeros(id INT, a DOUBLE[]) AS VALUES (1, [0.0]), (2, 
[-0.0]);
+
+query II
+SELECT l.id, r.id FROM nested_zeros l JOIN nested_zeros r ON l.a = r.a ORDER 
BY l.id, r.id;
+----
+1 1
+1 2
+2 1
+2 2
+
+statement ok
+DROP TABLE nested_zeros;
diff --git a/datafusion/sqllogictest/test_files/spark/misc/equal_null.slt 
b/datafusion/sqllogictest/test_files/spark/misc/equal_null.slt
index 71a3af6070..1a12668f60 100644
--- a/datafusion/sqllogictest/test_files/spark/misc/equal_null.slt
+++ b/datafusion/sqllogictest/test_files/spark/misc/equal_null.slt
@@ -23,25 +23,213 @@
 
 ## Original Query: SELECT equal_null(1, '11');
 ## PySpark 3.5.5 Result: {'equal_null(1, 11)': False, 'typeof(equal_null(1, 
11))': 'boolean', 'typeof(1)': 'int', 'typeof(11)': 'string'}
-#query
-#SELECT equal_null(1::int, '11'::string);
+query B
+SELECT equal_null(1::int, '11'::string);
+----
+false
 
 ## Original Query: SELECT equal_null(3, 3);
 ## PySpark 3.5.5 Result: {'equal_null(3, 3)': True, 'typeof(equal_null(3, 
3))': 'boolean', 'typeof(3)': 'int'}
-#query
-#SELECT equal_null(3::int);
+query B
+SELECT equal_null(3::int, 3::int);
+----
+true
 
 ## Original Query: SELECT equal_null(NULL, 'abc');
 ## PySpark 3.5.5 Result: {'equal_null(NULL, abc)': False, 
'typeof(equal_null(NULL, abc))': 'boolean', 'typeof(NULL)': 'void', 
'typeof(abc)': 'string'}
-#query
-#SELECT equal_null(NULL::void, 'abc'::string);
+query B
+SELECT equal_null(NULL, 'abc'::string);
+----
+false
 
 ## Original Query: SELECT equal_null(NULL, NULL);
 ## PySpark 3.5.5 Result: {'equal_null(NULL, NULL)': True, 
'typeof(equal_null(NULL, NULL))': 'boolean', 'typeof(NULL)': 'void'}
-#query
-#SELECT equal_null(NULL::void);
+query B
+SELECT equal_null(NULL, NULL);
+----
+true
 
 ## Original Query: SELECT equal_null(true, NULL);
 ## PySpark 3.5.5 Result: {'equal_null(true, NULL)': False, 
'typeof(equal_null(true, NULL))': 'boolean', 'typeof(true)': 'boolean', 
'typeof(NULL)': 'void'}
-#query
-#SELECT equal_null(true::boolean, NULL::void);
+query B
+SELECT equal_null(true::boolean, NULL);
+----
+false
+
+query B
+SELECT equal_null(NULL, true::boolean);
+----
+false
+
+query BB
+SELECT equal_null(1::int, 1::int), equal_null(1::int, 2::int);
+----
+true false
+
+query BB
+SELECT equal_null(NULL::int, 1::int), equal_null(NULL::int, NULL::int);
+----
+false true
+
+# EqualNullSafe is declared non-nullable in Spark, so the result is never NULL
+query B
+SELECT equal_null(NULL::int, NULL::int) IS NULL;
+----
+false
+
+query BB
+SELECT equal_null('abc'::string, 'abc'::string), equal_null('abc'::string, 
'abd'::string);
+----
+true false
+
+# The default UTF8_BINARY collation compares strings by byte
+query B
+SELECT equal_null('a'::string, 'A'::string);
+----
+false
+
+query BB
+SELECT equal_null(true, true), equal_null(true, false);
+----
+true false
+
+query BB
+SELECT equal_null(1::int, 1::bigint), equal_null(1::int, 1.0::double);
+----
+true true
+
+# Spark's float ordering makes NaN equal to itself, unlike IEEE-754
+query BB
+SELECT equal_null('NaN'::double, 'NaN'::double) AS d, equal_null('NaN'::float, 
'NaN'::float) AS f;
+----
+true true
+
+query BBB
+SELECT equal_null('NaN'::double, 1.0::double), equal_null('NaN'::double, 
NULL), equal_null('NaN'::double, 'Infinity'::double);
+----
+false false false
+
+# Spark's float ordering also makes -0.0 equal to 0.0
+query BB
+SELECT equal_null(0.0::double, -0.0::double) AS d, equal_null(0.0::float, 
-0.0::float) AS f;
+----
+true true
+
+query BB
+SELECT equal_null('Infinity'::double, 'Infinity'::double), 
equal_null('Infinity'::double, '-Infinity'::double);
+----
+true false
+
+statement ok
+CREATE TABLE equal_null_ints(id INT, a INT, b INT) AS VALUES
+(1, 1, 1),
+(2, 1, 2),
+(3, CAST(NULL AS INT), 1),
+(4, 1, CAST(NULL AS INT)),
+(5, CAST(NULL AS INT), CAST(NULL AS INT));
+
+query B
+SELECT equal_null(a, b) FROM equal_null_ints ORDER BY id;
+----
+true
+false
+false
+false
+true
+
+statement ok
+DROP TABLE equal_null_ints;
+
+statement ok
+CREATE TABLE equal_null_doubles(id INT, a DOUBLE, b DOUBLE) AS VALUES
+(1, 'NaN'::double, 'NaN'::double),
+(2, 0.0, -0.0),
+(3, 1.0, CAST(NULL AS DOUBLE)),
+(4, CAST(NULL AS DOUBLE), CAST(NULL AS DOUBLE));
+
+query B
+SELECT equal_null(a, b) FROM equal_null_doubles ORDER BY id;
+----
+true
+true
+false
+true
+
+statement ok
+DROP TABLE equal_null_doubles;
+
+query BB
+SELECT equal_null(array(1, 2), array(1, 2)), equal_null(array(1, 2), array(1, 
2, 3));
+----
+true false
+
+# Two NULLs in the same array slot compare equal, per Spark's array ordering
+query BB
+SELECT equal_null(array(1, NULL), array(1, NULL)), equal_null(array(1, NULL), 
array(1, 2));
+----
+true false
+
+query B
+SELECT equal_null(named_struct('a', 1), named_struct('a', 1));
+----
+true
+
+query BB
+SELECT equal_null(array(0.0::double), array(-0.0::double)), 
equal_null(named_struct('a', 0.0::double), named_struct('a', -0.0::double));
+----
+true true
+
+query B
+SELECT equal_null(1.0::decimal(2,1), 1.00::decimal(3,2));
+----
+true
+
+statement error Function 'equal_null' expects 2 arguments but received 1
+SELECT equal_null(1::int);
+
+statement error Function 'equal_null' expects 2 arguments but received 3
+SELECT equal_null(1::int, 2::int, 3::int);
+
+# Without the simplify() rewrite the function runs its own kernel, which Comet 
relies on
+statement ok
+set datafusion.optimizer.max_passes = 0;
+
+query BBBBB
+SELECT equal_null(NULL, NULL), equal_null(0.0::double, -0.0::double), 
equal_null('NaN'::double, 'NaN'::double), equal_null(array(1, NULL), array(1, 
NULL)), equal_null(array(0.0::double), array(-0.0::double));
+----
+true true true true true
+
+statement ok
+CREATE TABLE equal_null_physical(id INT, a INT, b INT) AS VALUES
+(1, 1, 1),
+(2, 1, CAST(NULL AS INT)),
+(3, CAST(NULL AS INT), CAST(NULL AS INT));
+
+query B
+SELECT equal_null(a, b) FROM equal_null_physical ORDER BY id;
+----
+true
+false
+true
+
+statement ok
+DROP TABLE equal_null_physical;
+
+statement ok
+set datafusion.explain.show_schema = true;
+
+query TT
+EXPLAIN SELECT equal_null(NULL::int, NULL::int);
+----
+logical_plan
+01)Projection: equal_null(CAST(NULL AS Int32), CAST(NULL AS Int32))
+02)--EmptyRelation: rows=1
+physical_plan
+01)ProjectionExec: expr=[equal_null(CAST(NULL AS Int32), CAST(NULL AS Int32)) 
as equal_null(NULL,NULL)], schema=[equal_null(NULL,NULL):Boolean]
+02)--PlaceholderRowExec, schema=[]
+
+statement ok
+reset datafusion.explain.show_schema;
+
+statement ok
+reset datafusion.optimizer.max_passes;


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

Reply via email to