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 723937960d GH-50223: [C++][Compute] Support string_view/binary_view
keys in the hash-aggregate Grouper (#50224)
723937960d is described below
commit 723937960de67dd62fbf4e6dfeff590efe19ecdd
Author: Fangchen Li <[email protected]>
AuthorDate: Tue Jul 7 01:54:04 2026 -0700
GH-50223: [C++][Compute] Support string_view/binary_view keys in the
hash-aggregate Grouper (#50224)
### Rationale for this change
Grouper rejected `string_view` and `binary_view` group keys with
`NotImplemented: Keys of type string_view`, so `Table.group_by` on a view-typed
key failed.
### What changes are included in this PR?
- Add `BinaryViewKeyEncoder` in `row_encoder_internal.{h,cc}`. It encodes
view keys using the existing variable-length row format and decodes them into a
fresh view array of the original type.
- `GrouperImpl::Make` dispatches view keys to the new encoder.
- `GrouperFastImpl::CanUse` rejects view keys, so they fall back to
GrouperImpl.
### Are these changes tested?
Yes, unit tests added.
### Are there any user-facing changes?
Yes. `Table.group_by` now accepts `string_view` and `binary_view` group keys
* GitHub Issue: #50223
Authored-by: Fangchen Li <[email protected]>
Signed-off-by: Antoine Pitrou <[email protected]>
---
cpp/src/arrow/compute/row/grouper.cc | 11 ++-
cpp/src/arrow/compute/row/grouper_benchmark.cc | 23 ++++++-
cpp/src/arrow/compute/row/grouper_test.cc | 80 +++++++++++++++++++++-
cpp/src/arrow/compute/row/row_encoder_internal.cc | 34 +++++++++
cpp/src/arrow/compute/row/row_encoder_internal.h | 50 +++++++++++---
.../arrow/compute/row/row_encoder_internal_test.cc | 70 +++++++++++++++++++
6 files changed, 252 insertions(+), 16 deletions(-)
diff --git a/cpp/src/arrow/compute/row/grouper.cc
b/cpp/src/arrow/compute/row/grouper.cc
index a342e5a6b1..d3eafef2cf 100644
--- a/cpp/src/arrow/compute/row/grouper.cc
+++ b/cpp/src/arrow/compute/row/grouper.cc
@@ -373,6 +373,12 @@ struct GrouperImpl : public Grouper {
continue;
}
+ if (is_binary_view_like(key_type.id())) {
+ impl->encoders_[i] =
+
std::make_unique<internal::BinaryViewKeyEncoder>(key_type.GetSharedPtr());
+ continue;
+ }
+
if (key_type.id() == Type::NA) {
impl->encoders_[i] = std::make_unique<internal::NullKeyEncoder>();
continue;
@@ -556,7 +562,10 @@ struct GrouperFastImpl : public Grouper {
}
#if ARROW_LITTLE_ENDIAN
for (size_t i = 0; i < key_types.size(); ++i) {
- if (is_large_binary_like(key_types[i].id())) {
+ // View keys fall back to GrouperImpl; the fast row format can't hold
+ // their variadic buffers.
+ if (is_large_binary_like(key_types[i].id()) ||
+ is_binary_view_like(key_types[i].id())) {
return false;
}
}
diff --git a/cpp/src/arrow/compute/row/grouper_benchmark.cc
b/cpp/src/arrow/compute/row/grouper_benchmark.cc
index 1e1a16d579..6dec5d6345 100644
--- a/cpp/src/arrow/compute/row/grouper_benchmark.cc
+++ b/cpp/src/arrow/compute/row/grouper_benchmark.cc
@@ -20,9 +20,11 @@
#include "arrow/util/key_value_metadata.h"
#include "arrow/util/string.h"
+#include "arrow/compute/cast.h"
#include "arrow/compute/row/grouper.h"
#include "arrow/testing/gtest_util.h"
#include "arrow/testing/random.h"
+#include "arrow/type_traits.h"
#include "arrow/util/benchmark_util.h"
namespace arrow {
@@ -62,8 +64,20 @@ static ExecBatch MakeRandomExecBatch(const DataTypeVector&
types, int64_t num_ro
std::vector<Datum> values;
values.resize(num_types);
for (int i = 0; i < num_types; ++i) {
- auto field = ::arrow::field("", types[i], metadata);
- values[i] = rng.ArrayOf(*field, num_rows, alignment, memory_pool);
+ // The "unique" cardinality knob isn't honored for view types, so generate
the
+ // plain equivalent and cast it (outside the timed loop) to match {utf8}.
+ const auto& type = types[i];
+ const bool is_view = is_binary_view_like(*type);
+ std::shared_ptr<DataType> gen_type = type;
+ if (is_view) {
+ gen_type = type->id() == Type::STRING_VIEW ? utf8() : binary();
+ }
+ auto field = ::arrow::field("", gen_type, metadata);
+ Datum value = rng.ArrayOf(*field, num_rows, alignment, memory_pool);
+ if (is_view) {
+ ASSIGN_OR_ABORT(value, compute::Cast(value, type));
+ }
+ values[i] = std::move(value);
}
return ExecBatch(std::move(values), num_rows);
@@ -125,6 +139,8 @@ BENCHMARK_CAPTURE(GrouperWithMultiTypes, "{boolean}",
{boolean()})->Apply(SetArg
BENCHMARK_CAPTURE(GrouperWithMultiTypes, "{int32}", {int32()})->Apply(SetArgs);
BENCHMARK_CAPTURE(GrouperWithMultiTypes, "{int64}", {int64()})->Apply(SetArgs);
BENCHMARK_CAPTURE(GrouperWithMultiTypes, "{utf8}", {utf8()})->Apply(SetArgs);
+// View keys use the generic GrouperImpl path (the {utf8} fast path rejects
them).
+BENCHMARK_CAPTURE(GrouperWithMultiTypes, "{utf8_view}",
{utf8_view()})->Apply(SetArgs);
BENCHMARK_CAPTURE(GrouperWithMultiTypes, "{fixed_size_binary(32)}",
{fixed_size_binary(32)})
->Apply(SetArgs);
@@ -147,6 +163,9 @@ BENCHMARK_CAPTURE(GrouperWithMultiTypes, "{int32, boolean,
utf8}",
BENCHMARK_CAPTURE(GrouperWithMultiTypes, "{int32, int64, boolean, utf8}",
{int32(), int64(), boolean(), utf8()})
->Apply(SetArgs);
+BENCHMARK_CAPTURE(GrouperWithMultiTypes, "{int32, int64, boolean, utf8_view}",
+ {int32(), int64(), boolean(), utf8_view()})
+ ->Apply(SetArgs);
BENCHMARK_CAPTURE(GrouperWithMultiTypes,
"{utf8, int32, int64, fixed_size_binary(32), boolean}",
{utf8(), int32(), int64(), fixed_size_binary(32), boolean()})
diff --git a/cpp/src/arrow/compute/row/grouper_test.cc
b/cpp/src/arrow/compute/row/grouper_test.cc
index 901d1b6178..edbf07e785 100644
--- a/cpp/src/arrow/compute/row/grouper_test.cc
+++ b/cpp/src/arrow/compute/row/grouper_test.cc
@@ -23,6 +23,7 @@
#include "arrow/array.h"
#include "arrow/array/util.h"
#include "arrow/compute/api_vector.h"
+#include "arrow/compute/cast.h"
#include "arrow/compute/exec.h"
#include "arrow/compute/row/grouper.h"
#include "arrow/compute/row/grouper_internal.h"
@@ -120,6 +121,8 @@ void TestGroupClassSupportedKeys(
ASSERT_OK(make_func({utf8(), binary(), large_utf8(), large_binary()}));
+ ASSERT_OK(make_func({utf8_view(), binary_view()}));
+
ASSERT_OK(make_func({fixed_size_binary(16), fixed_size_binary(32)}));
ASSERT_OK(make_func({decimal128(32, 10), decimal256(76, 20)}));
@@ -754,13 +757,24 @@ struct TestGrouper {
auto group_ids = id_batch.make_array();
ValidateOutput(*group_ids);
+ // Take has no view kernel yet (GH-43010); validate via the non-view cast.
+ // Output type is checked separately by ExpectUniques.
+ auto as_takeable = [](std::shared_ptr<Array> arr) ->
std::shared_ptr<Array> {
+ if (is_binary_view_like(*arr->type())) {
+ auto target = arr->type_id() == Type::STRING_VIEW ? utf8() : binary();
+ arr = Cast(Datum(arr), target).ValueOrDie().make_array();
+ }
+ return arr;
+ };
+
for (int i = 0; i < key_batch.num_values(); ++i) {
SCOPED_TRACE(ToChars(i) + "th key array");
auto original =
key_batch[i].is_array()
? key_batch[i].make_array()
: *MakeArrayFromScalar(*key_batch[i].scalar(), key_batch.length);
- ASSERT_OK_AND_ASSIGN(auto encoded, Take(*uniques_[i].make_array(),
*group_ids));
+ ASSERT_OK_AND_ASSIGN(auto encoded,
+ Take(*as_takeable(uniques_[i].make_array()),
*group_ids));
std::shared_ptr<Array> expected = original;
if (can_be_null && original->type_id() != Type::NA) {
// To compute the expected output, mask out the original entries that
@@ -791,7 +805,7 @@ struct TestGrouper {
expected_data->null_count = kUnknownNullCount;
expected = MakeArray(expected_data);
}
- AssertArraysEqual(*expected, *encoded, /*verbose=*/true,
+ AssertArraysEqual(*as_takeable(expected), *encoded, /*verbose=*/true,
EqualOptions().nans_equal(true));
}
}
@@ -913,6 +927,48 @@ TEST(Grouper, StringKey) {
}
}
+TEST(Grouper, StringViewKey) {
+ // Mix inline (<=12 byte) and out-of-line view keys.
+ for (auto ty : {utf8_view(), binary_view()}) {
+ ARROW_SCOPED_TRACE("key type = ", *ty);
+ {
+ TestGrouper g({ty});
+ g.ExpectConsume(R"([["eh"], ["eh"]])", "[0, 0]");
+ g.ExpectConsume(R"([["eh"], ["eh"]])", "[0, 0]");
+ g.ExpectConsume(R"([["be"], [null]])", "[1, 2]");
+ // Several distinct out-of-line (>12 byte) views, including two that
share
+ // the same 4-byte inline prefix.
+ g.ExpectConsume(
+ R"([["a long out-of-line view"], ["a long out-of-line view"],
+ ["a different long view"], ["a distinct long-ish view"]])",
+ "[3, 3, 4, 5]");
+ g.ExpectUniques(
+ R"([["eh"], ["be"], [null], ["a long out-of-line view"],
+ ["a different long view"], ["a distinct long-ish view"]])");
+ }
+ {
+ TestGrouper g({ty});
+ g.ExpectPopulate(R"([["eh"], ["eh"]])");
+ g.ExpectPopulate(R"([["be"], [null]])");
+ g.ExpectLookup(R"([["be"], [null], ["da"]])", "[1, 2, null]");
+ }
+ }
+}
+
+TEST(Grouper, StringViewInt64Key) {
+ for (auto ty : {utf8_view(), binary_view()}) {
+ ARROW_SCOPED_TRACE("key type = ", *ty);
+ TestGrouper g({ty, int64()});
+
+ g.ExpectConsume(R"([["eh", 0], ["eh", 0]])", "[0, 0]");
+ g.ExpectConsume(R"([["eh", 0], ["eh", null]])", "[0, 1]");
+ g.ExpectConsume(R"([["eh", 1], ["bee", 1]])", "[2, 3]");
+ g.ExpectConsume(R"([["eh", null], ["bee", 1]])", "[1, 3]");
+
+ g.ExpectUniques(R"([["eh", 0], ["eh", null], ["eh", 1], ["bee", 1]])");
+ }
+}
+
TEST(Grouper, DictKey) {
// For dictionary keys, all batches must share a single dictionary.
// Eventually, differing dictionaries will be unified and indices transposed
@@ -1074,6 +1130,10 @@ FieldVector AnnotateForRandomGeneration(FieldVector
fields) {
} else if (is_binary_like(*field->type())) {
// (note this is unsupported for large binary types)
field = field->WithMergedMetadata(key_value_metadata({"unique"},
{"100"}));
+ } else if (is_binary_view_like(*field->type())) {
+ // Views don't support the "unique" knob; small lengths keep cardinality
low.
+ field = field->WithMergedMetadata(
+ key_value_metadata({"min_length", "max_length"}, {"0", "5"}));
}
field = field->WithMergedMetadata(key_value_metadata({"null_probability"},
{"0.1"}));
}
@@ -1130,6 +1190,22 @@ TEST(Grouper, RandomStringInt64DoubleInt32Keys) {
TestRandomLookup(TestGrouper({utf8(), int64(), float64(), int32()}));
}
+TEST(Grouper, RandomStringViewKeys) {
+ for (auto ty : {utf8_view(), binary_view()}) {
+ ARROW_SCOPED_TRACE("key type = ", *ty);
+ TestRandomConsume(TestGrouper({ty}));
+ TestRandomLookup(TestGrouper({ty}));
+ }
+}
+
+TEST(Grouper, RandomStringViewInt64Keys) {
+ for (auto ty : {utf8_view(), binary_view()}) {
+ ARROW_SCOPED_TRACE("key type = ", *ty);
+ TestRandomConsume(TestGrouper({ty, int64()}));
+ TestRandomLookup(TestGrouper({ty, int64()}));
+ }
+}
+
TEST(Grouper, NullKeys) {
{
TestGrouper g({null()});
diff --git a/cpp/src/arrow/compute/row/row_encoder_internal.cc
b/cpp/src/arrow/compute/row/row_encoder_internal.cc
index eefc2ffb2f..ccf6642b95 100644
--- a/cpp/src/arrow/compute/row/row_encoder_internal.cc
+++ b/cpp/src/arrow/compute/row/row_encoder_internal.cc
@@ -17,6 +17,7 @@
#include "arrow/compute/row/row_encoder_internal.h"
+#include "arrow/array/builder_binary.h"
#include "arrow/util/bitmap_writer.h"
#include "arrow/util/logging_internal.h"
@@ -256,6 +257,34 @@ Result<std::shared_ptr<ArrayData>>
DictionaryKeyEncoder::Decode(uint8_t** encode
return data;
}
+Result<std::shared_ptr<ArrayData>> BinaryViewKeyEncoder::Decode(uint8_t**
encoded_bytes,
+ int32_t length,
+ MemoryPool*
pool) {
+ // Build a fresh view array; MakeBuilder gives the type-faithful builder and
+ // Append handles inline vs. out-of-line storage.
+ std::unique_ptr<ArrayBuilder> builder;
+ RETURN_NOT_OK(MakeBuilder(pool, type_, &builder));
+ auto& view_builder = checked_cast<BinaryViewBuilder&>(*builder);
+ RETURN_NOT_OK(view_builder.Reserve(length));
+
+ for (int32_t i = 0; i < length; ++i) {
+ uint8_t*& encoded_ptr = encoded_bytes[i];
+ const bool is_valid = (*encoded_ptr++ == kValidByte);
+ const auto key_length = util::SafeLoadAs<Offset>(encoded_ptr);
+ encoded_ptr += sizeof(Offset);
+ if (is_valid) {
+ RETURN_NOT_OK(view_builder.Append(encoded_ptr, key_length));
+ } else {
+ RETURN_NOT_OK(view_builder.AppendNull());
+ }
+ encoded_ptr += key_length; // zero for null rows
+ }
+
+ std::shared_ptr<Array> out;
+ RETURN_NOT_OK(view_builder.Finish(&out));
+ return out->data();
+}
+
void RowEncoder::Init(const std::vector<TypeHolder>& column_types,
ExecContext* ctx) {
ctx_ = ctx;
encoders_.resize(column_types.size());
@@ -301,6 +330,11 @@ void RowEncoder::Init(const std::vector<TypeHolder>&
column_types, ExecContext*
continue;
}
+ if (is_binary_view_like(type.id())) {
+ encoders_[i] =
std::make_shared<BinaryViewKeyEncoder>(type.GetSharedPtr());
+ continue;
+ }
+
// We should not get here
ARROW_DCHECK(false);
}
diff --git a/cpp/src/arrow/compute/row/row_encoder_internal.h
b/cpp/src/arrow/compute/row/row_encoder_internal.h
index 9337e78bf8..66595f3ef3 100644
--- a/cpp/src/arrow/compute/row/row_encoder_internal.h
+++ b/cpp/src/arrow/compute/row/row_encoder_internal.h
@@ -137,15 +137,22 @@ struct ARROW_COMPUTE_EXPORT DictionaryKeyEncoder :
FixedWidthKeyEncoder {
std::shared_ptr<Array> dictionary_;
};
-template <typename T>
-struct VarLengthKeyEncoder : KeyEncoder {
- using Offset = typename T::offset_type;
+// Shared implementation of the variable-length row encoding used by the plain
+// binary/string encoder and the view encoder. The on-row format is
+// [null byte][length : Offset][raw key bytes]; only Decode differs between the
+// two, so subclasses override just that.
+template <typename VisitType, typename OffsetType>
+struct BaseVarLengthKeyEncoder : KeyEncoder {
+ using Offset = OffsetType;
+
+ explicit BaseVarLengthKeyEncoder(std::shared_ptr<DataType> type)
+ : type_(std::move(type)) {}
void AddLength(const ExecValue& data, int64_t batch_length, int32_t*
lengths) override {
if (data.is_array()) {
int64_t i = 0;
ARROW_DCHECK_EQ(data.array.length, batch_length);
- VisitArraySpanInline<T>(
+ VisitArraySpanInline<VisitType>(
data.array,
[&](std::string_view bytes) {
lengths[i++] +=
@@ -155,8 +162,9 @@ struct VarLengthKeyEncoder : KeyEncoder {
} else {
const Scalar& scalar = *data.scalar;
const int32_t buffer_size =
- scalar.is_valid ?
static_cast<int32_t>(UnboxScalar<T>::Unbox(scalar).size())
- : 0;
+ scalar.is_valid
+ ?
static_cast<int32_t>(UnboxScalar<VisitType>::Unbox(scalar).size())
+ : 0;
for (int64_t i = 0; i < batch_length; i++) {
lengths[i] += kExtraByteForNull + sizeof(Offset) + buffer_size;
}
@@ -185,8 +193,8 @@ struct VarLengthKeyEncoder : KeyEncoder {
};
if (data.is_array()) {
ARROW_DCHECK_EQ(data.length(), batch_length);
- VisitArraySpanInline<T>(data.array, handle_next_valid_value,
- handle_next_null_value);
+ VisitArraySpanInline<VisitType>(data.array, handle_next_valid_value,
+ handle_next_null_value);
} else {
const auto& scalar = data.scalar_as<BaseBinaryScalar>();
if (scalar.is_valid) {
@@ -210,11 +218,24 @@ struct VarLengthKeyEncoder : KeyEncoder {
encoded_ptr += sizeof(Offset);
}
+ std::shared_ptr<DataType> type_;
+};
+
+// Encodes plain binary/string keys, decoding back into the aliasable
+// offsets + data buffer layout.
+template <typename T>
+struct VarLengthKeyEncoder : BaseVarLengthKeyEncoder<T, typename
T::offset_type> {
+ using Base = BaseVarLengthKeyEncoder<T, typename T::offset_type>;
+ using Base::Base;
+ using Base::type_;
+ using typename Base::Offset;
+
Result<std::shared_ptr<ArrayData>> Decode(uint8_t** encoded_bytes, int32_t
length,
MemoryPool* pool) override {
std::shared_ptr<Buffer> null_buf;
int32_t null_count;
- ARROW_RETURN_NOT_OK(DecodeNulls(pool, length, encoded_bytes, &null_buf,
&null_count));
+ ARROW_RETURN_NOT_OK(
+ this->DecodeNulls(pool, length, encoded_bytes, &null_buf,
&null_count));
Offset length_sum = 0;
for (int32_t i = 0; i < length; ++i) {
@@ -246,10 +267,17 @@ struct VarLengthKeyEncoder : KeyEncoder {
type_, length, {std::move(null_buf), std::move(offset_buf),
std::move(key_buf)},
null_count);
}
+};
- explicit VarLengthKeyEncoder(std::shared_ptr<DataType> type) :
type_(std::move(type)) {}
+// Encodes BinaryView/StringView keys in the same row format as the plain
+// encoder above. The row copies the key bytes, so Decode builds a fresh view
+// array rather than aliasing the input's variadic buffers.
+struct ARROW_COMPUTE_EXPORT BinaryViewKeyEncoder
+ : BaseVarLengthKeyEncoder<BinaryViewType, int32_t> {
+ using BaseVarLengthKeyEncoder::BaseVarLengthKeyEncoder;
- std::shared_ptr<DataType> type_;
+ Result<std::shared_ptr<ArrayData>> Decode(uint8_t** encoded_bytes, int32_t
length,
+ MemoryPool* pool) override;
};
struct ARROW_COMPUTE_EXPORT NullKeyEncoder : KeyEncoder {
diff --git a/cpp/src/arrow/compute/row/row_encoder_internal_test.cc
b/cpp/src/arrow/compute/row/row_encoder_internal_test.cc
index 78839d1ead..22a72117e9 100644
--- a/cpp/src/arrow/compute/row/row_encoder_internal_test.cc
+++ b/cpp/src/arrow/compute/row/row_encoder_internal_test.cc
@@ -20,7 +20,9 @@
#include "arrow/compute/row/row_encoder_internal.h"
+#include "arrow/array.h"
#include "arrow/array/validate.h"
+#include "arrow/scalar.h"
#include "arrow/testing/gtest_util.h"
#include "arrow/type.h"
#include "arrow/type_fwd.h"
@@ -65,4 +67,72 @@ TEST(TestKeyEncoder, BooleanScalar) {
}
}
+// Encodes `value` (an array, or a scalar repeated `length` times) and decodes
it
+// back, laying out per-row buffers the way RowEncoder does.
+Result<std::shared_ptr<ArrayData>> RoundTripThroughKeyEncoder(KeyEncoder*
encoder,
+ const ExecValue&
value,
+ int32_t length) {
+ std::vector<int32_t> lengths(length, 0);
+ encoder->AddLength(value, length, lengths.data());
+
+ std::vector<int32_t> offsets(length + 1, 0);
+ for (int32_t i = 0; i < length; ++i) {
+ offsets[i + 1] = offsets[i] + lengths[i];
+ }
+ std::vector<uint8_t> bytes(offsets[length]);
+ std::vector<uint8_t*> payload_ptrs(length);
+ auto reset_payload_ptrs = [&] {
+ for (int32_t i = 0; i < length; ++i) {
+ payload_ptrs[i] = bytes.data() + offsets[i];
+ }
+ };
+
+ reset_payload_ptrs();
+ ARROW_RETURN_NOT_OK(encoder->Encode(value, length, payload_ptrs.data()));
+ reset_payload_ptrs();
+ return encoder->Decode(payload_ptrs.data(), length,
::arrow::default_memory_pool());
+}
+
+// Round-trip view keys as an array: inline, out-of-line, empty, and null
values.
+TEST(TestKeyEncoder, BinaryViewArray) {
+ for (const auto& ty : {utf8_view(), binary_view()}) {
+ SCOPED_TRACE("type " + ty->ToString());
+ auto array =
+ ArrayFromJSON(ty, R"(["short", null, "a long out-of-line value", "",
"x"])");
+
+ BinaryViewKeyEncoder key_encoder(ty);
+ ASSERT_OK_AND_ASSIGN(
+ auto decoded, RoundTripThroughKeyEncoder(&key_encoder,
ExecValue{*array->data()},
+
static_cast<int32_t>(array->length())));
+
+ ASSERT_OK(arrow::internal::ValidateArrayFull(*decoded));
+ ASSERT_EQ(decoded->type->id(), ty->id());
+ AssertArraysEqual(*array, *MakeArray(decoded), /*verbose=*/true);
+ }
+}
+
+TEST(TestKeyEncoder, BinaryViewScalar) {
+ constexpr int32_t kBatchLength = 8;
+ for (const auto& ty : {utf8_view(), binary_view()}) {
+ SCOPED_TRACE("type " + ty->ToString());
+ // Scalar input path: inline, out-of-line, and null.
+ for (const auto& scalar :
+ {ScalarFromJSON(ty, R"("short")"),
+ ScalarFromJSON(ty, R"("a long out-of-line value")"),
MakeNullScalar(ty)}) {
+ SCOPED_TRACE("scalar " + scalar->ToString());
+ BinaryViewKeyEncoder key_encoder(ty);
+ ASSERT_OK_AND_ASSIGN(
+ auto decoded, RoundTripThroughKeyEncoder(&key_encoder,
ExecValue{scalar.get()},
+ kBatchLength));
+
+ ASSERT_OK(arrow::internal::ValidateArrayFull(*decoded));
+ ASSERT_EQ(decoded->type->id(), ty->id());
+ ASSERT_OK_AND_ASSIGN(
+ auto expected,
+ MakeArrayFromScalar(*scalar, kBatchLength,
::arrow::default_memory_pool()));
+ AssertArraysEqual(*expected, *MakeArray(decoded), /*verbose=*/true);
+ }
+ }
+}
+
} // namespace arrow::compute::internal