This is an automated email from the ASF dual-hosted git repository.
pitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new c6620b47de GH-51127: [C++] Fix is_null nan_is_null for
dictionary-encoded float arrays (#51000)
c6620b47de is described below
commit c6620b47de82f7ce5d41f4882a0fe68a40720257
Author: Jeremy Schoemaker <[email protected]>
AuthorDate: Mon Sep 28 09:34:04 2026 -0500
GH-51127: [C++] Fix is_null nan_is_null for dictionary-encoded float arrays
(#51000)
### Rationale for this change
`is_null(..., nan_is_null=true)` did not detect NaN values stored in a
dictionary's values, so NaN entries in dictionary-encoded
float/double/half-float arrays were reported as valid instead of null. Null
dictionary values must also keep being reported as null with both default
options and `nan_is_null=true`.
### What changes are included in this PR?
Adds NaN and null detection to `is_null` for dictionary-encoded
floating-point arrays. For dictionaries of floating types, the output bitmap is
built by calling `is_null` on the dictionary values (which also catches NaN
when `nan_is_null=true`), mapping that through the indices with `take`, and
OR-ing it into the existing null-index bitmap. `take` runs without bounds
checking, matching how other compute kernels treat dictionary indices as
already valid. Every other `is_null` path is u [...]
### Are these changes tested?
Yes. A single parameterized test in `scalar_validity_test.cc` covers
float16, float32, and float64 dictionaries: NaN values, null dictionary
entries, null indices, default options, and empty arrays.
### Are there any user-facing changes?
Yes. `is_null(..., nan_is_null=true)` now reports NaN entries in
dictionary-encoded floating-point arrays as null. Null dictionary values were
already reported as null and remain so.
* GitHub Issue: #51127
Authored-by: Jeremy Schoemaker <[email protected]>
Signed-off-by: Antoine Pitrou <[email protected]>
---
cpp/src/arrow/compute/kernels/scalar_validity.cc | 53 +++++++++++++++++++---
.../arrow/compute/kernels/scalar_validity_test.cc | 33 ++++++++++++++
2 files changed, 80 insertions(+), 6 deletions(-)
diff --git a/cpp/src/arrow/compute/kernels/scalar_validity.cc
b/cpp/src/arrow/compute/kernels/scalar_validity.cc
index c995b8dff8..0829af59dc 100644
--- a/cpp/src/arrow/compute/kernels/scalar_validity.cc
+++ b/cpp/src/arrow/compute/kernels/scalar_validity.cc
@@ -21,8 +21,11 @@
#include "arrow/compute/kernels/common_internal.h"
#include "arrow/compute/registry_internal.h"
+#include "arrow/compute/api_vector.h"
+#include "arrow/type.h"
#include "arrow/util/bit_util.h"
#include "arrow/util/bitmap_ops.h"
+#include "arrow/util/checked_cast.h"
#include "arrow/util/dict_util_internal.h"
#include "arrow/util/float16.h"
#include "arrow/util/logging_internal.h"
@@ -80,10 +83,42 @@ static void SetNanBits(const ArraySpan& arr, uint8_t*
out_bitmap, int64_t out_of
}
}
+// Maps `is_null` over dictionary values and then through the indices, so
+// both NaN and null dictionary entries are reported, whatever the index type.
+static Status SetNanBitsFromDictionary(KernelContext* ctx, const ArraySpan&
arr,
+ uint8_t* out_bitmap, int64_t
out_offset) {
+ if (arr.length == 0) {
+ return Status::OK();
+ }
+ NullOptions nan_is_null_options(/*nan_is_null=*/true);
+ ARROW_ASSIGN_OR_RAISE(Datum dict_is_null,
+ CallFunction("is_null",
{arr.dictionary().ToArrayData()},
+ &nan_is_null_options,
ctx->exec_context()));
+
+ const auto& dict_type = checked_cast<const DictionaryType&>(*arr.type);
+ auto indices = ArrayData::Make(dict_type.index_type(), arr.length,
+ {arr.GetBuffer(0), arr.GetBuffer(1)},
arr.GetNullCount(),
+ arr.offset);
+ ARROW_ASSIGN_OR_RAISE(Datum taken,
+ Take(dict_is_null, Datum(std::move(indices)),
+ TakeOptions::NoBoundsCheck(),
ctx->exec_context()));
+
+ const ArrayData& result = *taken.array();
+ if (arr.GetNullCount() > 0) {
+ ::arrow::internal::BitmapOrNot(result.buffers[1]->data(), result.offset,
+ arr.buffers[0].data, arr.offset, arr.length,
+ out_offset, out_bitmap);
+ } else {
+ CopyBitmap(result.buffers[1]->data(), result.offset, arr.length,
out_bitmap,
+ out_offset);
+ }
+ return Status::OK();
+}
+
// `nan_is_null` can only be true for the is_null kernel since the is_valid and
// true_unless_null kernels currently do not take `NullOptions`
-Status SetLogicalNullBits(const ArraySpan& span, uint8_t* out_bitmap, int64_t
out_offset,
- bool set_on_null, bool nan_is_null) {
+Status SetLogicalNullBits(KernelContext* ctx, const ArraySpan& span, uint8_t*
out_bitmap,
+ int64_t out_offset, bool set_on_null, bool
nan_is_null) {
const Type::type t = span.type->id();
if (t == Type::NA) {
// Input is all nulls, so all output bits are the same.
@@ -98,7 +133,10 @@ Status SetLogicalNullBits(const ArraySpan& span, uint8_t*
out_bitmap, int64_t ou
// TODO: propagate `nan_is_null`
ree_util::SetLogicalNullBits(span, out_bitmap, out_offset, set_on_null);
} else if (t == Type::DICTIONARY) {
- // TODO: propagate `nan_is_null`
+ const auto& dict_type = checked_cast<const DictionaryType&>(*span.type);
+ if (nan_is_null && is_floating(dict_type.value_type()->id())) {
+ return SetNanBitsFromDictionary(ctx, span, out_bitmap, out_offset);
+ }
dict_util::SetLogicalNullBits(span, out_bitmap, out_offset, set_on_null);
} else {
// Input is a type for which logical and physical nulls are the same, so
we can
@@ -141,13 +179,15 @@ Status SetLogicalNullBits(const ArraySpan& span, uint8_t*
out_bitmap, int64_t ou
Status IsValidExec(KernelContext* ctx, const ExecSpan& batch, ExecResult* out)
{
ArraySpan* out_span = out->array_span_mutable();
- return SetLogicalNullBits(batch[0].array, out_span->buffers[1].data,
out_span->offset,
+ return SetLogicalNullBits(ctx, batch[0].array, out_span->buffers[1].data,
+ out_span->offset,
/*set_on_null=*/false, /*nan_is_null=*/false);
}
Status IsNullExec(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) {
ArraySpan* out_span = out->array_span_mutable();
- return SetLogicalNullBits(batch[0].array, out_span->buffers[1].data,
out_span->offset,
+ return SetLogicalNullBits(ctx, batch[0].array, out_span->buffers[1].data,
+ out_span->offset,
/*set_on_null=*/true,
/*nan_is_null=*/NanOptionsState::Get(ctx).nan_is_null);
}
@@ -265,7 +305,8 @@ Status TrueUnlessNullExec(KernelContext* ctx, const
ExecSpan& batch, ExecResult*
// NullHandling::INTERSECTION and change the validity checks in exec.cc so
that
// they correctly handle logical nulls, but that would invove significant
changes
// in exec.cc which might have more side effects
- return SetLogicalNullBits(batch[0].array, out_span->buffers[0].data,
out_span->offset,
+ return SetLogicalNullBits(ctx, batch[0].array, out_span->buffers[0].data,
+ out_span->offset,
/*set_on_null=*/false, /*nan_is_null=*/false);
}
diff --git a/cpp/src/arrow/compute/kernels/scalar_validity_test.cc
b/cpp/src/arrow/compute/kernels/scalar_validity_test.cc
index 86895cdcec..0a041e7e47 100644
--- a/cpp/src/arrow/compute/kernels/scalar_validity_test.cc
+++ b/cpp/src/arrow/compute/kernels/scalar_validity_test.cc
@@ -208,6 +208,39 @@ TEST(TestValidityKernels, IsNullSetsZeroNullCount) {
ASSERT_EQ(out.array()->null_count, 0);
}
+TEST(TestValidityKernels, IsNullDictionaryNanIsNull) {
+ NullOptions default_options;
+ NullOptions nan_is_null_options(/*nan_is_null=*/true);
+
+ for (const auto& value_type : {float16(), float32(), float64()}) {
+ SCOPED_TRACE(value_type->ToString());
+ auto dict_ty = dictionary(int32(), value_type);
+ auto arr =
+ DictArrayFromJSON(dict_ty, "[0, 1, 2, 3, null, 1]", "[1.5, NaN, -0.0,
null]");
+
+ // Null dictionary values and null indices are always null.
+ CheckScalarUnary(
+ "is_null", arr,
+ ArrayFromJSON(boolean(), "[false, false, false, true, true, false]"));
+ CheckScalarUnary("is_null", arr,
+ ArrayFromJSON(boolean(), "[false, false, false, true,
true, false]"),
+ &default_options);
+ CheckScalarUnary("is_null", arr,
+ ArrayFromJSON(boolean(), "[false, true, false, true,
true, true]"),
+ &nan_is_null_options);
+
+ auto no_null_indices =
+ DictArrayFromJSON(dict_ty, "[3, 1, 0, 2]", "[1.5, NaN, -0.0, null]");
+ CheckScalarUnary("is_null", no_null_indices,
+ ArrayFromJSON(boolean(), "[true, true, false, false]"),
+ &nan_is_null_options);
+
+ auto empty = DictArrayFromJSON(dict_ty, "[]", "[]");
+ CheckScalarUnary("is_null", empty, ArrayFromJSON(boolean(), "[]"),
+ &nan_is_null_options);
+ }
+}
+
template <typename ArrowType>
class TestFloatingPointValidityKernels : public TestValidityKernels<ArrowType>
{
public: