blob: 2dffcf493b41cb9bc3edba94939e0ae57cb9d1eb [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 os
import shutil
import tempfile
import unittest
import uuid
import pyarrow as pa
from pypaimon import CatalogFactory, Schema
class DataEvolutionRowRollingTest(unittest.TestCase):
"""Row-count based data file rolling (target-file-row-num) for
data-evolution append tables."""
pa_schema = pa.schema([
('id', pa.int32()),
('name', pa.string()),
])
de_options = {
'row-tracking.enabled': 'true',
'data-evolution.enabled': 'true',
}
@classmethod
def setUpClass(cls):
cls.tempdir = tempfile.mkdtemp()
cls.catalog = CatalogFactory.create(
{'warehouse': os.path.join(cls.tempdir, 'warehouse')})
cls.catalog.create_database('default', True)
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.tempdir, ignore_errors=True)
def _create(self, options):
name = f'default.roll_{uuid.uuid4().hex[:8]}'
self.catalog.create_table(
name, Schema.from_pyarrow_schema(self.pa_schema, options=options),
False)
return self.catalog.get_table(name)
def _rows(self, n):
return pa.Table.from_pydict(
{'id': list(range(n)), 'name': [f'n{i}' for i in range(n)]},
schema=self.pa_schema)
def _write_files(self, table, data):
"""Write one Arrow table and return the committed DataFileMeta list."""
wb = table.new_batch_write_builder()
tw = wb.new_write()
tw.write_arrow(data)
msgs = tw.prepare_commit()
files = [f for m in msgs for f in m.new_files]
wb.new_commit().commit(msgs)
tw.close()
return files
def _read_ids(self, table):
rb = table.new_read_builder()
return sorted(
rb.new_read().to_arrow(rb.new_scan().plan().splits())
['id'].to_pylist())
def test_rolls_when_row_count_exceeds_limit(self):
table = self._create({**self.de_options, 'target-file-row-num': '3'})
files = self._write_files(table, self._rows(10))
# 10 rows, limit 3 -> 3 full files + a 1-row remainder.
self.assertEqual([1, 3, 3, 3], sorted(f.row_count for f in files))
self.assertEqual(list(range(10)), self._read_ids(table))
def test_exact_multiple_rolls_evenly(self):
table = self._create({**self.de_options, 'target-file-row-num': '3'})
files = self._write_files(table, self._rows(6))
self.assertEqual([3, 3], sorted(f.row_count for f in files))
self.assertEqual(list(range(6)), self._read_ids(table))
def test_below_limit_is_single_file(self):
table = self._create({**self.de_options, 'target-file-row-num': '100'})
files = self._write_files(table, self._rows(10))
self.assertEqual([10], [f.row_count for f in files])
def test_unset_option_does_not_roll_by_rows(self):
table = self._create(self.de_options)
files = self._write_files(table, self._rows(50))
# No row limit -> a small batch stays one file (size rolling only).
self.assertEqual([50], [f.row_count for f in files])
def test_oversized_row_rolls_by_itself(self):
# Each row exceeds target-file-size: the size trigger rolls every row by
# itself even though target-file-row-num is larger.
table = self._create({
**self.de_options,
'target-file-row-num': '3',
'target-file-size': '100 b',
})
big = 'x' * 500
data = pa.Table.from_pydict(
{'id': list(range(4)), 'name': [big] * 4}, schema=self.pa_schema)
files = self._write_files(table, data)
self.assertEqual([1, 1, 1, 1], [f.row_count for f in files])
self.assertEqual(list(range(4)), self._read_ids(table))
def test_non_de_table_still_fails_fast(self):
table = self._create({'target-file-row-num': '3'})
wb = table.new_batch_write_builder()
tw = wb.new_write()
with self.assertRaisesRegex(
NotImplementedError, 'row-count based file rolling'):
tw.write_arrow(self._rows(4))
if __name__ == '__main__':
unittest.main()