blob: 4af31d5ef05a85445429f846943d7a05d6cd7bd6 [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.
"""Sequential processing of row-id ranges at file-group boundaries."""
from typing import Callable, Dict, List
from pypaimon.utils.range import Range
from pypaimon.utils.range_helper import RangeHelper
__all__ = ["process_row_id_ranges"]
def process_row_id_ranges(
target: str,
catalog_options: Dict[str, str],
*,
rows_per_commit: int,
processor: Callable[[List[Range]], None]
) -> None:
"""Process the target's row-id file groups in sequential batches.
The latest snapshot is planned once before processing starts. Files with
overlapping row-id ranges (for example, a base file and its data-evolution
files) form one indivisible file group. Adjacent groups are accumulated
until their row count reaches ``rows_per_commit``, then ``processor`` is
called synchronously with their inclusive :class:`Range` objects.
A batch may exceed ``rows_per_commit`` because file groups are never split.
The processor owns reading, distributed execution, committing, retries, and
cleanup. Its exception is propagated immediately and later batches are not
processed.
"""
_validate_arguments(rows_per_commit, processor)
from pypaimon.catalog.catalog_factory import CatalogFactory
table = CatalogFactory.create(catalog_options).get_table(target)
if not table.options.row_tracking_enabled():
raise ValueError(
"process_row_id_ranges requires 'row-tracking.enabled'='true' "
"on '{}'.".format(target)
)
pending_ranges = []
pending_rows = 0
for row_id_range in _file_group_ranges(table):
pending_ranges.append(row_id_range)
pending_rows += row_id_range.count()
if pending_rows >= rows_per_commit:
processor(pending_ranges)
pending_ranges = []
pending_rows = 0
if pending_ranges:
processor(pending_ranges)
def _validate_arguments(rows_per_commit, processor) -> None:
if (
isinstance(rows_per_commit, bool)
or not isinstance(rows_per_commit, int)
or rows_per_commit <= 0
):
raise ValueError("rows_per_commit must be a positive integer.")
if not callable(processor):
raise ValueError("processor must be callable.")
def _file_group_ranges(table) -> List[Range]:
plan = table.new_read_builder().new_scan().plan_for_write()
files = [data_file for split in plan.splits() for data_file in split.files]
file_groups = RangeHelper(
lambda data_file: data_file.non_null_row_id_range()
).merge_overlapping_ranges(files)
ranges = []
for file_group in file_groups:
group_ranges = [
data_file.non_null_row_id_range() for data_file in file_group
]
ranges.append(
Range(
min(row_range.from_ for row_range in group_ranges),
max(row_range.to for row_range in group_ranges),
)
)
return sorted(ranges, key=lambda row_range: row_range.from_)