| # 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. | |
| """Reader for a block.""" | |
| import struct | |
| from typing import Callable, Optional | |
| from pypaimon.execution.python.globalindex.btree.block_aligned_type import BlockAlignedType | |
| from pypaimon.execution.python.globalindex.btree.block_entry import BlockEntry | |
| from pypaimon.execution.python.globalindex.memory_slice_input import MemorySliceInput | |
| class BlockReader: | |
| """Reader for a block.""" | |
| def __init__( | |
| self, | |
| block: bytes, | |
| record_count: int, | |
| comparator: Callable[[bytes, bytes], int] | |
| ): | |
| """Initialize BlockReader. | |
| Args: | |
| block: The block data bytes | |
| record_count: Number of records in the block | |
| comparator: Optional comparator function for keys | |
| """ | |
| self.block = block | |
| self.record_count = record_count | |
| self.comparator = comparator | |
| def block_input(self) -> MemorySliceInput: | |
| """Create a MemorySliceInput for this block.""" | |
| return MemorySliceInput(self.block) | |
| def iterator(self) -> 'BlockIterator': | |
| """Create a BlockIterator for this reader.""" | |
| return BlockIterator(self) | |
| def seek_to(self, record_position: int) -> int: | |
| """Seek to slice position from record position. | |
| Args: | |
| record_position: The record position to seek to | |
| Returns: | |
| The slice position | |
| Raises: | |
| NotImplementedError: If not implemented in subclass | |
| """ | |
| raise NotImplementedError("seekTo must be implemented in subclass") | |
| @staticmethod | |
| def create( | |
| block: bytes, | |
| comparator: Optional[Callable[[bytes, bytes], int]] = None | |
| ) -> 'BlockReader': | |
| """Create a BlockReader from block data. | |
| Args: | |
| block: The block data bytes | |
| comparator: Optional comparator function for keys | |
| Returns: | |
| A BlockReader instance (AlignedBlockReader or UnalignedBlockReader) | |
| """ | |
| # Read block trailer: last byte is aligned type, previous 4 bytes is record size or index length | |
| aligned_type_byte = block[-1] | |
| aligned_type = BlockAlignedType.from_byte(aligned_type_byte) | |
| int_value = struct.unpack('<I', block[-5:-1])[0] | |
| if aligned_type == BlockAlignedType.ALIGNED: | |
| # Aligned block: records have fixed size | |
| data = block[:-5] | |
| return AlignedBlockReader(data, int_value, comparator) | |
| else: | |
| # Unaligned block: uses index | |
| index_length = int_value * 4 | |
| index_offset = len(block) - 5 - index_length | |
| data = block[:index_offset] | |
| index = block[index_offset:index_offset + index_length] | |
| return UnalignedBlockReader(data, index, comparator) | |
| class AlignedBlockReader(BlockReader): | |
| """Block reader for aligned blocks (fixed record size).""" | |
| def __init__( | |
| self, | |
| data: bytes, | |
| record_size: int, | |
| comparator: Optional[Callable[[bytes, bytes], int]] = None | |
| ): | |
| """Initialize AlignedBlockReader. | |
| Args: | |
| data: The block data bytes | |
| record_size: The fixed size of each record | |
| comparator: Optional comparator function for keys | |
| """ | |
| record_count = len(data) // record_size | |
| super().__init__(data, record_count, comparator) | |
| self.record_size = record_size | |
| def seek_to(self, record_position: int) -> int: | |
| """Seek to slice position from record position. | |
| Args: | |
| record_position: The record position to seek to | |
| Returns: | |
| The slice position | |
| """ | |
| return record_position * self.record_size | |
| class UnalignedBlockReader(BlockReader): | |
| """Block reader for unaligned blocks (uses index).""" | |
| def __init__( | |
| self, | |
| data: bytes, | |
| index: bytes, | |
| comparator: Optional[Callable[[bytes, bytes], int]] = None | |
| ): | |
| """Initialize UnalignedBlockReader. | |
| Args: | |
| data: The block data bytes | |
| index: The index bytes (4 bytes per record) | |
| comparator: Optional comparator function for keys | |
| """ | |
| record_count = len(index) // 4 | |
| super().__init__(data, record_count, comparator) | |
| self.index = index | |
| def seek_to(self, record_position: int) -> int: | |
| """Seek to slice position from record position. | |
| Args: | |
| record_position: The record position to seek to | |
| Returns: | |
| The slice position | |
| """ | |
| # Read 4-byte integer from index at record_position * 4 | |
| offset = record_position * 4 | |
| return struct.unpack('<I', self.index[offset:offset + 4])[0] | |
| class BlockIterator: | |
| """Iterator for block entries.""" | |
| def __init__(self, reader: BlockReader): | |
| """Initialize BlockIterator. | |
| Args: | |
| reader: The BlockReader to iterate over | |
| """ | |
| self.reader = reader | |
| self.input = reader.block_input() | |
| self.polled: Optional[BlockEntry] = None | |
| def __iter__(self): | |
| """Return self as iterator.""" | |
| return self | |
| def __next__(self) -> BlockEntry: | |
| """Get next entry. | |
| Returns: | |
| The next BlockEntry | |
| Raises: | |
| StopIteration: If no more entries | |
| """ | |
| if not self.has_next(): | |
| raise StopIteration | |
| if self.polled is not None: | |
| result = self.polled | |
| self.polled = None | |
| return result | |
| return self.read_entry() | |
| def has_next(self) -> bool: | |
| """Check if there are more entries.""" | |
| return self.polled is not None or self.input.is_readable() | |
| def seek_to(self, target_key: bytes) -> bool: | |
| """Seek to the first key >= target_key using binary search. | |
| Args: | |
| target_key: The target key to seek to | |
| Returns: | |
| True if exact match found, False otherwise | |
| """ | |
| left = 0 | |
| right = self.reader.record_count - 1 | |
| while left <= right: | |
| mid = left + (right - left) // 2 | |
| self.input.set_position(self.reader.seek_to(mid)) | |
| mid_entry = self.read_entry() | |
| compare = self.reader.comparator(mid_entry.key, target_key) | |
| if compare == 0: | |
| self.polled = mid_entry | |
| return True | |
| elif compare > 0: | |
| self.polled = mid_entry | |
| right = mid - 1 | |
| else: | |
| self.polled = None | |
| left = mid + 1 | |
| return False | |
| def read_entry(self) -> BlockEntry: | |
| """Read a key-value entry. | |
| Returns: | |
| A BlockEntry containing key and value | |
| """ | |
| # Read key | |
| key_length = self.input.read_var_len_int() | |
| key = self.input.read_slice(key_length) | |
| # Read value | |
| value_length = self.input.read_var_len_int() | |
| value = self.input.read_slice(value_length) | |
| return BlockEntry(key, value) |