| # 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 | |
| from typing import List, Set, Tuple | |
| from pypaimon.manifest.manifest_file_manager import ManifestFileManager | |
| from pypaimon.manifest.manifest_list_manager import ManifestListManager | |
| from pypaimon.manifest.schema.manifest_entry import ManifestEntry | |
| from pypaimon.read.plan import Plan | |
| from pypaimon.read.scanner.append_table_split_generator import \ | |
| AppendTableSplitGenerator | |
| from pypaimon.snapshot.snapshot import Snapshot | |
| class IncrementalDiffScanner: | |
| """ | |
| Scan files added between two snapshots via set-diff. | |
| More efficient than reading N delta_manifest_lists when many intermediate | |
| snapshots are compaction-only. | |
| """ | |
| def __init__(self, table): | |
| from pypaimon.table.file_store_table import FileStoreTable | |
| self.table: FileStoreTable = table | |
| self.manifest_list_manager = ManifestListManager(table) | |
| self.manifest_file_manager = ManifestFileManager(table) | |
| options = self.table.options | |
| self.target_split_size = options.source_split_target_size() | |
| self.open_file_cost = options.source_split_open_file_cost() | |
| def scan(self, start_snapshot: Snapshot, end_snapshot: Snapshot) -> Plan: | |
| """Scan files added between start (exclusive) and end (inclusive) snapshots.""" | |
| added_entries = self.compute_diff(start_snapshot, end_snapshot) | |
| if not added_entries: | |
| return Plan([]) | |
| split_generator = AppendTableSplitGenerator( | |
| self.table, | |
| self.target_split_size, | |
| self.open_file_cost, | |
| {} # No deletion files for incremental diff | |
| ) | |
| splits = split_generator.create_splits(added_entries) | |
| return Plan(splits) | |
| def compute_diff( | |
| self, | |
| start_snapshot: Snapshot, | |
| end_snapshot: Snapshot | |
| ) -> List[ManifestEntry]: | |
| """Return files present in end_snapshot but absent from start_snapshot.""" | |
| start_manifest_files = self.manifest_list_manager.read_all(start_snapshot) | |
| end_manifest_files = self.manifest_list_manager.read_all(end_snapshot) | |
| max_workers = self.table.options.scan_manifest_parallelism(os.cpu_count() or 8) | |
| start_entries = self.manifest_file_manager.read_entries_parallel( | |
| start_manifest_files, | |
| max_workers=max_workers | |
| ) | |
| end_entries = self.manifest_file_manager.read_entries_parallel( | |
| end_manifest_files, | |
| max_workers=max_workers | |
| ) | |
| start_keys: Set[Tuple] = {self._entry_key(e) for e in start_entries} | |
| added_entries = [ | |
| entry for entry in end_entries | |
| if self._entry_key(entry) not in start_keys | |
| ] | |
| return added_entries | |
| def _entry_key(self, entry: ManifestEntry) -> Tuple: | |
| return ( | |
| tuple(entry.partition.values), | |
| entry.bucket, | |
| entry.file.file_name | |
| ) |