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 7017499f feat(file_index): add Java-compatible Bitmap reader (#754)
7017499f is described below

commit 7017499f4fd238e7c26541ff3ac491dbbe1d492a
Author: QuakeWang <[email protected]>
AuthorDate: Thu Aug 27 15:44:22 2026 +0800

    feat(file_index): add Java-compatible Bitmap reader (#754)
---
 crates/paimon/src/file_index/bitmap/mod.rs | 1789 ++++++++++++++++++++++++++++
 crates/paimon/src/file_index/mod.rs        |    4 +-
 2 files changed, 1792 insertions(+), 1 deletion(-)

diff --git a/crates/paimon/src/file_index/bitmap/mod.rs 
b/crates/paimon/src/file_index/bitmap/mod.rs
new file mode 100644
index 00000000..9a351c4b
--- /dev/null
+++ b/crates/paimon/src/file_index/bitmap/mod.rs
@@ -0,0 +1,1789 @@
+// 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.
+
+use std::cmp::Ordering;
+use std::collections::HashMap;
+use std::io::Cursor;
+use std::ops::Range;
+
+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 VERSION_2: u8 = 2;
+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)]
+enum BitmapValueCodec {
+    Boolean,
+    TinyInt,
+    SmallInt,
+    Int,
+    BigInt,
+    Float,
+    Double,
+    Date,
+    Time,
+    TimestampMillis,
+    TimestampMicros,
+    LocalZonedTimestampMillis,
+    LocalZonedTimestampMicros,
+    String,
+}
+
+impl BitmapValueCodec {
+    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::Date(_) => Ok(Self::Date),
+            DataType::Time(_) => Ok(Self::Time),
+            DataType::Timestamp(timestamp) if timestamp.precision() <= 3 => {
+                Ok(Self::TimestampMillis)
+            }
+            DataType::Timestamp(_) => Ok(Self::TimestampMicros),
+            DataType::LocalZonedTimestamp(timestamp) if timestamp.precision() 
<= 3 => {
+                Ok(Self::LocalZonedTimestampMillis)
+            }
+            DataType::LocalZonedTimestamp(_) => 
Ok(Self::LocalZonedTimestampMicros),
+            DataType::Char(_) | DataType::VarChar(_) => Ok(Self::String),
+            _ => Err(Error::Unsupported {
+                message: format!("Bitmap file index does not support data type 
{data_type:?}"),
+            }),
+        }
+    }
+
+    fn read_value(self, input: &mut Decoder<'_>) -> Result<BitmapValue> {
+        Ok(match self {
+            Self::Boolean => BitmapValue::Boolean(input.read_bool("boolean 
value")?),
+            Self::TinyInt => BitmapValue::TinyInt(input.read_i8("tinyint 
value")?),
+            Self::SmallInt => BitmapValue::SmallInt(input.read_i16("smallint 
value")?),
+            Self::Int => BitmapValue::Int(input.read_i32("int value")?),
+            Self::BigInt => BitmapValue::BigInt(input.read_i64("bigint 
value")?),
+            Self::Float => 
BitmapValue::Float(JavaFloat::new(input.read_f32("float value")?)),
+            Self::Double => 
BitmapValue::Double(JavaDouble::new(input.read_f64("double value")?)),
+            Self::Date => BitmapValue::Date(input.read_i32("date value")?),
+            Self::Time => BitmapValue::Time(input.read_i32("time value")?),
+            Self::TimestampMillis | Self::TimestampMicros => {
+                BitmapValue::Timestamp(input.read_i64("timestamp value")?)
+            }
+            Self::LocalZonedTimestampMillis | Self::LocalZonedTimestampMicros 
=> {
+                BitmapValue::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")?;
+                let value = std::str::from_utf8(bytes)
+                    .map_err(|error| {
+                        format_invalid(format!("invalid UTF-8 bitmap string 
value: {error}"))
+                    })?
+                    .to_string();
+                BitmapValue::String(value)
+            }
+        })
+    }
+
+    fn value(self, datum: &Datum) -> Result<BitmapValue> {
+        match (self, datum) {
+            (Self::Boolean, Datum::Bool(value)) => 
Ok(BitmapValue::Boolean(*value)),
+            (Self::TinyInt, Datum::TinyInt(value)) => 
Ok(BitmapValue::TinyInt(*value)),
+            (Self::SmallInt, Datum::SmallInt(value)) => 
Ok(BitmapValue::SmallInt(*value)),
+            (Self::Int, Datum::Int(value)) => Ok(BitmapValue::Int(*value)),
+            (Self::BigInt, Datum::Long(value)) => 
Ok(BitmapValue::BigInt(*value)),
+            (Self::Float, Datum::Float(value)) => 
Ok(BitmapValue::Float(JavaFloat::new(*value))),
+            (Self::Double, Datum::Double(value)) => {
+                Ok(BitmapValue::Double(JavaDouble::new(*value)))
+            }
+            (Self::Date, Datum::Date(value)) => Ok(BitmapValue::Date(*value)),
+            (Self::Time, Datum::Time(value)) => Ok(BitmapValue::Time(*value)),
+            (Self::TimestampMillis, Datum::Timestamp { millis, nanos }) => {
+                validate_nanos(*nanos)?;
+                Ok(BitmapValue::Timestamp(*millis))
+            }
+            (Self::TimestampMicros, Datum::Timestamp { millis, nanos }) => {
+                Ok(BitmapValue::Timestamp(timestamp_micros(*millis, *nanos)?))
+            }
+            (Self::LocalZonedTimestampMillis, Datum::LocalZonedTimestamp { 
millis, nanos }) => {
+                validate_nanos(*nanos)?;
+                Ok(BitmapValue::LocalZonedTimestamp(*millis))
+            }
+            (Self::LocalZonedTimestampMicros, Datum::LocalZonedTimestamp { 
millis, nanos }) => Ok(
+                BitmapValue::LocalZonedTimestamp(timestamp_micros(*millis, 
*nanos)?),
+            ),
+            (Self::String, Datum::String(value)) => 
Ok(BitmapValue::String(value.clone())),
+            _ => Err(Error::DataInvalid {
+                message: format!("Datum {datum:?} does not match Bitmap codec 
{self:?}"),
+                source: None,
+            }),
+        }
+    }
+}
+
+#[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)
+    }
+
+    fn opposite_zero(self) -> Option<Self> {
+        match self.0 {
+            0 => Some(Self(0x8000_0000)),
+            0x8000_0000 => Some(Self(0)),
+            _ => None,
+        }
+    }
+}
+
+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)
+    }
+
+    fn opposite_zero(self) -> Option<Self> {
+        match self.0 {
+            0 => Some(Self(0x8000_0000_0000_0000)),
+            0x8000_0000_0000_0000 => Some(Self(0)),
+            _ => None,
+        }
+    }
+}
+
+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())
+    }
+}
+
+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,
+        })
+    }
+}
+
+fn timestamp_micros(millis: i64, nanos: i32) -> Result<i64> {
+    validate_nanos(nanos)?;
+    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,
+        })
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+enum BitmapValue {
+    Boolean(bool),
+    TinyInt(i8),
+    SmallInt(i16),
+    Int(i32),
+    BigInt(i64),
+    Float(JavaFloat),
+    Double(JavaDouble),
+    Date(i32),
+    Time(i32),
+    Timestamp(i64),
+    LocalZonedTimestamp(i64),
+    String(String),
+}
+
+impl BitmapValue {
+    fn equivalent_zero(&self) -> Option<Self> {
+        match self {
+            Self::Float(value) => value.opposite_zero().map(Self::Float),
+            Self::Double(value) => value.opposite_zero().map(Self::Double),
+            _ => None,
+        }
+    }
+
+    fn is_nan(&self) -> bool {
+        match self {
+            Self::Float(value) => value.value().is_nan(),
+            Self::Double(value) => value.value().is_nan(),
+            _ => false,
+        }
+    }
+}
+
+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_bool(&mut self, field: &str) -> Result<bool> {
+        Ok(self.read_u8(field)? != 0)
+    }
+
+    fn read_i8(&mut self, field: &str) -> Result<i8> {
+        Ok(self.read_u8(field)? as i8)
+    }
+
+    fn read_i16(&mut self, field: &str) -> Result<i16> {
+        let bytes = self.read_exact(2, field)?;
+        Ok(i16::from_be_bytes([bytes[0], bytes[1]]))
+    }
+
+    fn read_i32(&mut self, field: &str) -> Result<i32> {
+        let bytes = self.read_exact(4, field)?;
+        Ok(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
+    }
+
+    fn read_i64(&mut self, field: &str) -> Result<i64> {
+        let bytes = self.read_exact(8, field)?;
+        Ok(i64::from_be_bytes([
+            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], 
bytes[6], bytes[7],
+        ]))
+    }
+
+    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}")))
+    }
+}
+
+#[derive(Debug)]
+struct RawEntry {
+    key: Option<BitmapValue>,
+    offset: i32,
+    length: Option<i32>,
+}
+
+#[derive(Debug)]
+struct BitmapEntry {
+    key: Option<BitmapValue>,
+    location: BitmapLocation,
+}
+
+#[derive(Clone, Debug)]
+enum BitmapLocation {
+    Singleton(u32),
+    Serialized(Range<usize>),
+}
+
+#[derive(Debug)]
+struct V1Index {
+    null_entry: Option<BitmapLocation>,
+    entries: HashMap<BitmapValue, BitmapLocation>,
+}
+
+#[derive(Debug)]
+struct V2Block {
+    first_key: BitmapValue,
+    range: Range<usize>,
+}
+
+#[derive(Debug)]
+struct V2Index {
+    non_null_count: usize,
+    null_entry: Option<BitmapLocation>,
+    blocks: Vec<V2Block>,
+    body_start: usize,
+}
+
+#[derive(Debug)]
+enum BitmapIndex {
+    V1(V1Index),
+    V2(V2Index),
+}
+
+fn read_row_count(input: &mut Decoder<'_>) -> Result<u32> {
+    let row_count = input.read_i32("row count")?;
+    u32::try_from(row_count).map_err(|_| format_invalid(format!("negative row 
count: {row_count}")))
+}
+
+fn validate_entry_count(row_count: u32, non_null_count: usize, has_null: bool) 
-> Result<()> {
+    let distinct_count = non_null_count
+        .checked_add(usize::from(has_null))
+        .ok_or_else(|| format_invalid("bitmap entry count overflow"))?;
+    if distinct_count > row_count as usize {
+        return Err(format_invalid(format!(
+            "bitmap entry count {distinct_count} exceeds row count {row_count}"
+        )));
+    }
+    Ok(())
+}
+
+fn singleton_position(offset: i32, row_count: u32) -> Result<u32> {
+    let position = u32::try_from(-1_i64 - i64::from(offset))
+        .map_err(|_| format_invalid(format!("invalid singleton offset: 
{offset}")))?;
+    if position >= row_count {
+        return Err(format_invalid(format!(
+            "singleton row position {position} exceeds row count {row_count}"
+        )));
+    }
+    Ok(position)
+}
+
+fn parse_v1(
+    serialized: &[u8],
+    input: &mut Decoder<'_>,
+    codec: BitmapValueCodec,
+) -> Result<(u32, Vec<BitmapEntry>)> {
+    let row_count = read_row_count(input)?;
+    let non_null_count = input.read_count("non-null bitmap count")?;
+    let has_null = input.read_bool("has-null flag")?;
+    validate_entry_count(row_count, non_null_count, has_null)?;
+
+    let mut raw_entries = Vec::new();
+    if has_null {
+        raw_entries.push(RawEntry {
+            key: None,
+            offset: input.read_i32("null bitmap offset")?,
+            length: None,
+        });
+    }
+    for index in 0..non_null_count {
+        raw_entries.push(RawEntry {
+            key: Some(codec.read_value(input)?),
+            offset: input.read_i32(&format!("bitmap entry {index} offset"))?,
+            length: None,
+        });
+    }
+
+    let body_start = input.position();
+    let body_length = serialized.len() - body_start;
+    let positive_offsets = raw_entries
+        .iter()
+        .filter(|entry| entry.offset >= 0)
+        .map(|entry| entry.offset as usize)
+        .collect::<Vec<_>>();
+
+    if let Some((first, rest)) = positive_offsets.split_first() {
+        if *first != 0 {
+            return Err(format_invalid(format!(
+                "first V1 bitmap offset must be 0, but was {first}"
+            )));
+        }
+        for offsets in positive_offsets.windows(2) {
+            if offsets[0] >= offsets[1] {
+                return Err(format_invalid(format!(
+                    "V1 bitmap offsets must increase, but found {} then {}",
+                    offsets[0], offsets[1]
+                )));
+            }
+        }
+        let last = rest.last().unwrap_or(first);
+        if *last >= body_length {
+            return Err(format_invalid(format!(
+                "V1 bitmap offset {} exceeds body length {body_length}",
+                last
+            )));
+        }
+    } else if body_length != 0 {
+        return Err(format_invalid(format!(
+            "V1 bitmap body has {body_length} bytes without a serialized 
bitmap"
+        )));
+    }
+
+    let mut positive_index = 0;
+    let mut entries = Vec::with_capacity(raw_entries.len());
+    for entry in raw_entries {
+        let location = if entry.offset < 0 {
+            BitmapLocation::Singleton(singleton_position(entry.offset, 
row_count)?)
+        } else {
+            let start = positive_offsets[positive_index];
+            let end = positive_offsets
+                .get(positive_index + 1)
+                .copied()
+                .unwrap_or(body_length);
+            positive_index += 1;
+            BitmapLocation::Serialized(body_start + start..body_start + end)
+        };
+        entries.push(BitmapEntry {
+            key: entry.key,
+            location,
+        });
+    }
+
+    Ok((row_count, entries))
+}
+
+fn parse_v2(
+    serialized: &[u8],
+    input: &mut Decoder<'_>,
+    codec: BitmapValueCodec,
+) -> Result<(u32, V2Index)> {
+    let row_count = read_row_count(input)?;
+    let non_null_count = input.read_count("non-null bitmap count")?;
+    let has_null = input.read_bool("has-null flag")?;
+    validate_entry_count(row_count, non_null_count, has_null)?;
+
+    let raw_null_entry = if has_null {
+        Some(RawEntry {
+            key: None,
+            offset: input.read_i32("null bitmap offset")?,
+            length: Some(input.read_i32("null bitmap length")?),
+        })
+    } else {
+        None
+    };
+
+    let block_count = input.read_count("bitmap index block count")?;
+    if block_count > non_null_count || (block_count == 0) != (non_null_count 
== 0) {
+        return Err(format_invalid(format!(
+            "bitmap index block count {block_count} is inconsistent with 
{non_null_count} entries"
+        )));
+    }
+
+    let mut blocks = Vec::new();
+    for index in 0..block_count {
+        let key = codec.read_value(input)?;
+        if blocks
+            .last()
+            .is_some_and(|(previous, _): &(BitmapValue, usize)| previous >= 
&key)
+        {
+            return Err(format_invalid("V2 bitmap index block keys are not 
sorted"));
+        }
+        let offset = input.read_i32(&format!("bitmap index block {index} 
offset"))?;
+        let offset = usize::try_from(offset)
+            .map_err(|_| format_invalid(format!("negative bitmap index block 
offset: {offset}")))?;
+        blocks.push((key, offset));
+    }
+
+    let bitmap_body_offset = input.read_i32("bitmap body offset")?;
+    let bitmap_body_offset = usize::try_from(bitmap_body_offset).map_err(|_| {
+        format_invalid(format!("negative bitmap body offset: 
{bitmap_body_offset}"))
+    })?;
+    let index_block_start = input.position();
+    let body_start = index_block_start
+        .checked_add(bitmap_body_offset)
+        .ok_or_else(|| format_invalid("bitmap body start overflow"))?;
+    if body_start > serialized.len() {
+        return Err(format_invalid(format!(
+            "bitmap body start {body_start} exceeds payload length {}",
+            serialized.len()
+        )));
+    }
+
+    let mut index_blocks = Vec::with_capacity(blocks.len());
+    let mut previous_offset = None;
+    let mut blocks = blocks.into_iter().enumerate().peekable();
+    while let Some((block_index, (first_key, offset))) = blocks.next() {
+        if block_index == 0 && offset != 0 {
+            return Err(format_invalid(format!(
+                "first bitmap index block offset must be 0, but was {offset}"
+            )));
+        }
+        if let Some(previous_offset) = previous_offset {
+            if offset <= previous_offset {
+                return Err(format_invalid(format!(
+                    "bitmap index block offsets must increase, but found 
{previous_offset} then {offset}"
+                )));
+            }
+        }
+        previous_offset = Some(offset);
+        let end = blocks
+            .peek()
+            .map(|(_, (_, next_offset))| *next_offset)
+            .unwrap_or(bitmap_body_offset);
+        if end > bitmap_body_offset || end.saturating_sub(offset) < 4 {
+            return Err(format_invalid(format!(
+                "bitmap index block {block_index} range {offset}..{end} is 
outside the {bitmap_body_offset}-byte index area"
+            )));
+        }
+        index_blocks.push(V2Block {
+            first_key,
+            range: index_block_start + offset..index_block_start + end,
+        });
+    }
+
+    if block_count == 0 && bitmap_body_offset != 0 {
+        return Err(format_invalid(format!(
+            "V2 bitmap index area has {bitmap_body_offset} bytes without any 
blocks"
+        )));
+    }
+
+    let null_entry = raw_null_entry
+        .map(|entry| {
+            let length = entry
+                .length
+                .ok_or_else(|| format_invalid("missing V2 null bitmap 
length"))?;
+            v2_location(
+                serialized.len(),
+                body_start,
+                row_count,
+                false,
+                entry.offset,
+                length,
+            )
+        })
+        .transpose()?;
+
+    if non_null_count == 0 {
+        let expected_end = match &null_entry {
+            Some(BitmapLocation::Serialized(range)) if range.start == 
body_start => range.end,
+            Some(BitmapLocation::Serialized(range)) => {
+                return Err(format_invalid(format!(
+                    "null bitmap starts at {}, expected {body_start}",
+                    range.start
+                )))
+            }
+            _ => body_start,
+        };
+        if expected_end != serialized.len() {
+            return Err(format_invalid(format!(
+                "bitmap body ends at {expected_end}, but payload length is {}",
+                serialized.len()
+            )));
+        }
+    }
+
+    Ok((
+        row_count,
+        V2Index {
+            non_null_count,
+            null_entry,
+            blocks: index_blocks,
+            body_start,
+        },
+    ))
+}
+
+fn v2_location(
+    payload_length: usize,
+    body_start: usize,
+    row_count: u32,
+    non_null: bool,
+    offset: i32,
+    length: i32,
+) -> Result<BitmapLocation> {
+    if offset < 0 {
+        if (non_null && length != -1) || (!non_null && length <= 0) {
+            return Err(format_invalid(format!(
+                "singleton bitmap has invalid length {length}"
+            )));
+        }
+        return singleton_position(offset, 
row_count).map(BitmapLocation::Singleton);
+    }
+
+    let length = usize::try_from(length)
+        .map_err(|_| format_invalid(format!("negative serialized bitmap 
length: {length}")))?;
+    if length == 0 {
+        return Err(format_invalid("serialized bitmap length must be 
positive"));
+    }
+    let start = body_start
+        .checked_add(offset as usize)
+        .ok_or_else(|| format_invalid("serialized bitmap start overflow"))?;
+    let end = start
+        .checked_add(length)
+        .ok_or_else(|| format_invalid("serialized bitmap end overflow"))?;
+    if end > payload_length {
+        return Err(format_invalid(format!(
+            "serialized bitmap range {start}..{end} exceeds payload length 
{payload_length}"
+        )));
+    }
+    Ok(BitmapLocation::Serialized(start..end))
+}
+
+impl V1Index {
+    fn try_new(entries: Vec<BitmapEntry>) -> Result<Self> {
+        let mut null_entry = None;
+        let mut value_entries = HashMap::new();
+        for entry in entries {
+            match entry.key {
+                None if null_entry.is_some() => {
+                    return Err(format_invalid("duplicate null bitmap entry"));
+                }
+                None => null_entry = Some(entry.location),
+                Some(key) => {
+                    if value_entries.insert(key, entry.location).is_some() {
+                        return Err(format_invalid("duplicate bitmap value"));
+                    }
+                }
+            }
+        }
+        Ok(Self {
+            null_entry,
+            entries: value_entries,
+        })
+    }
+}
+
+impl V2Index {
+    fn find_location(
+        &self,
+        serialized: &[u8],
+        codec: BitmapValueCodec,
+        row_count: u32,
+        value: &BitmapValue,
+    ) -> Result<Option<BitmapLocation>> {
+        let Some(block_index) = self
+            .blocks
+            .partition_point(|block| block.first_key.cmp(value).is_le())
+            .checked_sub(1)
+        else {
+            return Ok(None);
+        };
+        let block = &self.blocks[block_index];
+        let mut input = Decoder::new(&serialized[block.range.clone()]);
+        let entry_count = input.read_count("bitmap index block entry count")?;
+        if entry_count == 0 || entry_count > self.non_null_count {
+            return Err(format_invalid(format!(
+                "invalid bitmap index block entry count: {entry_count}"
+            )));
+        }
+
+        let mut previous_key = None;
+        let mut matched = None;
+        for entry_index in 0..entry_count {
+            let key = codec.read_value(&mut input)?;
+            if entry_index == 0 && key != block.first_key {
+                return Err(format_invalid(format!(
+                    "bitmap index block {block_index} first key does not match 
its secondary index"
+                )));
+            }
+            if previous_key
+                .as_ref()
+                .is_some_and(|previous| previous >= &key)
+            {
+                return Err(format_invalid(format!(
+                    "bitmap index block {block_index} keys are not sorted"
+                )));
+            }
+            previous_key = Some(key.clone());
+
+            let location = v2_location(
+                serialized.len(),
+                self.body_start,
+                row_count,
+                true,
+                input.read_i32("bitmap offset")?,
+                input.read_i32("bitmap length")?,
+            )?;
+            if &key == value {
+                matched = Some(location);
+            }
+        }
+        if input.remaining() != 0 {
+            return Err(format_invalid(format!(
+                "bitmap index block {block_index} has {} trailing bytes",
+                input.remaining()
+            )));
+        }
+        Ok(matched)
+    }
+}
+
+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)
+}
+
+/// Reader for the supported Java Paimon Bitmap V1 and V2 payload types.
+pub(crate) struct BitmapFileIndexReader {
+    codec: BitmapValueCodec,
+    row_count: u32,
+    serialized: Bytes,
+    index: BitmapIndex,
+}
+
+impl BitmapFileIndexReader {
+    pub(crate) fn try_new(data_type: DataType, serialized: Bytes) -> 
Result<Self> {
+        let codec = BitmapValueCodec::try_new(&data_type)?;
+        let mut input = Decoder::new(&serialized);
+        let version = input.read_u8("Bitmap version")?;
+        let (row_count, index) = match version {
+            VERSION_1 => {
+                let (row_count, entries) = parse_v1(&serialized, &mut input, 
codec)?;
+                (row_count, BitmapIndex::V1(V1Index::try_new(entries)?))
+            }
+            VERSION_2 => {
+                let (row_count, index) = parse_v2(&serialized, &mut input, 
codec)?;
+                (row_count, BitmapIndex::V2(index))
+            }
+            _ => {
+                return Err(format_invalid(format!(
+                    "unsupported Bitmap version: {version}"
+                )))
+            }
+        };
+
+        Ok(Self {
+            codec,
+            row_count,
+            serialized,
+            index,
+        })
+    }
+
+    fn all_rows(&self) -> RoaringBitmap {
+        let mut rows = RoaringBitmap::new();
+        rows.insert_range(0..self.row_count);
+        rows
+    }
+
+    fn bitmap(&self, location: &BitmapLocation, field: &str) -> 
Result<RoaringBitmap> {
+        match location {
+            BitmapLocation::Singleton(position) => 
Ok([*position].into_iter().collect()),
+            BitmapLocation::Serialized(range) => {
+                deserialize_bitmap(&self.serialized[range.clone()], 
self.row_count, field)
+            }
+        }
+    }
+
+    fn null_bitmap(&self) -> Result<RoaringBitmap> {
+        let location = match &self.index {
+            BitmapIndex::V1(index) => index.null_entry.as_ref(),
+            BitmapIndex::V2(index) => index.null_entry.as_ref(),
+        };
+        match location {
+            Some(location) => self.bitmap(location, "null bitmap"),
+            None => Ok(RoaringBitmap::new()),
+        }
+    }
+
+    fn literals_bitmap(
+        &self,
+        literals: &[Datum],
+        skip_nan_literals: bool,
+    ) -> Result<RoaringBitmap> {
+        let mut selection = RoaringBitmap::new();
+        for literal in literals {
+            let value = self.codec.value(literal)?;
+            if skip_nan_literals && value.is_nan() {
+                continue;
+            }
+            let equivalent_zero = value.equivalent_zero();
+            for value in std::iter::once(value).chain(equivalent_zero) {
+                let location = match &self.index {
+                    BitmapIndex::V1(index) => 
index.entries.get(&value).cloned(),
+                    BitmapIndex::V2(index) => {
+                        index.find_location(&self.serialized, self.codec, 
self.row_count, &value)?
+                    }
+                };
+                if let Some(location) = location {
+                    selection |= self.bitmap(&location, "value bitmap")?;
+                }
+            }
+        }
+        Ok(selection)
+    }
+
+    fn complement(&self, excluded: &RoaringBitmap) -> RoaringBitmap {
+        let mut selection = self.all_rows();
+        selection -= excluded;
+        selection
+    }
+
+    /// Evaluates a predicate while preserving lazy format errors for direct 
callers.
+    pub(crate) fn try_evaluate(
+        &self,
+        data_type: &DataType,
+        operator: PredicateOperator,
+        literals: &[Datum],
+    ) -> Result<FileIndexResult> {
+        if BitmapValueCodec::try_new(data_type).ok() != Some(self.codec) {
+            return Ok(FileIndexResult::Remain);
+        }
+
+        let selection = match operator {
+            PredicateOperator::Eq if literals.len() == 1 => {
+                self.literals_bitmap(literals, false)?
+            }
+            PredicateOperator::In => self.literals_bitmap(literals, false)?,
+            PredicateOperator::NotEq if literals.len() == 1 => {
+                // Arrow's scalar predicate kernels treat NaN as unequal to
+                // itself. A NaN literal therefore cannot exclude any row, even
+                // though Java's bitmap dictionary has a canonical NaN key.
+                self.complement(&self.literals_bitmap(literals, true)?)
+            }
+            PredicateOperator::NotIn => 
self.complement(&self.literals_bitmap(literals, true)?),
+            PredicateOperator::IsNull if literals.is_empty() => 
self.null_bitmap()?,
+            PredicateOperator::IsNotNull if literals.is_empty() => {
+                self.complement(&self.null_bitmap()?)
+            }
+            _ => return Ok(FileIndexResult::Remain),
+        };
+        Ok(FileIndexResult::Selection(selection))
+    }
+}
+
+impl FileIndexReader for BitmapFileIndexReader {
+    fn evaluate(
+        &self,
+        _column: &str,
+        _index: usize,
+        data_type: &DataType,
+        operator: PredicateOperator,
+        literals: &[Datum],
+    ) -> FileIndexResult {
+        // The shared reader trait is intentionally infallible. A corrupt lazy
+        // section must fail open instead of pruning potentially matching rows.
+        self.try_evaluate(data_type, operator, literals)
+            .unwrap_or(FileIndexResult::Remain)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::spec::{
+        BigIntType, BooleanType, CharType, DateType, DoubleType, FloatType, 
IntType,
+        LocalZonedTimestampType, SmallIntType, TimeType, TimestampType, 
TinyIntType, VarCharType,
+    };
+
+    // Generated by Java release-2.0 BitmapFileIndex.Writer at commit 
ad720da96e7b.
+    const JAVA_BOOLEAN_V1: &str = concat!(
+        "010000000500000002010000000000fffffffe01000000143a30000001000000",
+        "0000010010000000020004003a30000001000000000001001000000000000300"
+    );
+    const JAVA_BOOLEAN_V2: &str = concat!(
+        "0200000005000000020100000000000000140000000100000000000000001600",
+        "00000200fffffffeffffffff0100000014000000143a3000000100000000000100",
+        "10000000020004003a30000001000000000001001000000000000300"
+    );
+    const JAVA_TINYINT_V1: &str = concat!(
+        "01000000050000000201000000007ffffffffe80000000143a3000000100000000",
+        "00010010000000020004003a30000001000000000001001000000000000300"
+    );
+    const JAVA_TINYINT_V2: &str = concat!(
+        "020000000500000002010000000000000014000000018000000000000000160000",
+        "00028000000014000000147ffffffffeffffffff3a300000010000000000010010",
+        "000000020004003a30000001000000000001001000000000000300"
+    );
+    const JAVA_SMALLINT_V1: &str = concat!(
+        "01000000050000000201000000005ba0fffffffecfc7000000143a300000010000",
+        "000000010010000000020004003a30000001000000000001001000000000000300"
+    );
+    const JAVA_SMALLINT_V2: &str = concat!(
+        "02000000050000000201000000000000001400000001cfc7000000000000001800",
+        "000002cfc700000014000000145ba0fffffffeffffffff3a30000001000000000001",
+        "0010000000020004003a30000001000000000001001000000000000300"
+    );
+    const JAVA_INT_V1: &str = concat!(
+        "01000000050000000201000000000000002afffffffef8a432eb000000143a3000",
+        "00010000000000010010000000020004003a300000010000000000010010000000",
+        "00000300"
+    );
+    const JAVA_INT_V2: &str = concat!(
+        "02000000050000000201000000000000001400000001f8a432eb00000000000000",
+        "1c00000002f8a432eb00000014000000140000002afffffffeffffffff3a300000",
+        "010000000000010010000000020004003a30000001000000000001001000000000",
+        "000300"
+    );
+    const JAVA_BIGINT_V1: &str = concat!(
+        "01000000050000000201000000007ffffffffffffe37fffffffe800000000000007b",
+        "000000143a300000010000000000010010000000020004003a3000000100000000",
+        "0001001000000000000300"
+    );
+    const JAVA_BIGINT_V2: &str = concat!(
+        "02000000050000000201000000000000001400000001800000000000007b000000",
+        "000000002400000002800000000000007b00000014000000147ffffffffffffe37",
+        "fffffffeffffffff3a300000010000000000010010000000020004003a30000001",
+        "000000000001001000000000000300"
+    );
+    const JAVA_FLOAT_V1: &str = concat!(
+        
"010000000a0000000401000000007fc00000000000148000000000000028000000000000",
+        
"003cbfc00000000000503a300000010000000000010010000000040009003a3000000100",
+        
"00000000010010000000030008003a300000010000000000010010000000020007003a30",
+        
"0000010000000000010010000000010006003a3000000100000000000100100000000000",
+        "0500"
+    );
+    const JAVA_FLOAT_V2: &str = concat!(
+        
"020000000a0000000401000000000000001400000004bfc0000000000000800000000000",
+        
"001000000000000000207fc00000000000300000004000000001bfc00000000000500000",
+        
"00140000000180000000000000280000001400000001000000000000003c000000140000",
+        
"00017fc0000000000014000000143a300000010000000000010010000000040009003a30",
+        
"0000010000000000010010000000030008003a3000000100000000000100100000000200",
+        
"07003a300000010000000000010010000000010006003a30000001000000000001001000",
+        "000000000500"
+    );
+    const JAVA_DOUBLE_V1: &str = concat!(
+        
"010000000a00000004010000000080000000000000000000001400000000000000000000",
+        
"00287ff80000000000000000003cbff8000000000000000000503a300000010000000000",
+        
"010010000000040009003a300000010000000000010010000000020007003a3000000100",
+        
"00000000010010000000010006003a300000010000000000010010000000030008003a30",
+        "000001000000000001001000000000000500"
+    );
+    const JAVA_DOUBLE_V2: &str = concat!(
+        
"020000000a0000000401000000000000001400000004bff8000000000000000000008000",
+        
"000000000000000000140000000000000000000000287ff80000000000000000003c0000",
+        
"005000000001bff800000000000000000050000000140000000180000000000000000000",
+        
"0014000000140000000100000000000000000000002800000014000000017ff800000000",
+        
"00000000003c000000143a300000010000000000010010000000040009003a3000000100",
+        
"00000000010010000000020007003a300000010000000000010010000000010006003a30",
+        
"0000010000000000010010000000030008003a3000000100000000000100100000000000",
+        "0500"
+    );
+    const JAVA_DATE_V1: &str = concat!(
+        "010000000500000002010000000000004e20fffffffeffffffff000000143a300000",
+        "010000000000010010000000020004003a30000001000000000001001000000000",
+        "000300"
+    );
+    const JAVA_DATE_V2: &str = concat!(
+        "02000000050000000201000000000000001400000001ffffffff000000000000001c",
+        "00000002ffffffff000000140000001400004e20fffffffeffffffff3a3000000100",
+        "00000000010010000000020004003a300000010000000000010010000000000003",
+        "00"
+    );
+    const JAVA_TIME_V1: &str = concat!(
+        "0100000005000000020100000000000000000000001405265bfffffffffe3a3000",
+        "00010000000000010010000000020004003a300000010000000000010010000000",
+        "00000300"
+    );
+    const JAVA_TIME_V2: &str = concat!(
+        "020000000500000002010000000000000014000000010000000000000000000000",
+        "1c0000000200000000000000140000001405265bfffffffffeffffffff3a300000",
+        "010000000000010010000000020004003a30000001000000000001001000000000",
+        "000300"
+    );
+    const JAVA_TIMESTAMP_MILLIS_V1: &str = concat!(
+        "01000000050000000201000000000000018bcfe5687bfffffffefffffffff8a432eb",
+        "000000143a300000010000000000010010000000020004003a3000000100000000",
+        "0001001000000000000300"
+    );
+    const JAVA_TIMESTAMP_MILLIS_V2: &str = concat!(
+        "02000000050000000201000000000000001400000001fffffffff8a432eb000000",
+        "000000002400000002fffffffff8a432eb00000014000000140000018bcfe5687b",
+        "fffffffeffffffff3a300000010000000000010010000000020004003a30000001",
+        "000000000001001000000000000300"
+    );
+    const JAVA_TIMESTAMP_MICROS_V1: &str = concat!(
+        "0100000005000000020100000000ffffffffffffffff0000001400060a2418202240",
+        "fffffffe3a300000010000000000010010000000020004003a3000000100000000",
+        "0001001000000000000300"
+    );
+    const JAVA_TIMESTAMP_MICROS_V2: &str = concat!(
+        "02000000050000000201000000000000001400000001ffffffffffffffff000000",
+        "000000002400000002ffffffffffffffff000000140000001400060a2418202240",
+        "fffffffeffffffff3a300000010000000000010010000000020004003a30000001",
+        "000000000001001000000000000300"
+    );
+    const JAVA_STRING_V1: &str = concat!(
+        "0100000005000000020100000000000000017afffffffe0000000d5061696d6f6e",
+        "2de6b4bee89299000000143a300000010000000000010010000000020004003a30",
+        "000001000000000001001000000000000300"
+    );
+    const JAVA_STRING_V2: &str = concat!(
+        "020000000500000002010000000000000014000000010000000d5061696d6f6e2d",
+        "e6b4bee89299000000000000002a000000020000000d5061696d6f6e2de6b4be",
+        "e892990000001400000014000000017afffffffeffffffff3a300000010000000000",
+        "010010000000020004003a30000001000000000001001000000000000300"
+    );
+    const JAVA_INT_MULTIBLOCK_V2: &str = concat!(
+        "020000000500000004000000000400000001000000000000000200000010000000",
+        "03000000200000000400000030000000400000000100000001fffffffeffffffff",
+        "0000000100000002fffffffcffffffff0000000100000003fffffffdffffffff0000",
+        "00010000000400000000000000143a300000010000000000010010000000000004",
+        "00"
+    );
+    const JAVA_SINGLETON_NULL_V1: &str =
+        "01000000030000000201fffffffd00000000ffffffff00000001fffffffe";
+    const JAVA_SINGLETON_NULL_V2: &str = concat!(
+        "02000000030000000201fffffffd00000012000000010000000000000000000000",
+        "1c0000000200000000ffffffffffffffff00000001fffffffeffffffff"
+    );
+    const JAVA_EMPTY_V1: &str = "01000000000000000000";
+    const JAVA_EMPTY_V2: &str = "020000000000000000000000000000000000";
+
+    struct Fixture {
+        name: &'static str,
+        data_type: DataType,
+        repeated: Datum,
+        singleton: Datum,
+        v1: &'static str,
+        v2: &'static str,
+    }
+
+    fn bytes(encoded: &str) -> Bytes {
+        Bytes::from(hex::decode(encoded).unwrap())
+    }
+
+    fn selection(rows: impl IntoIterator<Item = u32>) -> FileIndexResult {
+        FileIndexResult::Selection(rows.into_iter().collect())
+    }
+
+    fn evaluate(
+        reader: &BitmapFileIndexReader,
+        data_type: &DataType,
+        operator: PredicateOperator,
+        literals: &[Datum],
+    ) -> FileIndexResult {
+        reader.evaluate("field", 0, data_type, operator, literals)
+    }
+
+    fn fixtures() -> Vec<Fixture> {
+        vec![
+            Fixture {
+                name: "boolean",
+                data_type: DataType::Boolean(BooleanType::new()),
+                repeated: Datum::Bool(true),
+                singleton: Datum::Bool(false),
+                v1: JAVA_BOOLEAN_V1,
+                v2: JAVA_BOOLEAN_V2,
+            },
+            Fixture {
+                name: "tinyint",
+                data_type: DataType::TinyInt(TinyIntType::new()),
+                repeated: Datum::TinyInt(-128),
+                singleton: Datum::TinyInt(127),
+                v1: JAVA_TINYINT_V1,
+                v2: JAVA_TINYINT_V2,
+            },
+            Fixture {
+                name: "smallint",
+                data_type: DataType::SmallInt(SmallIntType::new()),
+                repeated: Datum::SmallInt(-12_345),
+                singleton: Datum::SmallInt(23_456),
+                v1: JAVA_SMALLINT_V1,
+                v2: JAVA_SMALLINT_V2,
+            },
+            Fixture {
+                name: "int",
+                data_type: DataType::Int(IntType::new()),
+                repeated: Datum::Int(-123_456_789),
+                singleton: Datum::Int(42),
+                v1: JAVA_INT_V1,
+                v2: JAVA_INT_V2,
+            },
+            Fixture {
+                name: "bigint",
+                data_type: DataType::BigInt(BigIntType::new()),
+                repeated: Datum::Long(i64::MIN + 123),
+                singleton: Datum::Long(i64::MAX - 456),
+                v1: JAVA_BIGINT_V1,
+                v2: JAVA_BIGINT_V2,
+            },
+            Fixture {
+                name: "date",
+                data_type: DataType::Date(DateType::new()),
+                repeated: Datum::Date(-1),
+                singleton: Datum::Date(20_000),
+                v1: JAVA_DATE_V1,
+                v2: JAVA_DATE_V2,
+            },
+            Fixture {
+                name: "time",
+                data_type: DataType::Time(TimeType::new(3).unwrap()),
+                repeated: Datum::Time(0),
+                singleton: Datum::Time(86_399_999),
+                v1: JAVA_TIME_V1,
+                v2: JAVA_TIME_V2,
+            },
+            Fixture {
+                name: "timestamp_millis",
+                data_type: DataType::Timestamp(TimestampType::new(3).unwrap()),
+                repeated: Datum::Timestamp {
+                    millis: -123_456_789,
+                    nanos: 0,
+                },
+                singleton: Datum::Timestamp {
+                    millis: 1_700_000_000_123,
+                    nanos: 0,
+                },
+                v1: JAVA_TIMESTAMP_MILLIS_V1,
+                v2: JAVA_TIMESTAMP_MILLIS_V2,
+            },
+            Fixture {
+                name: "timestamp_micros",
+                data_type: DataType::Timestamp(TimestampType::new(6).unwrap()),
+                repeated: Datum::Timestamp {
+                    millis: -1,
+                    nanos: 999_000,
+                },
+                singleton: Datum::Timestamp {
+                    millis: 1_700_000_000_123,
+                    nanos: 456_000,
+                },
+                v1: JAVA_TIMESTAMP_MICROS_V1,
+                v2: JAVA_TIMESTAMP_MICROS_V2,
+            },
+            Fixture {
+                name: "local_zoned_timestamp_millis",
+                data_type: 
DataType::LocalZonedTimestamp(LocalZonedTimestampType::new(3).unwrap()),
+                repeated: Datum::LocalZonedTimestamp {
+                    millis: -123_456_789,
+                    nanos: 0,
+                },
+                singleton: Datum::LocalZonedTimestamp {
+                    millis: 1_700_000_000_123,
+                    nanos: 0,
+                },
+                v1: JAVA_TIMESTAMP_MILLIS_V1,
+                v2: JAVA_TIMESTAMP_MILLIS_V2,
+            },
+            Fixture {
+                name: "local_zoned_timestamp_micros",
+                data_type: 
DataType::LocalZonedTimestamp(LocalZonedTimestampType::new(6).unwrap()),
+                repeated: Datum::LocalZonedTimestamp {
+                    millis: -1,
+                    nanos: 999_000,
+                },
+                singleton: Datum::LocalZonedTimestamp {
+                    millis: 1_700_000_000_123,
+                    nanos: 456_000,
+                },
+                v1: JAVA_TIMESTAMP_MICROS_V1,
+                v2: JAVA_TIMESTAMP_MICROS_V2,
+            },
+            Fixture {
+                name: "char",
+                data_type: DataType::Char(CharType::new(20).unwrap()),
+                repeated: Datum::String("Paimon-\u{6d3e}\u{8499}".to_string()),
+                singleton: Datum::String("z".to_string()),
+                v1: JAVA_STRING_V1,
+                v2: JAVA_STRING_V2,
+            },
+            Fixture {
+                name: "varchar",
+                data_type: DataType::VarChar(VarCharType::new(20).unwrap()),
+                repeated: Datum::String("Paimon-\u{6d3e}\u{8499}".to_string()),
+                singleton: Datum::String("z".to_string()),
+                v1: JAVA_STRING_V1,
+                v2: JAVA_STRING_V2,
+            },
+        ]
+    }
+
+    #[test]
+    fn test_java_v1_v2_golden_payloads_and_predicates() {
+        for fixture in fixtures() {
+            let v1 = BitmapFileIndexReader::try_new(fixture.data_type.clone(), 
bytes(fixture.v1))
+                .unwrap_or_else(|error| panic!("{} V1 failed: {error}", 
fixture.name));
+            let v2 = BitmapFileIndexReader::try_new(fixture.data_type.clone(), 
bytes(fixture.v2))
+                .unwrap_or_else(|error| panic!("{} V2 failed: {error}", 
fixture.name));
+
+            let cases = [
+                (
+                    PredicateOperator::Eq,
+                    vec![fixture.repeated.clone()],
+                    selection([0, 3]),
+                ),
+                (
+                    PredicateOperator::Eq,
+                    vec![fixture.singleton.clone()],
+                    selection([1]),
+                ),
+                (
+                    PredicateOperator::In,
+                    vec![fixture.repeated.clone(), fixture.singleton.clone()],
+                    selection([0, 1, 3]),
+                ),
+                (
+                    PredicateOperator::NotEq,
+                    vec![fixture.repeated.clone()],
+                    selection([1, 2, 4]),
+                ),
+                (
+                    PredicateOperator::NotIn,
+                    vec![fixture.repeated.clone(), fixture.singleton.clone()],
+                    selection([2, 4]),
+                ),
+                (PredicateOperator::IsNull, vec![], selection([2, 4])),
+                (PredicateOperator::IsNotNull, vec![], selection([0, 1, 3])),
+            ];
+
+            for (operator, literals, expected) in cases {
+                let v1_result = evaluate(&v1, &fixture.data_type, operator, 
&literals);
+                let v2_result = evaluate(&v2, &fixture.data_type, operator, 
&literals);
+                assert_eq!(v1_result, expected, "{} V1 {operator}", 
fixture.name);
+                assert_eq!(v2_result, expected, "{} V2 {operator}", 
fixture.name);
+                assert_eq!(v1_result, v2_result, "{} {operator}", 
fixture.name);
+            }
+        }
+    }
+
+    #[test]
+    fn test_java_floating_v1_v2_golden_payloads_and_predicates() {
+        let fixtures = [
+            (
+                "float",
+                DataType::Float(FloatType::new()),
+                JAVA_FLOAT_V1,
+                JAVA_FLOAT_V2,
+                Datum::Float(-1.5),
+                Datum::Float(0.0),
+                Datum::Float(-0.0),
+                Datum::Float(f32::from_bits(0xffa1_2345)),
+            ),
+            (
+                "double",
+                DataType::Double(DoubleType::new()),
+                JAVA_DOUBLE_V1,
+                JAVA_DOUBLE_V2,
+                Datum::Double(-1.5),
+                Datum::Double(0.0),
+                Datum::Double(-0.0),
+                Datum::Double(f64::from_bits(0xfff0_1234_5678_9abc)),
+            ),
+        ];
+
+        for (name, data_type, v1, v2, normal, positive_zero, negative_zero, 
nan) in fixtures {
+            let v1 = BitmapFileIndexReader::try_new(data_type.clone(), 
bytes(v1))
+                .unwrap_or_else(|error| panic!("{name} V1 failed: {error}"));
+            let v2 = BitmapFileIndexReader::try_new(data_type.clone(), 
bytes(v2))
+                .unwrap_or_else(|error| panic!("{name} V2 failed: {error}"));
+            let cases = [
+                (
+                    PredicateOperator::Eq,
+                    vec![normal.clone()],
+                    selection([0, 5]),
+                ),
+                (
+                    PredicateOperator::Eq,
+                    vec![positive_zero.clone()],
+                    selection([1, 2, 6, 7]),
+                ),
+                (
+                    PredicateOperator::Eq,
+                    vec![negative_zero.clone()],
+                    selection([1, 2, 6, 7]),
+                ),
+                (PredicateOperator::Eq, vec![nan.clone()], selection([3, 8])),
+                (
+                    PredicateOperator::In,
+                    vec![normal.clone(), positive_zero.clone(), nan.clone()],
+                    selection([0, 1, 2, 3, 5, 6, 7, 8]),
+                ),
+                (
+                    PredicateOperator::NotEq,
+                    vec![positive_zero.clone()],
+                    selection([0, 3, 4, 5, 8, 9]),
+                ),
+                (
+                    PredicateOperator::NotEq,
+                    vec![nan.clone()],
+                    selection(0..10),
+                ),
+                (
+                    PredicateOperator::NotIn,
+                    vec![normal.clone(), nan.clone()],
+                    selection([1, 2, 3, 4, 6, 7, 8, 9]),
+                ),
+                (PredicateOperator::IsNull, vec![], selection([4, 9])),
+                (
+                    PredicateOperator::IsNotNull,
+                    vec![],
+                    selection([0, 1, 2, 3, 5, 6, 7, 8]),
+                ),
+            ];
+
+            for (operator, literals, expected) in cases {
+                let v1_result = evaluate(&v1, &data_type, operator, &literals);
+                let v2_result = evaluate(&v2, &data_type, operator, &literals);
+                assert_eq!(v1_result, expected, "{name} V1 {operator}");
+                assert_eq!(v2_result, expected, "{name} V2 {operator}");
+                assert_eq!(v1_result, v2_result, "{name} {operator}");
+            }
+        }
+    }
+
+    #[test]
+    fn test_timestamp_precision_boundary_and_validation() {
+        let timestamp = Datum::Timestamp {
+            millis: -1,
+            nanos: 999_999,
+        };
+        assert_eq!(
+            
BitmapValueCodec::try_new(&DataType::Timestamp(TimestampType::new(3).unwrap()))
+                .unwrap()
+                .value(&timestamp)
+                .unwrap(),
+            BitmapValue::Timestamp(-1)
+        );
+        assert_eq!(
+            
BitmapValueCodec::try_new(&DataType::Timestamp(TimestampType::new(4).unwrap()))
+                .unwrap()
+                .value(&timestamp)
+                .unwrap(),
+            BitmapValue::Timestamp(-1)
+        );
+        assert_eq!(
+            
BitmapValueCodec::try_new(&DataType::Timestamp(TimestampType::new(9).unwrap()))
+                .unwrap()
+                .value(&Datum::Timestamp {
+                    millis: 1_700_000_000_123,
+                    nanos: 456_999,
+                })
+                .unwrap(),
+            BitmapValue::Timestamp(1_700_000_000_123_456)
+        );
+
+        for invalid in [
+            Datum::Timestamp {
+                millis: 0,
+                nanos: -1,
+            },
+            Datum::Timestamp {
+                millis: 0,
+                nanos: 1_000_000,
+            },
+            Datum::Timestamp {
+                millis: i64::MAX,
+                nanos: 0,
+            },
+        ] {
+            
assert!(BitmapValueCodec::TimestampMicros.value(&invalid).is_err());
+        }
+    }
+
+    #[test]
+    fn test_v2_multiple_index_blocks() {
+        let data_type = DataType::Int(IntType::new());
+        let reader =
+            BitmapFileIndexReader::try_new(data_type.clone(), 
bytes(JAVA_INT_MULTIBLOCK_V2))
+                .unwrap();
+
+        assert_eq!(
+            evaluate(&reader, &data_type, PredicateOperator::Eq, 
&[Datum::Int(4)]),
+            selection([0, 4])
+        );
+        assert_eq!(
+            evaluate(
+                &reader,
+                &data_type,
+                PredicateOperator::In,
+                &[Datum::Int(1), Datum::Int(2), Datum::Int(3)]
+            ),
+            selection([1, 2, 3])
+        );
+        assert_eq!(
+            evaluate(&reader, &data_type, PredicateOperator::IsNull, &[]),
+            selection([])
+        );
+        assert_eq!(
+            evaluate(&reader, &data_type, PredicateOperator::IsNotNull, &[]),
+            selection(0..5)
+        );
+    }
+
+    #[test]
+    fn test_singleton_null_and_empty_payloads() {
+        let data_type = DataType::Int(IntType::new());
+        for payload in [JAVA_SINGLETON_NULL_V1, JAVA_SINGLETON_NULL_V2] {
+            let reader = BitmapFileIndexReader::try_new(data_type.clone(), 
bytes(payload)).unwrap();
+            assert_eq!(
+                evaluate(&reader, &data_type, PredicateOperator::IsNull, &[]),
+                selection([2])
+            );
+            assert_eq!(
+                evaluate(&reader, &data_type, PredicateOperator::IsNotNull, 
&[]),
+                selection([0, 1])
+            );
+        }
+
+        for payload in [JAVA_EMPTY_V1, JAVA_EMPTY_V2] {
+            let reader = BitmapFileIndexReader::try_new(data_type.clone(), 
bytes(payload)).unwrap();
+            for (operator, literals) in [
+                (PredicateOperator::In, Vec::new()),
+                (PredicateOperator::NotIn, Vec::new()),
+                (PredicateOperator::IsNull, Vec::new()),
+                (PredicateOperator::IsNotNull, Vec::new()),
+            ] {
+                assert_eq!(
+                    evaluate(&reader, &data_type, operator, &literals),
+                    selection([])
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn test_unsupported_predicates_and_invalid_literals_remain() {
+        let data_type = DataType::Int(IntType::new());
+        let reader = BitmapFileIndexReader::try_new(data_type.clone(), 
bytes(JAVA_INT_V2)).unwrap();
+
+        for (operator, literals) in [
+            (PredicateOperator::Eq, vec![]),
+            (PredicateOperator::Eq, vec![Datum::Int(1), Datum::Int(2)]),
+            (PredicateOperator::IsNull, vec![Datum::Int(1)]),
+            (PredicateOperator::Gt, vec![Datum::Int(1)]),
+            (PredicateOperator::Eq, vec![Datum::Long(42)]),
+        ] {
+            assert_eq!(
+                evaluate(&reader, &data_type, operator, &literals),
+                FileIndexResult::Remain
+            );
+        }
+        assert_eq!(
+            evaluate(
+                &reader,
+                &DataType::BigInt(BigIntType::new()),
+                PredicateOperator::Eq,
+                &[Datum::Long(42)]
+            ),
+            FileIndexResult::Remain
+        );
+    }
+
+    fn assert_invalid(data_type: &DataType, payload: impl Into<Bytes>) {
+        assert!(matches!(
+            BitmapFileIndexReader::try_new(data_type.clone(), payload.into()),
+            Err(Error::FileIndexFormatInvalid { .. })
+        ));
+    }
+
+    fn assert_evaluation_invalid(
+        reader: &BitmapFileIndexReader,
+        data_type: &DataType,
+        operator: PredicateOperator,
+        literals: &[Datum],
+    ) {
+        assert!(matches!(
+            reader.try_evaluate(data_type, operator, literals),
+            Err(Error::FileIndexFormatInvalid { .. })
+        ));
+    }
+
+    fn overwrite_i32(bytes: &mut [u8], offset: usize, value: i32) {
+        bytes[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
+    }
+
+    fn v2_singleton_int_fixture(value_count: usize, entries_per_block: usize) 
-> Bytes {
+        assert!(value_count > 0 && entries_per_block > 0);
+        let mut blocks = Vec::new();
+        let mut secondary = Vec::new();
+        let mut block_offset = 0_i32;
+
+        for start in (0..value_count).step_by(entries_per_block) {
+            let end = (start + entries_per_block).min(value_count);
+            secondary.push((start as i32, block_offset));
+
+            let mut block = Vec::new();
+            block.extend_from_slice(&((end - start) as i32).to_be_bytes());
+            for position in start..end {
+                block.extend_from_slice(&(position as i32).to_be_bytes());
+                block.extend_from_slice(&(-1_i32 - position as 
i32).to_be_bytes());
+                block.extend_from_slice(&(-1_i32).to_be_bytes());
+            }
+            block_offset += block.len() as i32;
+            blocks.push(block);
+        }
+
+        let mut payload = vec![VERSION_2];
+        payload.extend_from_slice(&(value_count as i32).to_be_bytes());
+        payload.extend_from_slice(&(value_count as i32).to_be_bytes());
+        payload.push(0);
+        payload.extend_from_slice(&(secondary.len() as i32).to_be_bytes());
+        for (first_key, offset) in secondary {
+            payload.extend_from_slice(&first_key.to_be_bytes());
+            payload.extend_from_slice(&offset.to_be_bytes());
+        }
+        payload.extend_from_slice(&block_offset.to_be_bytes());
+        for block in blocks {
+            payload.extend_from_slice(&block);
+        }
+        Bytes::from(payload)
+    }
+
+    #[test]
+    fn test_v2_keeps_high_cardinality_dictionary_lazy() {
+        let data_type = DataType::Int(IntType::new());
+        let value_count = 10_000;
+        let entries_per_block = 128;
+        let payload = v2_singleton_int_fixture(value_count, entries_per_block);
+        let payload_ptr = payload.as_ptr();
+        let reader = BitmapFileIndexReader::try_new(data_type.clone(), 
payload.clone()).unwrap();
+        let blocks = match &reader.index {
+            BitmapIndex::V2(index) => &index.blocks,
+            BitmapIndex::V1(_) => panic!("expected V2 index"),
+        };
+        assert_eq!(reader.serialized.as_ptr(), payload_ptr);
+        assert_eq!(blocks.len(), value_count.div_ceil(entries_per_block));
+        assert_eq!(
+            evaluate(
+                &reader,
+                &data_type,
+                PredicateOperator::In,
+                &[Datum::Int(0), Datum::Int(5_000), Datum::Int(9_999)]
+            ),
+            selection([0, 5_000, 9_999])
+        );
+
+        let last_block_start = blocks.last().unwrap().range.start;
+        let mut corrupted = payload.to_vec();
+        overwrite_i32(&mut corrupted, last_block_start, 0);
+        let corrupted =
+            BitmapFileIndexReader::try_new(data_type.clone(), 
Bytes::from(corrupted)).unwrap();
+        assert_eq!(
+            evaluate(
+                &corrupted,
+                &data_type,
+                PredicateOperator::Eq,
+                &[Datum::Int(0)]
+            ),
+            selection([0])
+        );
+        assert_evaluation_invalid(
+            &corrupted,
+            &data_type,
+            PredicateOperator::Eq,
+            &[Datum::Int(9_999)],
+        );
+    }
+
+    #[test]
+    fn test_rejects_invalid_versions_and_every_truncated_prefix() {
+        let data_type = DataType::Int(IntType::new());
+        for payload in [vec![], vec![0], vec![3], vec![255]] {
+            assert_invalid(&data_type, Bytes::from(payload));
+        }
+
+        for valid in [
+            hex::decode(JAVA_INT_V1).unwrap(),
+            hex::decode(JAVA_INT_V2).unwrap(),
+        ] {
+            for length in 0..valid.len() {
+                let payload = Bytes::copy_from_slice(&valid[..length]);
+                match BitmapFileIndexReader::try_new(data_type.clone(), 
payload) {
+                    Err(Error::FileIndexFormatInvalid { .. }) => {}
+                    Err(error) => panic!("unexpected error for prefix 
{length}: {error}"),
+                    Ok(reader) => {
+                        let null_result =
+                            reader.try_evaluate(&data_type, 
PredicateOperator::IsNull, &[]);
+                        let value_result = reader.try_evaluate(
+                            &data_type,
+                            PredicateOperator::Eq,
+                            &[Datum::Int(-123_456_789)],
+                        );
+                        assert!(
+                            matches!(null_result, 
Err(Error::FileIndexFormatInvalid { .. }))
+                                || matches!(
+                                    value_result,
+                                    Err(Error::FileIndexFormatInvalid { .. })
+                                ),
+                            "truncated prefix {length} was accepted by every 
accessed entry"
+                        );
+                    }
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn test_rejects_invalid_v1_offsets_and_corrupt_bitmaps() {
+        let data_type = DataType::Int(IntType::new());
+        let valid = hex::decode(JAVA_INT_V1).unwrap();
+
+        let mut singleton_out_of_range = valid.clone();
+        overwrite_i32(&mut singleton_out_of_range, 18, -6);
+        assert_invalid(&data_type, Bytes::from(singleton_out_of_range));
+
+        let mut offset_out_of_range = valid.clone();
+        overwrite_i32(&mut offset_out_of_range, 26, 40);
+        assert_invalid(&data_type, Bytes::from(offset_out_of_range));
+
+        let mut offsets_not_increasing = valid.clone();
+        overwrite_i32(&mut offsets_not_increasing, 10, 21);
+        assert_invalid(&data_type, Bytes::from(offsets_not_increasing));
+
+        let mut corrupt_bitmap = valid.clone();
+        corrupt_bitmap[30] = 0;
+        let reader =
+            BitmapFileIndexReader::try_new(data_type.clone(), 
Bytes::from(corrupt_bitmap)).unwrap();
+        assert_eq!(
+            evaluate(
+                &reader,
+                &data_type,
+                PredicateOperator::Eq,
+                &[Datum::Int(-123_456_789)]
+            ),
+            selection([0, 3])
+        );
+        assert_evaluation_invalid(&reader, &data_type, 
PredicateOperator::IsNull, &[]);
+        assert_eq!(
+            evaluate(&reader, &data_type, PredicateOperator::IsNull, &[]),
+            FileIndexResult::Remain
+        );
+
+        let mut trailing = valid;
+        trailing.push(0);
+        let reader =
+            BitmapFileIndexReader::try_new(data_type.clone(), 
Bytes::from(trailing)).unwrap();
+        assert_evaluation_invalid(
+            &reader,
+            &data_type,
+            PredicateOperator::Eq,
+            &[Datum::Int(-123_456_789)],
+        );
+    }
+
+    #[test]
+    fn test_rejects_invalid_v2_offsets_lengths_and_corrupt_bitmaps() {
+        let data_type = DataType::Int(IntType::new());
+        let valid = hex::decode(JAVA_INT_V2).unwrap();
+
+        let mut block_offset = valid.clone();
+        overwrite_i32(&mut block_offset, 26, 1);
+        assert_invalid(&data_type, Bytes::from(block_offset));
+
+        let mut body_offset = valid.clone();
+        overwrite_i32(&mut body_offset, 30, 100);
+        assert_invalid(&data_type, Bytes::from(body_offset));
+
+        let mut bitmap_length = valid.clone();
+        overwrite_i32(&mut bitmap_length, 46, 21);
+        let reader =
+            BitmapFileIndexReader::try_new(data_type.clone(), 
Bytes::from(bitmap_length)).unwrap();
+        assert_evaluation_invalid(
+            &reader,
+            &data_type,
+            PredicateOperator::Eq,
+            &[Datum::Int(-123_456_789)],
+        );
+
+        let mut singleton_length = valid.clone();
+        overwrite_i32(&mut singleton_length, 58, 0);
+        let reader =
+            BitmapFileIndexReader::try_new(data_type.clone(), 
Bytes::from(singleton_length))
+                .unwrap();
+        assert_evaluation_invalid(
+            &reader,
+            &data_type,
+            PredicateOperator::Eq,
+            &[Datum::Int(42)],
+        );
+
+        let mut null_singleton_out_of_range = valid.clone();
+        overwrite_i32(&mut null_singleton_out_of_range, 10, -6);
+        assert_invalid(&data_type, Bytes::from(null_singleton_out_of_range));
+
+        let mut corrupt_bitmap = valid.clone();
+        corrupt_bitmap[62] = 0;
+        let reader =
+            BitmapFileIndexReader::try_new(data_type.clone(), 
Bytes::from(corrupt_bitmap)).unwrap();
+        assert_eq!(
+            evaluate(
+                &reader,
+                &data_type,
+                PredicateOperator::Eq,
+                &[Datum::Int(-123_456_789)]
+            ),
+            selection([0, 3])
+        );
+        assert_evaluation_invalid(&reader, &data_type, 
PredicateOperator::IsNull, &[]);
+    }
+}
diff --git a/crates/paimon/src/file_index/mod.rs 
b/crates/paimon/src/file_index/mod.rs
index 79eca6ea..48616abb 100644
--- a/crates/paimon/src/file_index/mod.rs
+++ b/crates/paimon/src/file_index/mod.rs
@@ -15,9 +15,11 @@
 // specific language governing permissions and limitations
 // under the License.
 
-// Bloom reader/writer and predicate plumbing stay crate-private until the
+// Concrete readers/writers and predicate plumbing stay crate-private until
 // factory, data-writer, and scan integration land in later changes.
 #[allow(dead_code)]
+pub(crate) mod bitmap;
+#[allow(dead_code)]
 pub(crate) mod bloom_filter;
 mod file_index_format;
 #[allow(dead_code)]

Reply via email to