Skip to content

Commit 30f92df

Browse files
committed
GH-50338: [C++] Add ComputeLogicalNullCount to Datum
Datum::null_count() does not account for types that carry logical nulls without a validity bitmap (union, dictionary and run-end encoded types). Add Datum::ComputeLogicalNullCount(), delegating to ArrayData::ComputeLogicalNullCount() and ChunkedArray::ComputeLogicalNullCount() for array-like data; for scalars, is_valid already reflects logical validity.
1 parent 98347d2 commit 30f92df

3 files changed

Lines changed: 82 additions & 1 deletion

File tree

cpp/src/arrow/datum.cc

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,26 @@ int64_t Datum::null_count() const {
142142
const auto& val = *std::get<std::shared_ptr<Scalar>>(this->value);
143143
return val.is_valid ? 0 : 1;
144144
} else {
145-
DCHECK(false) << "This function only valid for array-like values";
145+
DCHECK(false) << "This function only valid for scalar or array-like values";
146+
return 0;
147+
}
148+
}
149+
150+
int64_t Datum::ComputeLogicalNullCount() const {
151+
if (this->kind() == Datum::ARRAY) {
152+
return std::get<std::shared_ptr<ArrayData>>(this->value)->ComputeLogicalNullCount();
153+
} else if (this->kind() == Datum::CHUNKED_ARRAY) {
154+
return std::get<std::shared_ptr<ChunkedArray>>(this->value)
155+
->ComputeLogicalNullCount();
156+
} else if (this->kind() == Datum::SCALAR) {
157+
// Union and run-end encoded scalars derive is_valid from their underlying
158+
// value, so it reflects logical validity. A DictionaryScalar's is_valid
159+
// only tracks index validity, so this differs from the array path when a
160+
// valid index points to a null dictionary value.
161+
const auto& val = *std::get<std::shared_ptr<Scalar>>(this->value);
162+
return val.is_valid ? 0 : 1;
163+
} else {
164+
DCHECK(false) << "This function only valid for scalar or array-like values";
146165
return 0;
147166
}
148167
}

cpp/src/arrow/datum.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,20 @@ struct ARROW_EXPORT Datum {
276276
/// Only valid for scalar and array-like data.
277277
int64_t null_count() const;
278278

279+
/// \brief Compute the logical null count.
280+
///
281+
/// Only valid for scalar and array-like data. Unlike null_count(), this
282+
/// accounts for types whose logical nulls are not captured by the top-level
283+
/// validity bitmap, such as union, run-end encoded and dictionary types; for
284+
/// those types the count is recomputed on every call. For scalars this
285+
/// returns the same value as null_count(); note that a DictionaryScalar
286+
/// counts as non-null whenever its index is valid, even if the index points
287+
/// to a null dictionary value.
288+
///
289+
/// \see ArrayData::ComputeLogicalNullCount
290+
/// \see ChunkedArray::ComputeLogicalNullCount
291+
int64_t ComputeLogicalNullCount() const;
292+
279293
/// \brief The value type of the variant, if any
280294
///
281295
/// \return nullptr if no type

cpp/src/arrow/datum_test.cc

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,54 @@ TEST(Datum, NullCount) {
139139
ASSERT_EQ(3, val3.null_count());
140140
}
141141

142+
TEST(Datum, ComputeLogicalNullCount) {
143+
// For scalars, is_valid already reflects logical validity.
144+
Datum val1(std::make_shared<Int8Scalar>(1));
145+
ASSERT_EQ(0, val1.ComputeLogicalNullCount());
146+
147+
Datum val2(MakeNullScalar(int8()));
148+
ASSERT_EQ(1, val2.ComputeLogicalNullCount());
149+
150+
// For arrays with a validity bitmap, the logical null count matches
151+
// null_count().
152+
Datum val3(ArrayFromJSON(int8(), "[1, null, null, null]"));
153+
ASSERT_EQ(3, val3.null_count());
154+
ASSERT_EQ(3, val3.ComputeLogicalNullCount());
155+
156+
// Union arrays carry logical nulls in their children without a top-level
157+
// validity bitmap, so null_count() is 0 while the logical null count is not.
158+
auto union_type = sparse_union({field("a", int8()), field("b", boolean())});
159+
auto union_arr =
160+
ArrayFromJSON(union_type, R"([[0, null], [1, true], [0, 5], [1, null]])");
161+
Datum val4(union_arr);
162+
ASSERT_EQ(0, val4.null_count());
163+
ASSERT_EQ(2, val4.ComputeLogicalNullCount());
164+
165+
// Chunked arrays sum the logical null count over the chunks.
166+
auto union_chunk = ArrayFromJSON(union_type, R"([[0, 1], [1, null]])");
167+
ASSERT_OK_AND_ASSIGN(auto chunked, ChunkedArray::Make({union_arr, union_chunk}));
168+
Datum val5(chunked);
169+
ASSERT_EQ(0, val5.null_count());
170+
ASSERT_EQ(3, val5.ComputeLogicalNullCount());
171+
172+
// Dictionary arrays have a validity bitmap on the indices, but a valid
173+
// index referencing a null dictionary value is also a logical null.
174+
auto dict_type = dictionary(int32(), utf8());
175+
auto dict_arr = DictArrayFromJSON(dict_type, /*indices=*/"[0, 1, null, 1]",
176+
/*dictionary=*/R"([null, "a"])");
177+
Datum val6(dict_arr);
178+
ASSERT_EQ(1, val6.null_count());
179+
ASSERT_EQ(2, val6.ComputeLogicalNullCount());
180+
181+
// A DictionaryScalar's is_valid only reflects index validity, so unlike the
182+
// array path, a valid index referencing a null dictionary value does not
183+
// count as a logical null.
184+
ASSERT_OK_AND_ASSIGN(auto dict_scalar, dict_arr->GetScalar(0));
185+
Datum val7(dict_scalar);
186+
ASSERT_EQ(0, val7.null_count());
187+
ASSERT_EQ(0, val7.ComputeLogicalNullCount());
188+
}
189+
142190
TEST(Datum, MutableArray) {
143191
auto arr = ArrayFromJSON(int8(), "[1, 2, 3, 4]");
144192

0 commit comments

Comments
 (0)