This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 519d1c5b feat(file-index): read range bitmap indexes (#849)
519d1c5b is described below
commit 519d1c5bc31fca0b2d5db8264d8af53416e3f777
Author: jianguotian <[email protected]>
AuthorDate: Thu Sep 17 08:08:05 2026 +0800
feat(file-index): read range bitmap indexes (#849)
---
.../paimon/src/file_index/file_indexer_factory.rs | 111 +-
crates/paimon/src/file_index/mod.rs | 2 +
crates/paimon/src/file_index/range_bitmap.rs | 1548 ++++++++++++++++++++
3 files changed, 1659 insertions(+), 2 deletions(-)
diff --git a/crates/paimon/src/file_index/file_indexer_factory.rs
b/crates/paimon/src/file_index/file_indexer_factory.rs
index 7750e2ae..55b63f36 100644
--- a/crates/paimon/src/file_index/file_indexer_factory.rs
+++ b/crates/paimon/src/file_index/file_indexer_factory.rs
@@ -23,16 +23,23 @@ use crate::file_index::bitmap::BitmapFileIndexReader;
use crate::file_index::bloom_filter::{BloomFilterReader, BloomFilterWriter};
use crate::file_index::file_index_reader::FileIndexReader;
use crate::file_index::file_index_writer::FileIndexWriter;
+use crate::file_index::range_bitmap::RangeBitmapFileIndexReader;
use crate::spec::DataType;
use crate::{Error, Result};
pub(crate) const BITMAP_INDEX: &str = "bitmap";
pub(crate) const BLOOM_FILTER_INDEX: &str = "bloom-filter";
+pub(crate) const RANGE_BITMAP_INDEX: &str = "range-bitmap";
+
+struct FailOpenFileIndexReader;
+
+impl FileIndexReader for FailOpenFileIndexReader {}
#[derive(Clone, Copy)]
enum BuiltinFileIndexer {
Bitmap,
BloomFilter,
+ RangeBitmap,
}
impl BuiltinFileIndexer {
@@ -40,6 +47,7 @@ impl BuiltinFileIndexer {
match identifier {
BITMAP_INDEX => Ok(Self::Bitmap),
BLOOM_FILTER_INDEX => Ok(Self::BloomFilter),
+ RANGE_BITMAP_INDEX => Ok(Self::RangeBitmap),
_ => Err(Error::Unsupported {
message: format!("Unknown file index identifier:
{identifier}"),
}),
@@ -52,7 +60,10 @@ pub(crate) struct FileIndexerFactory;
impl FileIndexerFactory {
pub(crate) fn is_supported(identifier: &str) -> bool {
- matches!(identifier, BITMAP_INDEX | BLOOM_FILTER_INDEX)
+ matches!(
+ identifier,
+ BITMAP_INDEX | BLOOM_FILTER_INDEX | RANGE_BITMAP_INDEX
+ )
}
pub(crate) fn create_writer(
@@ -67,6 +78,9 @@ impl FileIndexerFactory {
BuiltinFileIndexer::BloomFilter => {
Ok(Box::new(BloomFilterWriter::try_new(data_type, options)?))
}
+ BuiltinFileIndexer::RangeBitmap => Err(Error::Unsupported {
+ message: "Writing range-bitmap indexes is not supported
yet".to_string(),
+ }),
}
}
@@ -82,14 +96,30 @@ impl FileIndexerFactory {
BuiltinFileIndexer::BloomFilter => {
Ok(Box::new(BloomFilterReader::try_new(data_type,
serialized)?))
}
+ BuiltinFileIndexer::RangeBitmap => {
+ // File indexes are optional accelerators. Rust used to ignore
+ // range-bitmap payloads entirely, so a payload written by a
+ // newer Java version or damaged in storage must conservatively
+ // disable pruning instead of turning a readable data file into
+ // a query failure.
+ Ok(
+ match RangeBitmapFileIndexReader::try_new(data_type,
serialized) {
+ Ok(reader) => Box::new(reader),
+ Err(_) => Box::new(FailOpenFileIndexReader),
+ },
+ )
+ }
}
}
}
#[cfg(test)]
mod tests {
+ use bytes::{BufMut, BytesMut};
+
use super::*;
- use crate::spec::{BinaryType, BooleanType, Datum, IntType};
+ use crate::file_index::file_index_result::FileIndexResult;
+ use crate::spec::{BinaryType, BooleanType, Datum, IntType,
PredicateOperator};
fn int_type() -> DataType {
DataType::Int(IntType::new())
@@ -147,6 +177,11 @@ mod tests {
#[test]
fn test_unknown_identifier_is_rejected() {
+ assert!(FileIndexerFactory::is_supported(RANGE_BITMAP_INDEX));
+ assert!(matches!(
+ FileIndexerFactory::create_writer(RANGE_BITMAP_INDEX, int_type(),
&Options::new()),
+ Err(Error::Unsupported { .. })
+ ));
assert!(matches!(
FileIndexerFactory::create_writer("unknown", int_type(),
&Options::new()),
Err(Error::Unsupported { .. })
@@ -170,4 +205,76 @@ mod tests {
assert!(writer.empty(), "{identifier}");
}
}
+
+ #[test]
+ fn test_range_bitmap_huge_cardinality_fails_open() {
+ let mut dictionary = BytesMut::new();
+ dictionary.put_i32(13);
+ dictionary.put_u8(1);
+ dictionary.put_i32(0);
+ dictionary.put_i32(0);
+ dictionary.put_i32(0);
+
+ let mut serialized = BytesMut::new();
+ serialized.put_i32(21);
+ serialized.put_u8(1);
+ serialized.put_i32(i32::MAX);
+ serialized.put_i32(i32::MAX);
+ serialized.put_i32(0);
+ serialized.put_i32(0);
+ serialized.put_i32(dictionary.len() as i32);
+ serialized.extend_from_slice(&dictionary);
+ let serialized = serialized.freeze();
+
+ assert!(matches!(
+ RangeBitmapFileIndexReader::try_new(int_type(),
serialized.clone()),
+ Err(Error::FileIndexFormatInvalid { .. })
+ ));
+
+ let reader =
+ FileIndexerFactory::create_reader(RANGE_BITMAP_INDEX, int_type(),
serialized).unwrap();
+ assert_eq!(
+ FileIndexResult::Remain,
+ reader.evaluate("a", 0, &int_type(), PredicateOperator::Eq,
&[Datum::Int(0)])
+ );
+ }
+
+ #[test]
+ fn test_range_bitmap_malformed_bsi_fails_open() {
+ // Java V1 index for [1, 3, 5, 7, 9, null, null, 10]. Change its
+ // declared slice count from three to one while leaving the BSI header
+ // and payload otherwise intact.
+ let mut serialized = hex::decode(concat!(
+ "00000015010000000800000006000000010000000a000000420000000d010000",
+ "0001000000040000001900000000010000000100000000000000000000000500",
+ "00001400000004000000030000000500000007000000090000000a0000002201",
+ "030000001300000018000000000000001600000016000000140000002a000000",
+ "143b3000000100000500020000000400070000003a3000000100000000000200",
+ "100000000100030007003a300000010000000000010010000000020003003a30",
+ "000001000000000001001000000004000700"
+ ))
+ .unwrap();
+ let outer_header_length =
i32::from_be_bytes(serialized[0..4].try_into().unwrap()) as usize;
+ let dictionary_length_offset = 4 + outer_header_length - 4;
+ let dictionary_length = i32::from_be_bytes(
+ serialized[dictionary_length_offset..dictionary_length_offset + 4]
+ .try_into()
+ .unwrap(),
+ ) as usize;
+ let bsi_offset = 4 + outer_header_length + dictionary_length;
+ serialized[bsi_offset + 5] = 1;
+ let serialized = Bytes::from(serialized);
+
+ assert!(matches!(
+ RangeBitmapFileIndexReader::try_new(int_type(),
serialized.clone()),
+ Err(Error::FileIndexFormatInvalid { .. })
+ ));
+
+ let reader =
+ FileIndexerFactory::create_reader(RANGE_BITMAP_INDEX, int_type(),
serialized).unwrap();
+ assert_eq!(
+ FileIndexResult::Remain,
+ reader.evaluate("a", 0, &int_type(), PredicateOperator::Eq,
&[Datum::Int(1)])
+ );
+ }
}
diff --git a/crates/paimon/src/file_index/mod.rs
b/crates/paimon/src/file_index/mod.rs
index ba62ab8b..3e5b5132 100644
--- a/crates/paimon/src/file_index/mod.rs
+++ b/crates/paimon/src/file_index/mod.rs
@@ -33,5 +33,7 @@ pub(crate) mod file_index_result;
pub(crate) mod file_index_writer;
#[allow(dead_code)]
pub(crate) mod file_indexer_factory;
+#[allow(dead_code)]
+pub(crate) mod range_bitmap;
pub use file_index_format::*;
diff --git a/crates/paimon/src/file_index/range_bitmap.rs
b/crates/paimon/src/file_index/range_bitmap.rs
new file mode 100644
index 00000000..c488640e
--- /dev/null
+++ b/crates/paimon/src/file_index/range_bitmap.rs
@@ -0,0 +1,1548 @@
+// 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 Java Paimon's `range-bitmap` file index.
+//!
+//! The index maps ordered dictionary codes to row positions with a bit-sliced
+//! bitmap. Evaluating it produces the same conservative row selection consumed
+//! by the Parquet and Mosaic readers, allowing Mosaic to skip unselected row
+//! groups before decoding them.
+
+use std::cmp::Ordering;
+use std::io::Cursor;
+
+use bytes::Bytes;
+use roaring::RoaringBitmap;
+
+use crate::file_index::file_index_reader::FileIndexReader;
+use crate::file_index::file_index_result::FileIndexResult;
+use crate::spec::{DataType, Datum, PredicateOperator};
+use crate::{Error, Result};
+
+const VERSION_1: u8 = 1;
+const JAVA_CANONICAL_FLOAT_NAN_BITS: u32 = 0x7fc0_0000;
+const JAVA_CANONICAL_DOUBLE_NAN_BITS: u64 = 0x7ff8_0000_0000_0000;
+
+fn format_invalid(message: impl Into<String>) -> Error {
+ Error::FileIndexFormatInvalid {
+ message: message.into(),
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+struct JavaFloat(u32);
+
+impl JavaFloat {
+ fn new(value: f32) -> Self {
+ Self(if value.is_nan() {
+ JAVA_CANONICAL_FLOAT_NAN_BITS
+ } else {
+ value.to_bits()
+ })
+ }
+
+ fn value(self) -> f32 {
+ f32::from_bits(self.0)
+ }
+}
+
+impl PartialOrd for JavaFloat {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for JavaFloat {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.value().total_cmp(&other.value())
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+struct JavaDouble(u64);
+
+impl JavaDouble {
+ fn new(value: f64) -> Self {
+ Self(if value.is_nan() {
+ JAVA_CANONICAL_DOUBLE_NAN_BITS
+ } else {
+ value.to_bits()
+ })
+ }
+
+ fn value(self) -> f64 {
+ f64::from_bits(self.0)
+ }
+}
+
+impl PartialOrd for JavaDouble {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for JavaDouble {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.value().total_cmp(&other.value())
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+enum RangeValue {
+ Boolean(bool),
+ TinyInt(i8),
+ SmallInt(i16),
+ Int(i32),
+ BigInt(i64),
+ Float(JavaFloat),
+ Double(JavaDouble),
+ Decimal(i64),
+ Date(i32),
+ Time(i32),
+ Timestamp(i64),
+ LocalZonedTimestamp(i64),
+ String(String),
+}
+
+impl RangeValue {
+ fn is_nan(&self) -> bool {
+ match self {
+ Self::Float(value) => value.value().is_nan(),
+ Self::Double(value) => value.value().is_nan(),
+ _ => false,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum RangeValueCodec {
+ Boolean,
+ TinyInt,
+ SmallInt,
+ Int,
+ BigInt,
+ Float,
+ Double,
+ Decimal { scale: u32 },
+ Date,
+ Time,
+ TimestampMillis,
+ TimestampMicros,
+ LocalZonedTimestampMillis,
+ LocalZonedTimestampMicros,
+ String,
+}
+
+impl RangeValueCodec {
+ fn try_new(data_type: &DataType) -> Result<Self> {
+ match data_type {
+ DataType::Boolean(_) => Ok(Self::Boolean),
+ DataType::TinyInt(_) => Ok(Self::TinyInt),
+ DataType::SmallInt(_) => Ok(Self::SmallInt),
+ DataType::Int(_) => Ok(Self::Int),
+ DataType::BigInt(_) => Ok(Self::BigInt),
+ DataType::Float(_) => Ok(Self::Float),
+ DataType::Double(_) => Ok(Self::Double),
+ DataType::Decimal(decimal) if decimal.precision() <= 18 =>
Ok(Self::Decimal {
+ scale: decimal.scale(),
+ }),
+ DataType::Date(_) => Ok(Self::Date),
+ DataType::Time(_) => Ok(Self::Time),
+ DataType::Timestamp(timestamp) if timestamp.precision() <= 3 => {
+ Ok(Self::TimestampMillis)
+ }
+ DataType::Timestamp(timestamp) if timestamp.precision() <= 6 => {
+ Ok(Self::TimestampMicros)
+ }
+ DataType::LocalZonedTimestamp(timestamp) if timestamp.precision()
<= 3 => {
+ Ok(Self::LocalZonedTimestampMillis)
+ }
+ DataType::LocalZonedTimestamp(timestamp) if timestamp.precision()
<= 6 => {
+ Ok(Self::LocalZonedTimestampMicros)
+ }
+ DataType::Char(_) | DataType::VarChar(_) => Ok(Self::String),
+ _ => Err(Error::Unsupported {
+ message: format!(
+ "Range bitmap file index does not support data type
{data_type:?}"
+ ),
+ }),
+ }
+ }
+
+ fn fixed_length(self) -> Option<usize> {
+ match self {
+ Self::Boolean | Self::TinyInt => Some(1),
+ Self::SmallInt => Some(2),
+ Self::Int | Self::Float | Self::Date | Self::Time => Some(4),
+ Self::BigInt
+ | Self::Double
+ | Self::Decimal { .. }
+ | Self::TimestampMillis
+ | Self::TimestampMicros
+ | Self::LocalZonedTimestampMillis
+ | Self::LocalZonedTimestampMicros => Some(8),
+ Self::String => None,
+ }
+ }
+
+ fn read_value(self, input: &mut Decoder<'_>) -> Result<RangeValue> {
+ Ok(match self {
+ Self::Boolean => RangeValue::Boolean(input.read_u8("boolean
value")? == 1),
+ Self::TinyInt => RangeValue::TinyInt(input.read_u8("tinyint
value")? as i8),
+ Self::SmallInt => RangeValue::SmallInt(input.read_i16("smallint
value")?),
+ Self::Int => RangeValue::Int(input.read_i32("int value")?),
+ Self::BigInt => RangeValue::BigInt(input.read_i64("bigint
value")?),
+ Self::Float =>
RangeValue::Float(JavaFloat::new(input.read_f32("float value")?)),
+ Self::Double =>
RangeValue::Double(JavaDouble::new(input.read_f64("double value")?)),
+ Self::Decimal { .. } =>
RangeValue::Decimal(input.read_i64("decimal value")?),
+ Self::Date => RangeValue::Date(input.read_i32("date value")?),
+ Self::Time => RangeValue::Time(input.read_i32("time value")?),
+ Self::TimestampMillis | Self::TimestampMicros => {
+ RangeValue::Timestamp(input.read_i64("timestamp value")?)
+ }
+ Self::LocalZonedTimestampMillis | Self::LocalZonedTimestampMicros
=> {
+ RangeValue::LocalZonedTimestamp(input.read_i64("local zoned
timestamp value")?)
+ }
+ Self::String => {
+ let length = input.read_count("string value length")?;
+ let bytes = input.read_exact(length, "string value")?;
+ RangeValue::String(
+ std::str::from_utf8(bytes)
+ .map_err(|error| {
+ format_invalid(format!(
+ "invalid UTF-8 range bitmap string value:
{error}"
+ ))
+ })?
+ .to_string(),
+ )
+ }
+ })
+ }
+
+ fn value(self, datum: &Datum) -> Result<RangeValue> {
+ match (self, datum) {
+ (Self::Boolean, Datum::Bool(value)) =>
Ok(RangeValue::Boolean(*value)),
+ (Self::TinyInt, Datum::TinyInt(value)) =>
Ok(RangeValue::TinyInt(*value)),
+ (Self::SmallInt, Datum::SmallInt(value)) =>
Ok(RangeValue::SmallInt(*value)),
+ (Self::Int, Datum::Int(value)) => Ok(RangeValue::Int(*value)),
+ (Self::BigInt, Datum::Long(value)) =>
Ok(RangeValue::BigInt(*value)),
+ (Self::Float, Datum::Float(value)) =>
Ok(RangeValue::Float(JavaFloat::new(*value))),
+ (Self::Double, Datum::Double(value)) =>
Ok(RangeValue::Double(JavaDouble::new(*value))),
+ (
+ Self::Decimal { scale },
+ Datum::Decimal {
+ unscaled,
+ scale: datum_scale,
+ ..
+ },
+ ) if scale == *datum_scale => i64::try_from(*unscaled)
+ .map(RangeValue::Decimal)
+ .map_err(|_| Error::DataInvalid {
+ message: format!("Decimal unscaled value does not fit i64:
{unscaled}"),
+ source: None,
+ }),
+ (Self::Date, Datum::Date(value)) => Ok(RangeValue::Date(*value)),
+ (Self::Time, Datum::Time(value)) => Ok(RangeValue::Time(*value)),
+ (Self::TimestampMillis, Datum::Timestamp { millis, nanos }) => {
+ Ok(RangeValue::Timestamp(timestamp_millis(*millis, *nanos)?))
+ }
+ (Self::TimestampMicros, Datum::Timestamp { millis, nanos }) => {
+ Ok(RangeValue::Timestamp(timestamp_micros(*millis, *nanos)?))
+ }
+ (Self::LocalZonedTimestampMillis, Datum::LocalZonedTimestamp {
millis, nanos }) => Ok(
+ RangeValue::LocalZonedTimestamp(timestamp_millis(*millis,
*nanos)?),
+ ),
+ (Self::LocalZonedTimestampMicros, Datum::LocalZonedTimestamp {
millis, nanos }) => Ok(
+ RangeValue::LocalZonedTimestamp(timestamp_micros(*millis,
*nanos)?),
+ ),
+ (Self::String, Datum::String(value)) =>
Ok(RangeValue::String(value.clone())),
+ _ => Err(Error::DataInvalid {
+ message: format!("Datum {datum:?} does not match range bitmap
codec {self:?}"),
+ source: None,
+ }),
+ }
+ }
+}
+
+fn validate_nanos(nanos: i32) -> Result<()> {
+ if (0..=999_999).contains(&nanos) {
+ Ok(())
+ } else {
+ Err(Error::DataInvalid {
+ message: format!("Timestamp nanos-of-millisecond is out of range:
{nanos}"),
+ source: None,
+ })
+ }
+}
+
+// Truncating a predicate literal can remove matching rows from the index
+// selection before the residual predicate gets a chance to evaluate them.
+fn timestamp_millis(millis: i64, nanos: i32) -> Result<i64> {
+ validate_nanos(nanos)?;
+ if nanos != 0 {
+ return Err(Error::DataInvalid {
+ message: format!(
+ "Timestamp literal cannot be represented in milliseconds:
millis={millis}, nanos={nanos}"
+ ),
+ source: None,
+ });
+ }
+ Ok(millis)
+}
+
+fn timestamp_micros(millis: i64, nanos: i32) -> Result<i64> {
+ validate_nanos(nanos)?;
+ if nanos % 1_000 != 0 {
+ return Err(Error::DataInvalid {
+ message: format!(
+ "Timestamp literal cannot be represented in microseconds:
millis={millis}, nanos={nanos}"
+ ),
+ source: None,
+ });
+ }
+ millis
+ .checked_mul(1_000)
+ .and_then(|value| value.checked_add(i64::from(nanos / 1_000)))
+ .ok_or_else(|| Error::DataInvalid {
+ message: format!(
+ "Timestamp cannot be represented in microseconds:
millis={millis}, nanos={nanos}"
+ ),
+ source: None,
+ })
+}
+
+struct Decoder<'a> {
+ bytes: &'a [u8],
+ position: usize,
+}
+
+impl<'a> Decoder<'a> {
+ fn new(bytes: &'a [u8]) -> Self {
+ Self { bytes, position: 0 }
+ }
+
+ fn position(&self) -> usize {
+ self.position
+ }
+
+ fn remaining(&self) -> usize {
+ self.bytes.len() - self.position
+ }
+
+ fn read_exact(&mut self, length: usize, field: &str) -> Result<&'a [u8]> {
+ let end = self
+ .position
+ .checked_add(length)
+ .ok_or_else(|| format_invalid(format!("{field} range overflow")))?;
+ let value = self.bytes.get(self.position..end).ok_or_else(|| {
+ format_invalid(format!(
+ "truncated {field}: need {length} bytes, but only {} remain",
+ self.remaining()
+ ))
+ })?;
+ self.position = end;
+ Ok(value)
+ }
+
+ fn read_u8(&mut self, field: &str) -> Result<u8> {
+ Ok(self.read_exact(1, field)?[0])
+ }
+
+ fn read_i16(&mut self, field: &str) -> Result<i16> {
+ Ok(i16::from_be_bytes(
+ self.read_exact(2, field)?.try_into().unwrap(),
+ ))
+ }
+
+ fn read_i32(&mut self, field: &str) -> Result<i32> {
+ Ok(i32::from_be_bytes(
+ self.read_exact(4, field)?.try_into().unwrap(),
+ ))
+ }
+
+ fn read_i64(&mut self, field: &str) -> Result<i64> {
+ Ok(i64::from_be_bytes(
+ self.read_exact(8, field)?.try_into().unwrap(),
+ ))
+ }
+
+ fn read_f32(&mut self, field: &str) -> Result<f32> {
+ Ok(f32::from_bits(self.read_i32(field)? as u32))
+ }
+
+ fn read_f64(&mut self, field: &str) -> Result<f64> {
+ Ok(f64::from_bits(self.read_i64(field)? as u64))
+ }
+
+ fn read_count(&mut self, field: &str) -> Result<usize> {
+ let value = self.read_i32(field)?;
+ usize::try_from(value).map_err(|_| format_invalid(format!("negative
{field}: {value}")))
+ }
+}
+
+fn read_sized_header<'a>(input: &mut Decoder<'a>, field: &str) ->
Result<Decoder<'a>> {
+ let length = input.read_count(&format!("{field} header length"))?;
+ Ok(Decoder::new(
+ input.read_exact(length, &format!("{field} header"))?,
+ ))
+}
+
+fn ensure_consumed(input: &Decoder<'_>, field: &str) -> Result<()> {
+ if input.remaining() == 0 {
+ Ok(())
+ } else {
+ Err(format_invalid(format!(
+ "{field} has {} trailing bytes",
+ input.remaining()
+ )))
+ }
+}
+
+fn read_version(input: &mut Decoder<'_>, field: &str) -> Result<()> {
+ let version = input.read_u8(&format!("{field} version"))?;
+ if version == VERSION_1 {
+ Ok(())
+ } else {
+ Err(format_invalid(format!(
+ "unsupported {field} version: {version}"
+ )))
+ }
+}
+
+fn push_sorted_value(values: &mut Vec<RangeValue>, value: RangeValue) ->
Result<()> {
+ if values.last().is_some_and(|previous| previous >= &value) {
+ return Err(format_invalid(
+ "range bitmap dictionary values are not strictly sorted",
+ ));
+ }
+ values.push(value);
+ Ok(())
+}
+
+fn parse_dictionary(
+ serialized: &[u8],
+ codec: RangeValueCodec,
+ cardinality: usize,
+) -> Result<Vec<RangeValue>> {
+ let mut input = Decoder::new(serialized);
+ let mut header = read_sized_header(&mut input, "range bitmap dictionary")?;
+ read_version(&mut header, "range bitmap dictionary")?;
+ let chunk_count = header.read_count("dictionary chunk count")?;
+ let offsets_length = header.read_count("dictionary offsets length")?;
+ let chunks_length = header.read_count("dictionary chunks length")?;
+ ensure_consumed(&header, "range bitmap dictionary header")?;
+
+ let expected_offsets_length = chunk_count
+ .checked_mul(4)
+ .ok_or_else(|| format_invalid("dictionary offsets length overflow"))?;
+ if offsets_length != expected_offsets_length {
+ return Err(format_invalid(format!(
+ "dictionary offsets length {offsets_length} does not match
{chunk_count} chunks"
+ )));
+ }
+
+ let offsets_bytes = input.read_exact(offsets_length, "dictionary
offsets")?;
+ let chunks = input.read_exact(chunks_length, "dictionary chunks")?;
+ let keys = input.read_exact(input.remaining(), "dictionary keys")?;
+
+ let mut offsets_input = Decoder::new(offsets_bytes);
+ let mut offsets = Vec::with_capacity(chunk_count);
+ for index in 0..chunk_count {
+ let offset = offsets_input.read_count(&format!("dictionary chunk
{index} offset"))?;
+ if index == 0 && offset != 0 {
+ return Err(format_invalid(format!(
+ "first dictionary chunk offset must be 0, but was {offset}"
+ )));
+ }
+ if offsets.last().is_some_and(|previous| *previous >= offset) {
+ return Err(format_invalid("dictionary chunk offsets do not
increase"));
+ }
+ if offset >= chunks_length {
+ return Err(format_invalid(format!(
+ "dictionary chunk offset {offset} exceeds chunk area length
{chunks_length}"
+ )));
+ }
+ offsets.push(offset);
+ }
+ if chunk_count == 0 && (chunks_length != 0 || !keys.is_empty()) {
+ return Err(format_invalid(
+ "empty dictionary contains chunk or key bytes",
+ ));
+ }
+
+ // Every dictionary value needs at least one byte in the serialized
+ // dictionary (fixed-width values store that byte directly, while strings
+ // need a length or offset). Reject an impossible cardinality before using
+ // the untrusted header value as an allocation size. `try_reserve_exact`
+ // then turns a genuine allocation failure into the same fail-open format
+ // error as any other malformed index.
+ if cardinality > serialized.len() {
+ return Err(format_invalid(format!(
+ "range bitmap cardinality {cardinality} exceeds dictionary payload
size {}",
+ serialized.len()
+ )));
+ }
+ let mut values = Vec::new();
+ values.try_reserve_exact(cardinality).map_err(|error| {
+ format_invalid(format!(
+ "failed to allocate range bitmap dictionary for {cardinality}
values: {error}"
+ ))
+ })?;
+ let mut expected_key_offset = 0usize;
+ for index in 0..chunk_count {
+ let start = offsets[index];
+ let end = offsets.get(index + 1).copied().unwrap_or(chunks_length);
+ let mut chunk = Decoder::new(&chunks[start..end]);
+ read_version(&mut chunk, "range bitmap dictionary chunk")?;
+ let first = codec.read_value(&mut chunk)?;
+ let code = chunk.read_count("dictionary chunk code")?;
+ if code != values.len() {
+ return Err(format_invalid(format!(
+ "dictionary chunk code {code} does not match expected {}",
+ values.len()
+ )));
+ }
+ let key_offset = chunk.read_count("dictionary key offset")?;
+ if key_offset != expected_key_offset {
+ return Err(format_invalid(format!(
+ "dictionary key offset {key_offset} does not match expected
{expected_key_offset}"
+ )));
+ }
+ let additional_count = chunk.read_count("dictionary chunk value
count")?;
+ push_sorted_value(&mut values, first)?;
+
+ if let Some(fixed_length) = codec.fixed_length() {
+ let keys_length = chunk.read_count("dictionary fixed keys
length")?;
+ let encoded_fixed_length = chunk.read_count("dictionary fixed key
length")?;
+ ensure_consumed(&chunk, "fixed dictionary chunk")?;
+ if encoded_fixed_length != fixed_length {
+ return Err(format_invalid(format!(
+ "dictionary fixed key length {encoded_fixed_length} does
not match {fixed_length}"
+ )));
+ }
+ let expected_length = additional_count
+ .checked_mul(fixed_length)
+ .ok_or_else(|| format_invalid("dictionary fixed keys length
overflow"))?;
+ if keys_length != expected_length {
+ return Err(format_invalid(format!(
+ "dictionary fixed keys length {keys_length} does not match
{expected_length}"
+ )));
+ }
+ let key_end = key_offset
+ .checked_add(keys_length)
+ .ok_or_else(|| format_invalid("dictionary fixed keys range
overflow"))?;
+ let mut key_input =
+ Decoder::new(keys.get(key_offset..key_end).ok_or_else(|| {
+ format_invalid("dictionary fixed keys range exceeds key
area")
+ })?);
+ for _ in 0..additional_count {
+ push_sorted_value(&mut values, codec.read_value(&mut
key_input)?)?;
+ }
+ ensure_consumed(&key_input, "dictionary fixed keys")?;
+ expected_key_offset = key_end;
+ } else {
+ let inner_offsets_length = chunk.read_count("dictionary string
offsets length")?;
+ let keys_length = chunk.read_count("dictionary string keys
length")?;
+ ensure_consumed(&chunk, "variable dictionary chunk")?;
+ let expected_length = additional_count
+ .checked_mul(4)
+ .ok_or_else(|| format_invalid("dictionary string offsets
length overflow"))?;
+ if inner_offsets_length != expected_length {
+ return Err(format_invalid(format!(
+ "dictionary string offsets length {inner_offsets_length}
does not match {expected_length}"
+ )));
+ }
+ let total_length = inner_offsets_length
+ .checked_add(keys_length)
+ .ok_or_else(|| format_invalid("dictionary string keys length
overflow"))?;
+ let key_end = key_offset
+ .checked_add(total_length)
+ .ok_or_else(|| format_invalid("dictionary string keys range
overflow"))?;
+ let payload = keys
+ .get(key_offset..key_end)
+ .ok_or_else(|| format_invalid("dictionary string keys range
exceeds key area"))?;
+ let mut inner_offsets_input =
Decoder::new(&payload[..inner_offsets_length]);
+ let mut inner_offsets = Vec::with_capacity(additional_count);
+ for value_index in 0..additional_count {
+ let offset = inner_offsets_input
+ .read_count(&format!("dictionary string {value_index}
offset"))?;
+ if value_index == 0 && offset != 0 {
+ return Err(format_invalid(format!(
+ "first dictionary string offset must be 0, but was
{offset}"
+ )));
+ }
+ if inner_offsets
+ .last()
+ .is_some_and(|previous| *previous >= offset)
+ {
+ return Err(format_invalid("dictionary string offsets do
not increase"));
+ }
+ if offset >= keys_length {
+ return Err(format_invalid(format!(
+ "dictionary string offset {offset} exceeds key length
{keys_length}"
+ )));
+ }
+ inner_offsets.push(offset);
+ }
+ let encoded_keys = &payload[inner_offsets_length..];
+ for value_index in 0..additional_count {
+ let start = inner_offsets[value_index];
+ let end = inner_offsets
+ .get(value_index + 1)
+ .copied()
+ .unwrap_or(keys_length);
+ let mut value_input = Decoder::new(&encoded_keys[start..end]);
+ push_sorted_value(&mut values, codec.read_value(&mut
value_input)?)?;
+ ensure_consumed(&value_input, "dictionary string value")?;
+ }
+ expected_key_offset = key_end;
+ }
+ }
+
+ if expected_key_offset != keys.len() {
+ return Err(format_invalid(format!(
+ "dictionary used {expected_key_offset} of {} key bytes",
+ keys.len()
+ )));
+ }
+ if values.len() != cardinality {
+ return Err(format_invalid(format!(
+ "dictionary contains {} values, expected {cardinality}",
+ values.len()
+ )));
+ }
+ Ok(values)
+}
+
+fn deserialize_bitmap(bytes: &[u8], row_count: u32, field: &str) ->
Result<RoaringBitmap> {
+ let mut cursor = Cursor::new(bytes);
+ let bitmap = RoaringBitmap::deserialize_from(&mut cursor)
+ .map_err(|error| format_invalid(format!("invalid RoaringBitmap for
{field}: {error}")))?;
+ if cursor.position() != bytes.len() as u64 {
+ return Err(format_invalid(format!(
+ "RoaringBitmap for {field} consumed {} of {} bytes",
+ cursor.position(),
+ bytes.len()
+ )));
+ }
+ if let Some(position) = bitmap.max() {
+ if position >= row_count {
+ return Err(format_invalid(format!(
+ "RoaringBitmap row position {position} exceeds row count
{row_count}"
+ )));
+ }
+ }
+ Ok(bitmap)
+}
+
+struct BitSliceIndex {
+ existing: RoaringBitmap,
+ slices: Vec<RoaringBitmap>,
+}
+
+impl BitSliceIndex {
+ fn parse(serialized: &[u8], row_count: u32, cardinality: usize) ->
Result<Self> {
+ let mut input = Decoder::new(serialized);
+ let mut header = read_sized_header(&mut input, "range bitmap BSI")?;
+ read_version(&mut header, "range bitmap BSI")?;
+ let slice_count = header.read_u8("BSI slice count")? as usize;
+ if slice_count == 0 || slice_count > 64 {
+ return Err(format_invalid(format!(
+ "invalid BSI slice count: {slice_count}"
+ )));
+ }
+ if cardinality > 0 {
+ // Java's writer constructs the BSI for codes in
+ // [0, cardinality - 1] and always emits at least one slice. An
+ // undersized BSI silently aliases dictionary codes, while an
+ // oversized one can introduce codes the dictionary cannot resolve.
+ let max_code = cardinality - 1;
+ let required_slice_count = ((usize::BITS -
max_code.leading_zeros()) as usize).max(1);
+ if slice_count != required_slice_count {
+ return Err(format_invalid(format!(
+ "BSI slice count {slice_count} does not match the
{required_slice_count} slices required for dictionary cardinality {cardinality}"
+ )));
+ }
+ }
+ let existing_length = header.read_count("BSI existence bitmap
length")?;
+ let indexes_length = header.read_count("BSI indexes length")?;
+ let expected_indexes_length = slice_count
+ .checked_mul(8)
+ .ok_or_else(|| format_invalid("BSI indexes length overflow"))?;
+ if indexes_length != expected_indexes_length {
+ return Err(format_invalid(format!(
+ "BSI indexes length {indexes_length} does not match
{slice_count} slices"
+ )));
+ }
+ let indexes = header.read_exact(indexes_length, "BSI indexes")?;
+ ensure_consumed(&header, "range bitmap BSI header")?;
+
+ let existing_bytes = input.read_exact(existing_length, "BSI existence
bitmap")?;
+ let existing = deserialize_bitmap(existing_bytes, row_count, "BSI
existence bitmap")?;
+ if cardinality == 0 && !existing.is_empty() {
+ return Err(format_invalid(
+ "empty range bitmap dictionary has non-empty existence bitmap",
+ ));
+ }
+ if cardinality > existing.len() as usize {
+ return Err(format_invalid(format!(
+ "range bitmap cardinality {cardinality} exceeds {} non-null
rows",
+ existing.len()
+ )));
+ }
+
+ let slice_bytes = input.read_exact(input.remaining(), "BSI slice
bitmaps")?;
+ let mut indexes_input = Decoder::new(indexes);
+ let mut slices = Vec::with_capacity(slice_count);
+ let mut expected_offset = 0usize;
+ for index in 0..slice_count {
+ let offset = indexes_input.read_count(&format!("BSI slice {index}
offset"))?;
+ let length = indexes_input.read_count(&format!("BSI slice {index}
length"))?;
+ if offset != expected_offset {
+ return Err(format_invalid(format!(
+ "BSI slice {index} offset {offset} does not match expected
{expected_offset}"
+ )));
+ }
+ let end = offset
+ .checked_add(length)
+ .ok_or_else(|| format_invalid("BSI slice range overflow"))?;
+ let slice = deserialize_bitmap(
+ slice_bytes
+ .get(offset..end)
+ .ok_or_else(|| format_invalid("BSI slice exceeds
payload"))?,
+ row_count,
+ &format!("BSI slice {index}"),
+ )?;
+ let mut outside = slice.clone();
+ outside -= &existing;
+ if !outside.is_empty() {
+ return Err(format_invalid(format!(
+ "BSI slice {index} contains rows outside the existence
bitmap"
+ )));
+ }
+ slices.push(slice);
+ expected_offset = end;
+ }
+ if expected_offset != slice_bytes.len() {
+ return Err(format_invalid(format!(
+ "BSI slices used {expected_offset} of {} bytes",
+ slice_bytes.len()
+ )));
+ }
+ let bsi = Self { existing, slices };
+ if cardinality > 0 {
+ let invalid_codes = bsi.gte(cardinality);
+ if !invalid_codes.is_empty() {
+ return Err(format_invalid(format!(
+ "BSI contains {} rows with codes outside dictionary
cardinality {cardinality}",
+ invalid_codes.len()
+ )));
+ }
+ }
+ Ok(bsi)
+ }
+
+ fn eq(&self, code: usize) -> RoaringBitmap {
+ let mut selected = self.existing.clone();
+ for (index, slice) in self.slices.iter().enumerate() {
+ if ((code >> index) & 1) == 1 {
+ selected &= slice;
+ } else {
+ selected -= slice;
+ }
+ }
+ selected
+ }
+
+ fn gt(&self, code: i64) -> RoaringBitmap {
+ if code < 0 {
+ return self.existing.clone();
+ }
+ let code = code as u64;
+ let start = code.trailing_ones() as usize;
+ let mut state = None;
+ for (index, slice) in self.slices.iter().enumerate().skip(start) {
+ match &mut state {
+ None => state = Some(slice.clone()),
+ Some(state) if ((code >> index) & 1) == 1 => *state &= slice,
+ Some(state) => *state |= slice,
+ }
+ }
+ let mut selected = state.unwrap_or_default();
+ selected &= &self.existing;
+ selected
+ }
+
+ fn gte(&self, code: usize) -> RoaringBitmap {
+ if code == 0 {
+ self.existing.clone()
+ } else {
+ self.gt(code as i64 - 1)
+ }
+ }
+}
+
+/// Java-compatible Range Bitmap V1 reader.
+pub(crate) struct RangeBitmapFileIndexReader {
+ codec: RangeValueCodec,
+ row_count: u32,
+ dictionary: Vec<RangeValue>,
+ bsi: BitSliceIndex,
+}
+
+impl RangeBitmapFileIndexReader {
+ pub(crate) fn try_new(data_type: DataType, serialized: Bytes) ->
Result<Self> {
+ let codec = RangeValueCodec::try_new(&data_type)?;
+ let mut input = Decoder::new(&serialized);
+ let mut header = read_sized_header(&mut input, "range bitmap")?;
+ read_version(&mut header, "range bitmap")?;
+ let row_count = u32::try_from(header.read_i32("range bitmap row
count")?)
+ .map_err(|_| format_invalid("range bitmap row count must be
non-negative"))?;
+ let cardinality = header.read_count("range bitmap cardinality")?;
+ if cardinality > row_count as usize {
+ return Err(format_invalid(format!(
+ "range bitmap cardinality {cardinality} exceeds row count
{row_count}"
+ )));
+ }
+ let min = (cardinality > 0)
+ .then(|| codec.read_value(&mut header))
+ .transpose()?;
+ let max = (cardinality > 0)
+ .then(|| codec.read_value(&mut header))
+ .transpose()?;
+ let dictionary_length = header.read_count("range bitmap dictionary
length")?;
+ ensure_consumed(&header, "range bitmap header")?;
+
+ let dictionary_bytes = input.read_exact(dictionary_length, "range
bitmap dictionary")?;
+ let dictionary = parse_dictionary(dictionary_bytes, codec,
cardinality)?;
+ if dictionary.first() != min.as_ref() || dictionary.last() !=
max.as_ref() {
+ return Err(format_invalid(
+ "range bitmap min/max do not match the dictionary",
+ ));
+ }
+ let bsi = BitSliceIndex::parse(
+ input.read_exact(input.remaining(), "range bitmap BSI")?,
+ row_count,
+ cardinality,
+ )?;
+
+ Ok(Self {
+ codec,
+ row_count,
+ dictionary,
+ bsi,
+ })
+ }
+
+ fn all_rows(&self) -> RoaringBitmap {
+ let mut rows = RoaringBitmap::new();
+ rows.insert_range(0..self.row_count);
+ rows
+ }
+
+ fn is_null(&self) -> RoaringBitmap {
+ let mut rows = self.all_rows();
+ rows -= &self.bsi.existing;
+ rows
+ }
+
+ fn not(&self, excluded: &RoaringBitmap) -> RoaringBitmap {
+ let mut selected = self.bsi.existing.clone();
+ selected -= excluded;
+ selected
+ }
+
+ fn include_nan_rows(&self, mut selected: RoaringBitmap) -> RoaringBitmap {
+ let nan = match self.codec {
+ RangeValueCodec::Float =>
Some(RangeValue::Float(JavaFloat::new(f32::NAN))),
+ RangeValueCodec::Double =>
Some(RangeValue::Double(JavaDouble::new(f64::NAN))),
+ _ => None,
+ };
+ if let Some(nan) = nan {
+ selected |= self.eq(&nan);
+ }
+ selected
+ }
+
+ fn eq(&self, value: &RangeValue) -> RoaringBitmap {
+ self.dictionary
+ .binary_search(value)
+ .map_or_else(|_| RoaringBitmap::new(), |code| self.bsi.eq(code))
+ }
+
+ fn gt(&self, value: &RangeValue) -> RoaringBitmap {
+ match self.dictionary.binary_search(value) {
+ Ok(code) => self.bsi.gt(code as i64),
+ Err(code) => self.bsi.gte(code),
+ }
+ }
+
+ fn gte(&self, value: &RangeValue) -> RoaringBitmap {
+ let code = self
+ .dictionary
+ .binary_search(value)
+ .unwrap_or_else(|code| code);
+ self.bsi.gte(code)
+ }
+
+ fn lt(&self, value: &RangeValue) -> RoaringBitmap {
+ self.not(&self.gte(value))
+ }
+
+ fn lte(&self, value: &RangeValue) -> RoaringBitmap {
+ self.not(&self.gt(value))
+ }
+
+ fn literals_bitmap(&self, literals: &[Datum], skip_nan: bool) ->
Result<RoaringBitmap> {
+ let mut selected = RoaringBitmap::new();
+ for literal in literals {
+ let value = self.codec.value(literal)?;
+ if !skip_nan || !value.is_nan() {
+ selected |= self.eq(&value);
+ }
+ }
+ Ok(selected)
+ }
+
+ pub(crate) fn try_evaluate(
+ &self,
+ data_type: &DataType,
+ operator: PredicateOperator,
+ literals: &[Datum],
+ ) -> Result<FileIndexResult> {
+ if RangeValueCodec::try_new(data_type).ok() != Some(self.codec) {
+ return Ok(FileIndexResult::Remain);
+ }
+
+ let selected = match operator {
+ PredicateOperator::IsNull if literals.is_empty() => self.is_null(),
+ PredicateOperator::IsNotNull if literals.is_empty() =>
self.bsi.existing.clone(),
+ PredicateOperator::Eq if literals.len() == 1 => {
+ self.eq(&self.codec.value(&literals[0])?)
+ }
+ PredicateOperator::NotEq if literals.len() == 1 => {
+ let value = self.codec.value(&literals[0])?;
+ if value.is_nan() {
+ self.bsi.existing.clone()
+ } else {
+ self.not(&self.eq(&value))
+ }
+ }
+ PredicateOperator::In => self.literals_bitmap(literals, false)?,
+ PredicateOperator::NotIn =>
self.not(&self.literals_bitmap(literals, true)?),
+ PredicateOperator::Lt if literals.len() == 1 => {
+ let value = self.codec.value(&literals[0])?;
+ if value.is_nan() {
+ return Ok(FileIndexResult::Remain);
+ }
+ self.include_nan_rows(self.lt(&value))
+ }
+ PredicateOperator::LtEq if literals.len() == 1 => {
+ let value = self.codec.value(&literals[0])?;
+ if value.is_nan() {
+ return Ok(FileIndexResult::Remain);
+ }
+ self.include_nan_rows(self.lte(&value))
+ }
+ PredicateOperator::Gt if literals.len() == 1 => {
+ let value = self.codec.value(&literals[0])?;
+ if value.is_nan() {
+ return Ok(FileIndexResult::Remain);
+ }
+ self.include_nan_rows(self.gt(&value))
+ }
+ PredicateOperator::GtEq if literals.len() == 1 => {
+ let value = self.codec.value(&literals[0])?;
+ if value.is_nan() {
+ return Ok(FileIndexResult::Remain);
+ }
+ self.include_nan_rows(self.gte(&value))
+ }
+ PredicateOperator::Between if literals.len() == 2 => {
+ let lower = self.codec.value(&literals[0])?;
+ let upper = self.codec.value(&literals[1])?;
+ if lower.is_nan() || upper.is_nan() {
+ return Ok(FileIndexResult::Remain);
+ }
+ let mut selected = self.gte(&lower);
+ selected &= self.lte(&upper);
+ self.include_nan_rows(selected)
+ }
+ _ => return Ok(FileIndexResult::Remain),
+ };
+ Ok(FileIndexResult::Selection(selected))
+ }
+}
+
+impl FileIndexReader for RangeBitmapFileIndexReader {
+ fn evaluate(
+ &self,
+ _column: &str,
+ _index: usize,
+ data_type: &DataType,
+ operator: PredicateOperator,
+ literals: &[Datum],
+ ) -> FileIndexResult {
+ // Predicate evaluation must fail open. A malformed lazy value or an
+ // incompatible literal can reduce performance, but must never lose
rows.
+ self.try_evaluate(data_type, operator, literals)
+ .unwrap_or(FileIndexResult::Remain)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::Arc;
+
+ use arrow_array::{ArrayRef, Float32Array};
+ use bytes::{BufMut, BytesMut};
+
+ use super::*;
+ use crate::arrow::residual::evaluate_exact_leaf_predicate;
+ use crate::spec::{IntType, LocalZonedTimestampType, TimestampType};
+
+ // Generated by Java RangeBitmapFileIndex.Writer on Apache Paimon master
+ // commit 2bbdb668f for [1, 3, 5, 7, 9, null, null, 10].
+ const JAVA_INT_V1: &str = concat!(
+ "00000015010000000800000006000000010000000a000000420000000d010000",
+ "0001000000040000001900000000010000000100000000000000000000000500",
+ "00001400000004000000030000000500000007000000090000000a0000002201",
+ "030000001300000018000000000000001600000016000000140000002a000000",
+ "143b3000000100000500020000000400070000003a3000000100000000000200",
+ "100000000100030007003a300000010000000000010010000000020003003a30",
+ "000001000000000001001000000004000700"
+ );
+ // Generated by the same Java writer for ["aa", "b", "你好", null, "ccc"].
+ const JAVA_STRING_V1: &str = concat!(
+ "0000001d01000000050000000400000002616100000006e4bda0e5a5bd000000",
+ "520000000d0100000001000000040000001b0000000001000000026161000000",
+ "0000000000000000030000000c0000001600000000000000050000000c000000",
+ "01620000000363636300000006e4bda0e5a5bd0000001a010200000018000000",
+ "10000000000000001400000014000000143a3000000100000000000300100000",
+ "0000000100020004003a300000010000000000010010000000010002003a3000",
+ "0001000000000001001000000002000400"
+ );
+ // Generated by the same Java writer for [-0.0, +0.0, 1.5, NaN, null].
+ const JAVA_FLOAT_V1: &str = concat!(
+ "00000015010000000500000004800000007fc000000000003a0000000d010000",
+ "0001000000040000001900000000018000000000000000000000000000000300",
+ "00000c00000004000000003fc000007fc000000000001a01020000000f000000",
+ "10000000000000001400000014000000143b3000000100000300010000000300",
+ "3a300000010000000000010010000000010003003a3000000100000000000100",
+ "1000000002000300"
+ );
+
+ fn int_type() -> DataType {
+ DataType::Int(IntType::new())
+ }
+
+ fn reader(bytes: Bytes) -> RangeBitmapFileIndexReader {
+ RangeBitmapFileIndexReader::try_new(int_type(), bytes).unwrap()
+ }
+
+ fn selection(rows: impl IntoIterator<Item = u32>) -> FileIndexResult {
+ FileIndexResult::Selection(rows.into_iter().collect())
+ }
+
+ fn assert_covers_residual(
+ reader: &RangeBitmapFileIndexReader,
+ array: &ArrayRef,
+ data_type: &DataType,
+ operator: PredicateOperator,
+ literals: &[Datum],
+ ) {
+ let residual = evaluate_exact_leaf_predicate(array, data_type,
operator, literals).unwrap();
+ let indexed = reader.try_evaluate(data_type, operator,
literals).unwrap();
+ for (row, matches) in residual.iter().enumerate() {
+ if matches != Some(true) {
+ continue;
+ }
+ let selected = match &indexed {
+ FileIndexResult::Remain => true,
+ FileIndexResult::Skip => false,
+ FileIndexResult::Selection(rows) => rows.contains(row as u32),
+ };
+ assert!(
+ selected,
+ "range bitmap dropped residual match at row {row} for
{operator:?} {literals:?}"
+ );
+ }
+ }
+
+ fn single_value_reader(
+ codec: RangeValueCodec,
+ value: RangeValue,
+ ) -> RangeBitmapFileIndexReader {
+ let mut existing = RoaringBitmap::new();
+ existing.insert(0);
+ RangeBitmapFileIndexReader {
+ codec,
+ row_count: 1,
+ dictionary: vec![value],
+ bsi: BitSliceIndex {
+ existing,
+ slices: vec![],
+ },
+ }
+ }
+
+ fn evaluate(
+ reader: &RangeBitmapFileIndexReader,
+ operator: PredicateOperator,
+ literals: &[Datum],
+ ) -> FileIndexResult {
+ reader
+ .try_evaluate(&int_type(), operator, literals)
+ .unwrap()
+ }
+
+ fn java_all_null_int_index(row_count: u32) -> Bytes {
+ let empty = {
+ let mut bytes = Vec::new();
+ RoaringBitmap::new().serialize_into(&mut bytes).unwrap();
+ bytes
+ };
+
+ let mut dictionary = BytesMut::new();
+ dictionary.put_i32(13);
+ dictionary.put_u8(VERSION_1);
+ dictionary.put_i32(0);
+ dictionary.put_i32(0);
+ dictionary.put_i32(0);
+
+ // Java's empty RangeBitmap creates a BSI with 64 empty slices because
+ // its maximum dictionary code is -1.
+ let slice_count = 64usize;
+ let indexes_length = slice_count * 8;
+ let mut bsi = BytesMut::new();
+ bsi.put_i32((1 + 1 + 4 + 4 + indexes_length) as i32);
+ bsi.put_u8(VERSION_1);
+ bsi.put_u8(slice_count as u8);
+ bsi.put_i32(empty.len() as i32);
+ bsi.put_i32(indexes_length as i32);
+ for index in 0..slice_count {
+ bsi.put_i32((index * empty.len()) as i32);
+ bsi.put_i32(empty.len() as i32);
+ }
+ bsi.extend_from_slice(&empty);
+ for _ in 0..slice_count {
+ bsi.extend_from_slice(&empty);
+ }
+
+ let mut serialized = BytesMut::new();
+ serialized.put_i32(13);
+ serialized.put_u8(VERSION_1);
+ serialized.put_i32(row_count as i32);
+ serialized.put_i32(0);
+ serialized.put_i32(dictionary.len() as i32);
+ serialized.extend_from_slice(&dictionary);
+ serialized.extend_from_slice(&bsi);
+ serialized.freeze()
+ }
+
+ fn int_index_with_bsi_slices(
+ dictionary_values: &[i32],
+ existing_rows: &[u32],
+ slices: &[&[u32]],
+ ) -> Bytes {
+ assert!(!dictionary_values.is_empty());
+ let bitmap_bytes = |positions: &[u32]| {
+ let bitmap = positions.iter().copied().collect::<RoaringBitmap>();
+ let mut bytes = Vec::new();
+ bitmap.serialize_into(&mut bytes).unwrap();
+ bytes
+ };
+
+ // One fixed-width dictionary chunk. The first value lives in the
+ // chunk and the rest in the keys area.
+ let mut chunk = BytesMut::new();
+ chunk.put_u8(VERSION_1);
+ chunk.put_i32(dictionary_values[0]);
+ chunk.put_i32(0);
+ chunk.put_i32(0);
+ chunk.put_i32((dictionary_values.len() - 1) as i32);
+ chunk.put_i32(((dictionary_values.len() - 1) * 4) as i32);
+ chunk.put_i32(4);
+
+ let mut dictionary = BytesMut::new();
+ dictionary.put_i32(13);
+ dictionary.put_u8(VERSION_1);
+ dictionary.put_i32(1);
+ dictionary.put_i32(4);
+ dictionary.put_i32(chunk.len() as i32);
+ dictionary.put_i32(0);
+ dictionary.extend_from_slice(&chunk);
+ for value in &dictionary_values[1..] {
+ dictionary.put_i32(*value);
+ }
+
+ let existing = bitmap_bytes(existing_rows);
+ let encoded_slices = slices
+ .iter()
+ .map(|positions| bitmap_bytes(positions))
+ .collect::<Vec<_>>();
+ let indexes_length = slices.len() * 8;
+ let mut bsi = BytesMut::new();
+ bsi.put_i32((1 + 1 + 4 + 4 + indexes_length) as i32);
+ bsi.put_u8(VERSION_1);
+ bsi.put_u8(slices.len() as u8);
+ bsi.put_i32(existing.len() as i32);
+ bsi.put_i32(indexes_length as i32);
+ let mut offset = 0usize;
+ for slice in &encoded_slices {
+ bsi.put_i32(offset as i32);
+ bsi.put_i32(slice.len() as i32);
+ offset += slice.len();
+ }
+ bsi.extend_from_slice(&existing);
+ for slice in encoded_slices {
+ bsi.extend_from_slice(&slice);
+ }
+
+ let mut serialized = BytesMut::new();
+ serialized.put_i32(21);
+ serialized.put_u8(VERSION_1);
+ serialized.put_i32(existing_rows.iter().copied().max().map_or(0, |max|
max + 1) as i32);
+ serialized.put_i32(dictionary_values.len() as i32);
+ serialized.put_i32(dictionary_values[0]);
+ serialized.put_i32(*dictionary_values.last().unwrap());
+ serialized.put_i32(dictionary.len() as i32);
+ serialized.extend_from_slice(&dictionary);
+ serialized.extend_from_slice(&bsi);
+ serialized.freeze()
+ }
+
+ #[test]
+ fn test_java_int_v1_predicates() {
+ let reader = reader(Bytes::from(hex::decode(JAVA_INT_V1).unwrap()));
+
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::Eq, &[Datum::Int(5)]),
+ selection([2])
+ );
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::Eq, &[Datum::Int(6)]),
+ selection([])
+ );
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::NotEq, &[Datum::Int(5)]),
+ selection([0, 1, 3, 4, 7])
+ );
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::Lt, &[Datum::Int(5)]),
+ selection([0, 1])
+ );
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::LtEq, &[Datum::Int(5)]),
+ selection([0, 1, 2])
+ );
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::Gt, &[Datum::Int(6)]),
+ selection([3, 4, 7])
+ );
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::GtEq, &[Datum::Int(9)]),
+ selection([4, 7])
+ );
+ assert_eq!(
+ evaluate(
+ &reader,
+ PredicateOperator::In,
+ &[Datum::Int(1), Datum::Int(10)]
+ ),
+ selection([0, 7])
+ );
+ assert_eq!(
+ evaluate(
+ &reader,
+ PredicateOperator::NotIn,
+ &[Datum::Int(1), Datum::Int(10)]
+ ),
+ selection([1, 2, 3, 4])
+ );
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::IsNull, &[]),
+ selection([5, 6])
+ );
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::IsNotNull, &[]),
+ selection([0, 1, 2, 3, 4, 7])
+ );
+ assert_eq!(
+ evaluate(
+ &reader,
+ PredicateOperator::Between,
+ &[Datum::Int(3), Datum::Int(7)]
+ ),
+ selection([1, 2, 3])
+ );
+ }
+
+ #[test]
+ fn test_java_string_v1_variable_dictionary() {
+ let data_type =
DataType::VarChar(crate::spec::VarCharType::new(32).unwrap());
+ let reader = RangeBitmapFileIndexReader::try_new(
+ data_type.clone(),
+ Bytes::from(hex::decode(JAVA_STRING_V1).unwrap()),
+ )
+ .unwrap();
+ let evaluate = |operator, literals: &[Datum]| {
+ reader.try_evaluate(&data_type, operator, literals).unwrap()
+ };
+
+ assert_eq!(
+ evaluate(PredicateOperator::Eq,
&[Datum::String("你好".to_string())]),
+ selection([2])
+ );
+ assert_eq!(
+ evaluate(PredicateOperator::Gt, &[Datum::String("b".to_string())]),
+ selection([2, 4])
+ );
+ assert_eq!(
+ evaluate(
+ PredicateOperator::Between,
+ &[
+ Datum::String("b".to_string()),
+ Datum::String("ccc".to_string())
+ ]
+ ),
+ selection([1, 4])
+ );
+ }
+
+ #[test]
+ fn test_java_float_v1_covers_residual_zero_and_nan_semantics() {
+ let data_type = DataType::Float(crate::spec::FloatType::new());
+ let reader = RangeBitmapFileIndexReader::try_new(
+ data_type.clone(),
+ Bytes::from(hex::decode(JAVA_FLOAT_V1).unwrap()),
+ )
+ .unwrap();
+ let evaluate = |operator, literals: &[Datum]| {
+ reader.try_evaluate(&data_type, operator, literals).unwrap()
+ };
+
+ // Java canonicalizes every NaN written into the dictionary, so the
same
+ // payload can represent either NaN sign in the original rows.
+ let possible_values: [ArrayRef; 2] = [
+ Arc::new(Float32Array::from(vec![
+ Some(-0.0),
+ Some(0.0),
+ Some(1.5),
+ Some(f32::NAN),
+ None,
+ ])),
+ Arc::new(Float32Array::from(vec![
+ Some(-0.0),
+ Some(0.0),
+ Some(1.5),
+ Some(f32::from_bits(0xffc0_0000)),
+ None,
+ ])),
+ ];
+ for values in &possible_values {
+ for literal in [
+ Datum::Float(-0.0),
+ Datum::Float(0.0),
+ Datum::Float(f32::NAN),
+ Datum::Float(f32::from_bits(0xffc0_0000)),
+ ] {
+ for operator in [
+ PredicateOperator::Eq,
+ PredicateOperator::NotEq,
+ PredicateOperator::Lt,
+ PredicateOperator::LtEq,
+ PredicateOperator::Gt,
+ PredicateOperator::GtEq,
+ PredicateOperator::In,
+ PredicateOperator::NotIn,
+ ] {
+ assert_covers_residual(
+ &reader,
+ values,
+ &data_type,
+ operator,
+ std::slice::from_ref(&literal),
+ );
+ }
+ }
+ }
+ assert_eq!(
+ evaluate(PredicateOperator::NotEq, &[Datum::Float(f32::NAN)]),
+ selection([0, 1, 2, 3])
+ );
+ }
+
+ #[test]
+ fn test_finer_timestamp_literals_fail_open() {
+ let cases = [
+ (
+ DataType::Timestamp(TimestampType::new(3).unwrap()),
+ RangeValueCodec::TimestampMillis,
+ RangeValue::Timestamp(1_000),
+ Datum::Timestamp {
+ millis: 1_000,
+ nanos: 1,
+ },
+ ),
+ (
+ DataType::Timestamp(TimestampType::new(6).unwrap()),
+ RangeValueCodec::TimestampMicros,
+ RangeValue::Timestamp(1_000_000),
+ Datum::Timestamp {
+ millis: 1_000,
+ nanos: 1,
+ },
+ ),
+ (
+
DataType::LocalZonedTimestamp(LocalZonedTimestampType::new(3).unwrap()),
+ RangeValueCodec::LocalZonedTimestampMillis,
+ RangeValue::LocalZonedTimestamp(1_000),
+ Datum::LocalZonedTimestamp {
+ millis: 1_000,
+ nanos: 1,
+ },
+ ),
+ (
+
DataType::LocalZonedTimestamp(LocalZonedTimestampType::new(6).unwrap()),
+ RangeValueCodec::LocalZonedTimestampMicros,
+ RangeValue::LocalZonedTimestamp(1_000_000),
+ Datum::LocalZonedTimestamp {
+ millis: 1_000,
+ nanos: 1,
+ },
+ ),
+ ];
+
+ for (data_type, codec, value, literal) in cases {
+ let reader = single_value_reader(codec, value);
+ for operator in [
+ PredicateOperator::Lt,
+ PredicateOperator::NotEq,
+ PredicateOperator::NotIn,
+ ] {
+ assert_eq!(
+ reader.evaluate(
+ "ts",
+ 0,
+ &data_type,
+ operator,
+ std::slice::from_ref(&literal)
+ ),
+ FileIndexResult::Remain
+ );
+ }
+ }
+ }
+
+ #[test]
+ fn test_exact_timestamp_literals_still_use_index() {
+ let millis_reader = single_value_reader(
+ RangeValueCodec::TimestampMillis,
+ RangeValue::Timestamp(1_000),
+ );
+ assert_eq!(
+ millis_reader.evaluate(
+ "ts",
+ 0,
+ &DataType::Timestamp(TimestampType::new(3).unwrap()),
+ PredicateOperator::Eq,
+ &[Datum::Timestamp {
+ millis: 1_000,
+ nanos: 0,
+ }],
+ ),
+ selection([0])
+ );
+
+ let micros_reader = single_value_reader(
+ RangeValueCodec::TimestampMicros,
+ RangeValue::Timestamp(1_000_001),
+ );
+ assert_eq!(
+ micros_reader.evaluate(
+ "ts",
+ 0,
+ &DataType::Timestamp(TimestampType::new(6).unwrap()),
+ PredicateOperator::Eq,
+ &[Datum::Timestamp {
+ millis: 1_000,
+ nanos: 1_000,
+ }],
+ ),
+ selection([0])
+ );
+ }
+
+ #[test]
+ fn test_all_null_index_keeps_every_null_row() {
+ let reader = reader(java_all_null_int_index(5));
+
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::IsNull, &[]),
+ selection(0..5)
+ );
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::IsNotNull, &[]),
+ selection([])
+ );
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::Eq, &[Datum::Int(1)]),
+ selection([])
+ );
+ }
+
+ #[test]
+ fn test_malformed_bsi_is_rejected() {
+ let mut bytes = hex::decode(JAVA_INT_V1).unwrap();
+ bytes.pop();
+ assert!(matches!(
+ RangeBitmapFileIndexReader::try_new(int_type(),
Bytes::from(bytes)),
+ Err(Error::FileIndexFormatInvalid { .. })
+ ));
+ }
+
+ #[test]
+ fn test_bsi_slice_width_must_match_dictionary_cardinality() {
+ let bytes = int_index_with_bsi_slices(&[1, 2, 3], &[0, 1, 2], &[&[1]]);
+ assert!(matches!(
+ RangeBitmapFileIndexReader::try_new(int_type(), bytes),
+ Err(Error::FileIndexFormatInvalid { .. })
+ ));
+ }
+
+ #[test]
+ fn test_bsi_codes_must_fit_dictionary_cardinality() {
+ // Row 2 has both bits set, encoding code 3 for a three-value
+ // dictionary whose valid codes are 0, 1, and 2.
+ let bytes = int_index_with_bsi_slices(&[1, 2, 3], &[0, 1, 2], &[&[1,
2], &[2]]);
+ assert!(matches!(
+ RangeBitmapFileIndexReader::try_new(int_type(), bytes),
+ Err(Error::FileIndexFormatInvalid { .. })
+ ));
+ }
+
+ #[test]
+ fn test_single_value_dictionary_keeps_one_slice() {
+ let bytes = int_index_with_bsi_slices(&[7], &[0, 1, 2], &[&[]]);
+ let reader = reader(bytes);
+ assert_eq!(
+ evaluate(&reader, PredicateOperator::Eq, &[Datum::Int(7)]),
+ selection([0, 1, 2])
+ );
+ }
+}