| # 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 one bitmap global index file.""" |
| |
| from dataclasses import dataclass |
| import struct |
| import threading |
| from typing import Callable, Dict, List, Optional |
| |
| from pypaimon.common.file_io import FileIO, pread, supports_pread |
| from pypaimon.globalindex.block_compression import ( |
| BLOCK_TRAILER_LENGTH, |
| decompress_block_with_trailer, |
| ) |
| from pypaimon.globalindex.global_index_meta import GlobalIndexIOMeta |
| from pypaimon.globalindex.global_index_result import GlobalIndexResult |
| from pypaimon.globalindex.key_serializer import KeySerializer |
| from pypaimon.globalindex.memory_slice_input import MemorySliceInput |
| from pypaimon.utils.roaring_bitmap import RoaringBitmap64 |
| |
| |
| @dataclass(frozen=True) |
| class _BlockInfo: |
| offset: int |
| length: int |
| |
| |
| @dataclass(frozen=True) |
| class _DictionaryBlockMeta(_BlockInfo): |
| first_key: bytes |
| |
| |
| @dataclass(frozen=True) |
| class _DictionaryEntry: |
| key: bytes |
| bitmap_block: _BlockInfo |
| |
| |
| @dataclass(frozen=True) |
| class _Footer: |
| null_rows_block: _BlockInfo |
| non_null_rows_block: _BlockInfo |
| index_block: _BlockInfo |
| |
| |
| class BitmapIndexReader: |
| """Synchronous reader for one bitmap global index file.""" |
| |
| _FOOTER_LENGTH = 48 |
| _MAGIC = 0x42474958 |
| _VERSION = 1 |
| |
| def __init__( |
| self, |
| key_serializer: KeySerializer, |
| file_io: FileIO, |
| index_path: str, |
| io_meta: GlobalIndexIOMeta, |
| ): |
| self._key_serializer = key_serializer |
| self._comparator = key_serializer.create_comparator() |
| self._io_meta = io_meta |
| file_path = (io_meta.external_path |
| if io_meta.external_path |
| else index_path + "/" + io_meta.file_name) |
| self._input_stream = file_io.new_input_stream(file_path) |
| self._supports_pread = supports_pread(self._input_stream) |
| self._io_lock = threading.Lock() |
| |
| self._footer = self._read_footer() |
| self._null_rows: Optional[RoaringBitmap64] = None |
| self._non_null_rows: Optional[RoaringBitmap64] = None |
| self._dictionary_blocks: Optional[List[_DictionaryBlockMeta]] = None |
| self._dictionary_block_cache: Dict[_DictionaryBlockMeta, List[_DictionaryEntry]] = {} |
| self._lazy_lock = threading.Lock() |
| |
| def _read_from(self, offset: int, length: int) -> bytes: |
| if self._supports_pread: |
| return pread(self._input_stream, length, offset) |
| with self._io_lock: |
| self._input_stream.seek(offset) |
| return self._input_stream.read(length) |
| |
| def _read_footer(self) -> _Footer: |
| if self._io_meta.file_size < self._FOOTER_LENGTH: |
| raise ValueError("Invalid bitmap global index file size.") |
| footer_data = self._read_from( |
| self._io_meta.file_size - self._FOOTER_LENGTH, |
| self._FOOTER_LENGTH) |
| (null_offset, null_length, |
| non_null_offset, non_null_length, |
| index_offset, index_length, |
| value_count, version, magic) = struct.unpack('>q i q i q i i i i', footer_data) |
| if magic != self._MAGIC: |
| raise ValueError("File is not a bitmap global index file.") |
| if version != self._VERSION: |
| raise ValueError(f"Unsupported bitmap global index file version: {version}") |
| if value_count < 0: |
| raise ValueError("Invalid bitmap value count.") |
| return _Footer( |
| _BlockInfo(null_offset, null_length), |
| _BlockInfo(non_null_offset, non_null_length), |
| _BlockInfo(index_offset, index_length)) |
| |
| def _read_bitmap(self, block: _BlockInfo) -> RoaringBitmap64: |
| return RoaringBitmap64.deserialize(self._read_from(block.offset, block.length)) |
| |
| def _read_compressible_block(self, block: _BlockInfo) -> bytes: |
| block_and_trailer = self._read_from( |
| block.offset, block.length + BLOCK_TRAILER_LENGTH) |
| return decompress_block_with_trailer(block_and_trailer, block.length) |
| |
| def _read_index_block(self) -> List[_DictionaryBlockMeta]: |
| data_input = MemorySliceInput( |
| self._read_compressible_block(self._footer.index_block)) |
| block_count = data_input.read_var_len_int() |
| if block_count < 0: |
| raise ValueError("Invalid bitmap dictionary block count.") |
| |
| blocks = [] |
| for _ in range(block_count): |
| key_length = data_input.read_var_len_int() |
| if key_length < 0: |
| raise ValueError("Invalid bitmap key length.") |
| first_key = data_input.read_slice(key_length) |
| offset = data_input.read_var_len_long() |
| length = data_input.read_var_len_int() |
| blocks.append(_DictionaryBlockMeta(offset, length, first_key)) |
| return sorted(blocks, key=lambda block: block.first_key) |
| |
| def _read_dictionary_block(self, block: _DictionaryBlockMeta) -> List[_DictionaryEntry]: |
| data_input = MemorySliceInput(self._read_compressible_block(block)) |
| entry_count = data_input.read_var_len_int() |
| if entry_count < 0: |
| raise ValueError("Invalid bitmap dictionary entry count.") |
| |
| entries = [] |
| for _ in range(entry_count): |
| key_length = data_input.read_var_len_int() |
| if key_length < 0: |
| raise ValueError("Invalid bitmap key length.") |
| key = data_input.read_slice(key_length) |
| bitmap_offset = data_input.read_var_len_long() |
| bitmap_length = data_input.read_var_len_int() |
| entries.append(_DictionaryEntry(key, _BlockInfo(bitmap_offset, bitmap_length))) |
| return entries |
| |
| def _get_null_rows(self) -> RoaringBitmap64: |
| if self._null_rows is None: |
| with self._lazy_lock: |
| if self._null_rows is None: |
| self._null_rows = self._read_bitmap(self._footer.null_rows_block) |
| return self._copy(self._null_rows) |
| |
| def _get_non_null_rows(self) -> RoaringBitmap64: |
| if self._non_null_rows is None: |
| with self._lazy_lock: |
| if self._non_null_rows is None: |
| self._non_null_rows = self._read_bitmap(self._footer.non_null_rows_block) |
| return self._copy(self._non_null_rows) |
| |
| def _get_dictionary_blocks(self) -> List[_DictionaryBlockMeta]: |
| if self._dictionary_blocks is None: |
| with self._lazy_lock: |
| if self._dictionary_blocks is None: |
| self._dictionary_blocks = self._read_index_block() |
| return self._dictionary_blocks |
| |
| def _get_dictionary_block(self, meta: _DictionaryBlockMeta) -> List[_DictionaryEntry]: |
| block = self._dictionary_block_cache.get(meta) |
| if block is None: |
| with self._lazy_lock: |
| block = self._dictionary_block_cache.get(meta) |
| if block is None: |
| block = self._read_dictionary_block(meta) |
| self._dictionary_block_cache[meta] = block |
| return block |
| |
| def _find_dictionary_block_index(self, key: bytes) -> int: |
| blocks = self._get_dictionary_blocks() |
| low = 0 |
| high = len(blocks) - 1 |
| while low <= high: |
| mid = (low + high) >> 1 |
| if blocks[mid].first_key <= key: |
| low = mid + 1 |
| else: |
| high = mid - 1 |
| return high |
| |
| def _find_bitmap_block(self, key: bytes) -> Optional[_BlockInfo]: |
| blocks = self._get_dictionary_blocks() |
| if not blocks: |
| return None |
| |
| index = self._find_dictionary_block_index(key) |
| if index < 0: |
| return None |
| |
| for entry in self._get_dictionary_block(blocks[index]): |
| if entry.key == key: |
| return entry.bitmap_block |
| if entry.key > key: |
| return None |
| return None |
| |
| def _scan_dictionary(self, predicate: Callable[[object], bool]) -> RoaringBitmap64: |
| result = RoaringBitmap64() |
| for block_meta in self._get_dictionary_blocks(): |
| for entry in self._get_dictionary_block(block_meta): |
| key = self._key_serializer.deserialize(entry.key) |
| if predicate(key): |
| result = RoaringBitmap64.or_(result, self._read_bitmap(entry.bitmap_block)) |
| return result |
| |
| def _scan_serialized_dictionary(self, predicate: Callable[[bytes], bool]) -> RoaringBitmap64: |
| result = RoaringBitmap64() |
| for block_meta in self._get_dictionary_blocks(): |
| for entry in self._get_dictionary_block(block_meta): |
| if predicate(entry.key): |
| result = RoaringBitmap64.or_(result, self._read_bitmap(entry.bitmap_block)) |
| return result |
| |
| def visit_is_not_null(self) -> Optional[GlobalIndexResult]: |
| return GlobalIndexResult.create(self._get_non_null_rows()) |
| |
| def visit_is_null(self) -> Optional[GlobalIndexResult]: |
| return GlobalIndexResult.create(self._get_null_rows()) |
| |
| def visit_starts_with(self, literal: object) -> Optional[GlobalIndexResult]: |
| prefix = self._key_serializer.serialize(literal) |
| if len(prefix) == 0: |
| return GlobalIndexResult.create(self._get_non_null_rows()) |
| return GlobalIndexResult.create(self._scan_serialized_dictionary( |
| lambda key: key.startswith(prefix))) |
| |
| def visit_ends_with(self, literal: object) -> Optional[GlobalIndexResult]: |
| suffix = self._key_serializer.serialize(literal) |
| if len(suffix) == 0: |
| return GlobalIndexResult.create(self._get_non_null_rows()) |
| return GlobalIndexResult.create(self._scan_serialized_dictionary( |
| lambda key: key.endswith(suffix))) |
| |
| def visit_contains(self, literal: object) -> Optional[GlobalIndexResult]: |
| infix = self._key_serializer.serialize(literal) |
| if len(infix) == 0: |
| return GlobalIndexResult.create(self._get_non_null_rows()) |
| return GlobalIndexResult.create(self._scan_serialized_dictionary( |
| lambda key: infix in key)) |
| |
| def visit_like(self, literal: object) -> Optional[GlobalIndexResult]: |
| from pypaimon.common.predicate import Like |
| return GlobalIndexResult.create( |
| self.like(lambda key: Like().test_by_value(key, [literal]))) |
| |
| def visit_less_than(self, literal: object) -> Optional[GlobalIndexResult]: |
| if literal is None: |
| return GlobalIndexResult.create_empty() |
| return GlobalIndexResult.create( |
| self._scan_dictionary(lambda key: self._comparator(key, literal) < 0)) |
| |
| def visit_greater_or_equal(self, literal: object) -> Optional[GlobalIndexResult]: |
| if literal is None: |
| return GlobalIndexResult.create_empty() |
| return GlobalIndexResult.create( |
| self._scan_dictionary(lambda key: self._comparator(key, literal) >= 0)) |
| |
| def visit_not_equal(self, literal: object) -> Optional[GlobalIndexResult]: |
| if literal is None: |
| return GlobalIndexResult.create_empty() |
| return GlobalIndexResult.create( |
| RoaringBitmap64.remove_all(self._get_non_null_rows(), self._equal(literal))) |
| |
| def visit_less_or_equal(self, literal: object) -> Optional[GlobalIndexResult]: |
| if literal is None: |
| return GlobalIndexResult.create_empty() |
| return GlobalIndexResult.create( |
| self._scan_dictionary(lambda key: self._comparator(key, literal) <= 0)) |
| |
| def visit_equal(self, literal: object) -> Optional[GlobalIndexResult]: |
| return GlobalIndexResult.create(self._equal(literal)) |
| |
| def visit_greater_than(self, literal: object) -> Optional[GlobalIndexResult]: |
| if literal is None: |
| return GlobalIndexResult.create_empty() |
| return GlobalIndexResult.create( |
| self._scan_dictionary(lambda key: self._comparator(key, literal) > 0)) |
| |
| def visit_in(self, literals: List[object]) -> Optional[GlobalIndexResult]: |
| result = RoaringBitmap64() |
| seen = set() |
| for literal in literals: |
| if literal is None: |
| continue |
| key = self._key_serializer.serialize(literal) |
| if key in seen: |
| continue |
| seen.add(key) |
| bitmap_block = self._find_bitmap_block(key) |
| if bitmap_block is not None: |
| result = RoaringBitmap64.or_(result, self._read_bitmap(bitmap_block)) |
| return GlobalIndexResult.create(result) |
| |
| def visit_not_in(self, literals: List[object]) -> Optional[GlobalIndexResult]: |
| if any(literal is None for literal in literals): |
| return GlobalIndexResult.create_empty() |
| result = self._get_non_null_rows() |
| in_result = self.visit_in(literals).results() |
| return GlobalIndexResult.create(RoaringBitmap64.remove_all(result, in_result)) |
| |
| def visit_between(self, from_v: object, to_v: object) -> Optional[GlobalIndexResult]: |
| if from_v is None or to_v is None or self._comparator(from_v, to_v) > 0: |
| return GlobalIndexResult.create_empty() |
| return GlobalIndexResult.create( |
| self._scan_dictionary( |
| lambda key: self._comparator(key, from_v) >= 0 |
| and self._comparator(key, to_v) <= 0)) |
| |
| def like(self, key_predicate: Callable[[object], bool]) -> RoaringBitmap64: |
| return self._scan_dictionary(key_predicate) |
| |
| def less_than(self, literal: object) -> RoaringBitmap64: |
| if literal is None: |
| return RoaringBitmap64() |
| return self._scan_dictionary(lambda key: self._comparator(key, literal) < 0) |
| |
| def greater_than(self, literal: object) -> RoaringBitmap64: |
| if literal is None: |
| return RoaringBitmap64() |
| return self._scan_dictionary(lambda key: self._comparator(key, literal) > 0) |
| |
| def _equal(self, literal: object) -> RoaringBitmap64: |
| if literal is None: |
| return RoaringBitmap64() |
| bitmap_block = self._find_bitmap_block(self._key_serializer.serialize(literal)) |
| if bitmap_block is None: |
| return RoaringBitmap64() |
| return self._read_bitmap(bitmap_block) |
| |
| @staticmethod |
| def _copy(bitmap: RoaringBitmap64) -> RoaringBitmap64: |
| return RoaringBitmap64.or_(RoaringBitmap64(), bitmap) |
| |
| def close(self) -> None: |
| if self._input_stream is not None: |
| self._input_stream.close() |