| # 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 |
| ) |