blob: 1504d555304d2b31b5f1a912fb7eda626e3fb1e6 [file]
# 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.
import unittest
from unittest.mock import patch
import pyarrow as pa
from pypaimon.read.reader.concat_batch_reader import (
DataEvolutionMergeReader,
MergeAllBatchReader,
)
from pypaimon.read.reader.iface.record_batch_reader import RecordBatchReader
class _BatchReader(RecordBatchReader):
def __init__(self, batches):
self._batches = iter(batches)
def read_arrow_batch(self):
return next(self._batches, None)
def close(self):
pass
class ConcatBatchReaderTest(unittest.TestCase):
def test_merge_all_coalesces_small_batches(self):
batches = [
pa.record_batch([pa.array(["a", "b"])], names=["value"]),
pa.record_batch([pa.array(["c", "d", "e"])], names=["value"]),
]
reader = MergeAllBatchReader(
[lambda batch=batch: _BatchReader([batch]) for batch in batches],
batch_size=2,
)
actual = []
while True:
batch = reader.read_arrow_batch()
if batch is None:
break
actual.append(batch.column(0).to_pylist())
self.assertEqual(actual, [["a", "b"], ["c", "d"], ["e"]])
def test_merge_all_flushes_before_arrow_offset_limit(self):
batches = [
pa.record_batch([pa.array(["aaaa"])], names=["value"]),
pa.record_batch([pa.array(["bbbb"])], names=["value"]),
]
reader = MergeAllBatchReader(
[lambda batch=batch: _BatchReader([batch]) for batch in batches],
batch_size=2,
)
with patch(
"pypaimon.read.reader.concat_batch_reader._MAX_ARROW_OFFSET",
4):
actual = []
while True:
batch = reader.read_arrow_batch()
if batch is None:
break
actual.append(batch.column(0).to_pylist())
self.assertEqual(actual, [["aaaa"], ["bbbb"]])
def test_merge_all_flushes_before_list_offset_limit(self):
child_count = 1100000000
def batch():
values = pa.nulls(child_count)
offsets = pa.array([0, child_count], type=pa.int32())
return pa.record_batch(
[pa.ListArray.from_arrays(offsets, values)], names=["value"])
batches = [batch(), batch()]
self.assertTrue(all(item.nbytes < child_count for item in batches))
reader = MergeAllBatchReader(
[lambda item=item: _BatchReader([item]) for item in batches],
batch_size=2,
)
actual = []
while True:
item = reader.read_arrow_batch()
if item is None:
break
actual.append(item.column(0).value_lengths()[0].as_py())
self.assertEqual(actual, [child_count, child_count])
def test_merge_all_checks_nested_and_map_offsets(self):
arrays = [
pa.array([["aaa"]], type=pa.list_(pa.string())),
pa.array(
[[("a", 1), ("b", 2)]],
type=pa.map_(pa.string(), pa.int32()),
),
]
for array in arrays:
with self.subTest(data_type=array.type):
batches = [
pa.record_batch([array], names=["value"]),
pa.record_batch([array], names=["value"]),
]
reader = MergeAllBatchReader(
[
lambda item=item: _BatchReader([item])
for item in batches
],
batch_size=2,
)
with patch(
"pypaimon.read.reader.concat_batch_reader."
"_MAX_ARROW_OFFSET",
3):
sizes = []
while True:
item = reader.read_arrow_batch()
if item is None:
break
sizes.append(item.num_rows)
self.assertEqual(sizes, [1, 1])
def test_misaligned_small_files_keep_bounded_batch_count(self):
row_count = 10000
left = MergeAllBatchReader([
lambda value=value: _BatchReader([
pa.record_batch([pa.array([value])], names=["left"])
])
for value in range(row_count)
])
right = _BatchReader([
pa.record_batch(
[pa.array(range(start, min(start + 1024, row_count)))],
names=["right"],
)
for start in range(0, row_count, 1024)
])
reader = DataEvolutionMergeReader(
row_offsets=[0, 1],
field_offsets=[0, 0],
readers=[left, right],
schema=pa.schema([("left", pa.int64()), ("right", pa.int64())]),
)
batch_sizes = []
values = []
while True:
batch = reader.read_arrow_batch()
if batch is None:
break
batch_sizes.append(batch.num_rows)
values.extend(batch.column(0).to_pylist())
self.assertEqual(batch_sizes, [1024] * 9 + [784])
self.assertEqual(values, list(range(row_count)))
def test_merge_reader_does_not_join_buffered_remainders(self):
left = _BatchReader([
pa.record_batch([pa.array([0])], names=["left"]),
pa.record_batch([pa.array([1, 2])], names=["left"]),
])
right = _BatchReader([
pa.record_batch([pa.array([10, 11])], names=["right"]),
pa.record_batch([pa.array([12])], names=["right"]),
])
reader = DataEvolutionMergeReader(
row_offsets=[0, 1],
field_offsets=[0, 0],
readers=[left, right],
schema=pa.schema([("left", pa.int64()), ("right", pa.int64())]),
)
with patch.object(
pa, "concat_arrays",
side_effect=AssertionError("must preserve Arrow chunks")):
actual = []
while True:
batch = reader.read_arrow_batch()
if batch is None:
break
actual.extend(
{"left": left, "right": right}
for left, right in zip(
batch.column(0).to_pylist(),
batch.column(1).to_pylist(),
)
)
self.assertEqual(actual, [
{"left": 0, "right": 10},
{"left": 1, "right": 11},
{"left": 2, "right": 12},
])
if __name__ == "__main__":
unittest.main()