blob: 1702696943a8e7607e856695e4cc7acbc8f8970a [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.
"""Reader for a block."""
import struct
from typing import Callable, Optional
from pypaimon.globalindex.btree.block_aligned_type import BlockAlignedType
from pypaimon.globalindex.btree.block_entry import BlockEntry
from pypaimon.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)