@@ -535,6 +535,26 @@ std::shared_ptr<arrow::RecordBatch> MakeSliceBatch(const std::shared_ptr<arrow::
535535 {array->Slice (offset, length)});
536536}
537537
538+ // / Checks that normalization keeps the same rows with every offset zeroed. `array` is used as a
539+ // / single column batch, so its own offset is whatever the caller built it with.
540+ void CheckNormalizedArray (const std::shared_ptr<arrow::Array>& array) {
541+ SCOPED_TRACE (" type=" + array->type ()->ToString ());
542+ std::shared_ptr<arrow::RecordBatch> batch = arrow::RecordBatch::Make (
543+ arrow::schema ({arrow::field (" f" , array->type ())}), array->length (), {array});
544+
545+ ASSERT_OK_AND_ASSIGN (
546+ std::shared_ptr<arrow::RecordBatch> normalized,
547+ ArrowUtils::NormalizeRecordBatchOffsets (batch, arrow::default_memory_pool ()));
548+ // A batch that needs normalization must not be returned unchanged.
549+ ASSERT_NE (normalized.get (), batch.get ());
550+
551+ arrow::Status validated = normalized->ValidateFull ();
552+ ASSERT_TRUE (validated.ok ()) << validated.ToString ();
553+ ASSERT_TRUE (normalized->Equals (*batch))
554+ << " expected " << batch->ToString () << " but got " << normalized->ToString ();
555+ ExpectAllOffsetsZero (*normalized->column_data (0 ), " f" );
556+ }
557+
538558// / Slices `array` and checks that normalization keeps the same rows with every offset zeroed.
539559void CheckNormalizedSlice (const std::shared_ptr<arrow::Array>& array, int64_t offset,
540560 int64_t length) {
@@ -599,6 +619,68 @@ TEST(ArrowUtilsTest, TestNormalizeRecordBatchOffsetsSharesValueBuffers) {
599619 }
600620}
601621
622+ // Check the situation that child offsets are non-zero while parent offset is zero.
623+ TEST (ArrowUtilsTest, TestNormalizeRecordBatchOffsetsRebasesNestedOffsetsUnderZeroParent) {
624+ std::shared_ptr<arrow::Array> ints =
625+ arrow::ipc::internal::json::ArrayFromJSON (arrow::int32 (), " [0, 1, 2, 3, 4, 5, 6, 7]" )
626+ .ValueOrDie ();
627+ std::shared_ptr<arrow::Array> texts =
628+ arrow::ipc::internal::json::ArrayFromJSON (
629+ arrow::utf8 (), R"( ["a", "bb", null, "dddd", "e", "ff", "ggg", "h"])" )
630+ .ValueOrDie ();
631+
632+ {
633+ // struct whose children are sliced: parent offset 0, both children offset 2
634+ std::shared_ptr<arrow::Array> array =
635+ arrow::StructArray::Make ({ints->Slice (2 , 4 ), texts->Slice (2 , 4 )},
636+ std::vector<std::string>{" a" , " b" })
637+ .ValueOrDie ();
638+ ASSERT_EQ (array->offset (), 0 );
639+ ASSERT_EQ (array->data ()->child_data [0 ]->offset , 2 );
640+ ASSERT_EQ (array->data ()->child_data [1 ]->offset , 2 );
641+ CheckNormalizedArray (array);
642+ }
643+ {
644+ // only the innermost array is sliced, so detection has to walk two levels down
645+ std::shared_ptr<arrow::Array> inner =
646+ arrow::StructArray::Make ({ints->Slice (3 , 4 )}, std::vector<std::string>{" a" })
647+ .ValueOrDie ();
648+ std::shared_ptr<arrow::Array> array =
649+ arrow::StructArray::Make ({inner}, std::vector<std::string>{" inner" }).ValueOrDie ();
650+ ASSERT_EQ (array->offset (), 0 );
651+ ASSERT_EQ (array->data ()->child_data [0 ]->offset , 0 );
652+ ASSERT_EQ (array->data ()->child_data [0 ]->child_data [0 ]->offset , 3 );
653+ CheckNormalizedArray (array);
654+ }
655+ {
656+ // list built over sliced values: parent offset 0, values offset 2, and the list offsets
657+ // address the values relative to that slice
658+ std::shared_ptr<arrow::Array> offsets =
659+ arrow::ipc::internal::json::ArrayFromJSON (arrow::int32 (), " [0, 1, 1, 3, 4]" )
660+ .ValueOrDie ();
661+ std::shared_ptr<arrow::Array> array =
662+ arrow::ListArray::FromArrays (*offsets, *ints->Slice (2 , 4 )).ValueOrDie ();
663+ ASSERT_EQ (array->offset (), 0 );
664+ ASSERT_EQ (array->data ()->child_data [0 ]->offset , 2 );
665+ CheckNormalizedArray (array);
666+ }
667+ {
668+ // map built over sliced keys and items, which land under the entries struct. Map keys
669+ // cannot be null, so this slice avoids the null in `texts`.
670+ std::shared_ptr<arrow::Array> offsets =
671+ arrow::ipc::internal::json::ArrayFromJSON (arrow::int32 (), " [0, 2, 2, 4]" ).ValueOrDie ();
672+ std::shared_ptr<arrow::Array> array =
673+ arrow::MapArray::FromArrays (offsets, texts->Slice (3 , 4 ), ints->Slice (4 , 4 ))
674+ .ValueOrDie ();
675+ ASSERT_EQ (array->offset (), 0 );
676+ const arrow::ArrayData& entries = *array->data ()->child_data [0 ];
677+ ASSERT_EQ (entries.offset , 0 );
678+ ASSERT_EQ (entries.child_data [0 ]->offset , 3 );
679+ ASSERT_EQ (entries.child_data [1 ]->offset , 4 );
680+ CheckNormalizedArray (array);
681+ }
682+ }
683+
602684TEST (ArrowUtilsTest, TestNormalizeRecordBatchOffsetsFallsBackForDictionary) {
603685 // A dictionary is not part of child_data, so this layout takes the copying fallback.
604686 std::shared_ptr<arrow::Array> indices =
0 commit comments