blob: 5474bf855abbb1474c5c8bdb6439a5044dd44b45 [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.
"""
FieldBunch classes for organizing files by field in data evolution.
These classes help organize DataFileMeta objects into groups based on their field content,
supporting both regular data files and blob files.
"""
from abc import ABC
from typing import List
from pypaimon.manifest.schema.data_file_meta import DataFileMeta
from pypaimon.utils.range import Range
class FieldBunch(ABC):
"""Interface for files organized by field."""
def row_count(self) -> int:
"""Return the total row count for this bunch."""
...
def files(self) -> List[DataFileMeta]:
"""Return the list of files in this bunch."""
...
class DataBunch(FieldBunch):
"""Files for a single data file."""
def __init__(self, data_file: DataFileMeta):
self.data_file = data_file
def row_count(self) -> int:
return self.data_file.row_count
def files(self) -> List[DataFileMeta]:
return [self.data_file]
class _SpecialFieldBunch(FieldBunch):
"""Base class for partial field files (blob/vector)."""
def __init__(self, expected_row_count: int, row_id_push_down: bool = False):
self._files: List[DataFileMeta] = []
self.expected_row_count = expected_row_count
self.row_id_push_down = row_id_push_down
self.latest_first_row_id = -1
self.expected_next_first_row_id = -1
self.latest_max_sequence_number = -1
self._row_count = 0
def _is_special_file(self, file_name: str) -> bool:
raise NotImplementedError
def _file_type_label(self) -> str:
raise NotImplementedError
def add(self, file: DataFileMeta) -> None:
if not self._is_special_file(file.file_name):
raise ValueError(
f"Only {self._file_type_label()} file can be added to "
f"a {self._file_type_label()} bunch.")
if self._files and file.write_cols != self._files[0].write_cols:
raise ValueError(
f"All files in a {self._file_type_label()} bunch should "
f"have the same write columns."
)
if file.first_row_id == self.latest_first_row_id:
if file.max_sequence_number >= self.latest_max_sequence_number:
raise ValueError(
f"{self._file_type_label().capitalize()} file with same first row id "
f"should have decreasing sequence number."
)
return
if self._files:
first_row_id = file.first_row_id
if self.row_id_push_down:
if first_row_id < self.expected_next_first_row_id:
if file.max_sequence_number > self.latest_max_sequence_number:
last_file = self._files.pop()
self._row_count -= last_file.row_count
else:
return
else:
if first_row_id < self.expected_next_first_row_id:
if file.max_sequence_number >= self.latest_max_sequence_number:
raise ValueError(
f"{self._file_type_label().capitalize()} file with overlapping "
f"row id should have decreasing sequence number."
)
return
elif first_row_id > self.expected_next_first_row_id:
raise ValueError(
f"{self._file_type_label().capitalize()} file first row id should "
f"be continuous, expect {self.expected_next_first_row_id} "
f"but got {first_row_id}"
)
if self._files:
if file.write_cols != self._files[0].write_cols:
raise ValueError(
f"All files in a {self._file_type_label()} bunch should "
f"have the same write columns."
)
self._files.append(file)
self._row_count += file.row_count
if self._row_count > self.expected_row_count:
raise ValueError(
f"{self._file_type_label().capitalize()} files row count exceed "
f"the expect {self.expected_row_count}"
)
self.latest_max_sequence_number = file.max_sequence_number
self.latest_first_row_id = file.first_row_id
self.expected_next_first_row_id = self.latest_first_row_id + file.row_count
def row_count(self) -> int:
return self._row_count
def files(self) -> List[DataFileMeta]:
return self._files
class BlobBunch(_SpecialFieldBunch):
"""Files for partial field (blob files)."""
def __init__(self, expected_row_count: int, row_id_push_down: bool = False):
super().__init__(expected_row_count, row_id_push_down)
self._finished = False
def add(self, file: DataFileMeta) -> None:
if self._finished:
raise ValueError("Cannot add a blob file to a finished blob bunch.")
if not self._is_special_file(file.file_name):
raise ValueError("Only blob file can be added to a blob bunch.")
if self._files and file.write_cols != self._files[0].write_cols:
raise ValueError("All files in a blob bunch should have the same write columns.")
self._files.append(file)
def finish(self) -> None:
if self._finished:
return
merged = Range.sort_and_merge_overlap(
[blob_file.row_id_range() for blob_file in self._files],
True,
True,
)
self._row_count = sum(row_range.count() for row_range in merged)
if self.expected_row_count >= 0 and self._row_count > self.expected_row_count:
raise ValueError(
f"Blob files row count exceed the expect {self.expected_row_count}"
)
if not self.row_id_push_down:
if len(merged) != 1:
raise ValueError("Blob file bunch should always contain a contiguous row range.")
if self.expected_row_count >= 0 and self._row_count != self.expected_row_count:
raise ValueError(
"The merged row count of blob file bunch should be aligned "
f"with normal files, expect {self.expected_row_count}, got {self._row_count}."
)
self._finished = True
def row_count(self) -> int:
self.finish()
return self._row_count
def sequential_read_optimize(self) -> bool:
if not self._files:
raise ValueError("Blob bunch should not be empty.")
max_sequence_number = self._files[0].max_sequence_number
return all(
file.max_sequence_number == max_sequence_number
for file in self._files
)
def _is_special_file(self, file_name: str) -> bool:
return DataFileMeta.is_blob_file(file_name)
def _file_type_label(self) -> str:
return "blob"
class VectorBunch(_SpecialFieldBunch):
"""Files for partial field (vector files)."""
def _is_special_file(self, file_name: str) -> bool:
return DataFileMeta.is_vector_file(file_name)
def _file_type_label(self) -> str:
return "vector"