blob: 8eb81b32592edac5a69705a4f5dec36105a34230 [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.
"""Snapshot file index used to route row-id updates."""
from dataclasses import dataclass, field
from typing import Dict, List, Tuple
from pypaimon.manifest.schema.data_file_meta import DataFileMeta
from pypaimon.manifest.schema.manifest_entry import ManifestEntry
from pypaimon.execution.python.read.scanner.data_evolution_split_generator import (DataEvolutionSplitGenerator)
from pypaimon.read.split import DataSplit
from pypaimon.utils.range import Range
@dataclass(frozen=True)
class RowIdFileIndex:
"""Snapshot files keyed by the first row ID of each data file group."""
snapshot_id: int
first_row_ids: List[int]
first_row_id_index: Dict[int, Tuple[DataSplit, List[DataFileMeta]]] = (
field(default_factory=dict)
)
valid_row_id_ranges: List[Range] = field(default_factory=list)
@classmethod
def from_table(cls, table) -> "RowIdFileIndex":
"""Plan the table's write snapshot and index its splits."""
scan = table.new_read_builder().new_scan()
plan = scan.plan_all()
snapshot_id = plan.snapshot_id if plan.snapshot_id is not None else -1
return cls.from_splits(snapshot_id, plan.splits())
@classmethod
def from_entries(
cls, table, snapshot_id: int, entries: List[ManifestEntry]
) -> "RowIdFileIndex":
"""Index an already resolved snapshot entry set."""
splits = DataEvolutionSplitGenerator(
table,
table.options.source_split_target_size(),
table.options.source_split_open_file_cost(),
).create_splits(entries)
return cls.from_splits(snapshot_id, splits)
@classmethod
def from_splits(
cls, snapshot_id: int, splits: List[DataSplit]
) -> "RowIdFileIndex":
index: Dict[int, Tuple[DataSplit, List[DataFileMeta]]] = {}
row_id_ranges: List[Range] = []
for split in splits:
files_with_row_id = [
file for file in split.files if file.first_row_id is not None
]
data_files = [
file for file in files_with_row_id
if not DataFileMeta.is_blob_file(file.file_name)
]
for file in split.files:
if (
file.first_row_id is None
or DataFileMeta.is_blob_file(file.file_name)
):
continue
row_id_ranges.append(file.row_id_range())
for file in data_files:
target_files = [
target_file
for target_file in files_with_row_id
if cls._overlaps(
file.row_id_range(), target_file.row_id_range()
)
]
entry = index.get(file.first_row_id)
if entry is None:
index[file.first_row_id] = (split, target_files)
else:
existing_files = entry[1]
existing_names = {
existing.file_name for existing in existing_files
}
existing_files.extend(
target_file
for target_file in target_files
if target_file.file_name not in existing_names
)
if row_id_ranges:
merged = Range.sort_and_merge_overlap(row_id_ranges, True, True)
else:
merged = []
return cls(
snapshot_id=snapshot_id,
first_row_ids=sorted(index.keys()),
first_row_id_index=index,
valid_row_id_ranges=merged,
)
@staticmethod
def _overlaps(left: Range, right: Range) -> bool:
return left.from_ <= right.to and right.from_ <= left.to