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 17ba6745 feat(file_index): add Java-compatible Bitmap V2 writer (#759)
17ba6745 is described below
commit 17ba674594d5544e928449916be645bbecc04b89
Author: QuakeWang <[email protected]>
AuthorDate: Sun Aug 30 21:01:52 2026 +0800
feat(file_index): add Java-compatible Bitmap V2 writer (#759)
---
crates/paimon/src/file_index/bitmap/mod.rs | 345 +++++++++++++++++++++++++-
crates/paimon/src/file_index/bitmap/writer.rs | 344 +++++++++++++++++++++++++
2 files changed, 688 insertions(+), 1 deletion(-)
diff --git a/crates/paimon/src/file_index/bitmap/mod.rs
b/crates/paimon/src/file_index/bitmap/mod.rs
index 9a351c4b..105cd739 100644
--- a/crates/paimon/src/file_index/bitmap/mod.rs
+++ b/crates/paimon/src/file_index/bitmap/mod.rs
@@ -28,6 +28,8 @@ use crate::file_index::file_index_result::FileIndexResult;
use crate::spec::{DataType, Datum, PredicateOperator};
use crate::{Error, Result};
+pub(crate) mod writer;
+
const VERSION_1: u8 = 1;
const VERSION_2: u8 = 2;
const JAVA_CANONICAL_FLOAT_NAN_BITS: u32 = 0x7fc0_0000;
@@ -950,8 +952,9 @@ impl FileIndexReader for BitmapFileIndexReader {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::common::Options;
use crate::spec::{
- BigIntType, BooleanType, CharType, DateType, DoubleType, FloatType,
IntType,
+ BigIntType, BinaryType, BooleanType, CharType, DateType, DoubleType,
FloatType, IntType,
LocalZonedTimestampType, SmallIntType, TimeType, TimestampType,
TinyIntType, VarCharType,
};
@@ -1100,6 +1103,20 @@ mod tests {
"00010000000400000000000000143a300000010000000000010010000000000004",
"00"
);
+ const JAVA_INT_MULTIPLE_BODIES_MULTIBLOCK_V2: &str = concat!(
+ "020000000800000003010000000000000014000000020000000100000000000000",
+ "030000001c0000002c000000020000000100000014000000140000000200000028",
+ "0000001400000001000000030000003c000000143a300000010000000000010010",
+ "000000020005003a300000010000000000010010000000010006003a3000000100",
+ "00000000010010000000000004003a300000010000000000010010000000030007",
+ "00"
+ );
+ const JAVA_STRING_BLOCK_BOUNDARY_V2: &str = concat!(
+ "020000000400000003000000000200000001610000000000000002636300000021",
+ "0000003300000002000000016100000000000000140000000462626262ffffffff",
+ "ffffffff00000001000000026363fffffffdffffffff3a3000000100000000000100",
+ "1000000001000300"
+ );
const JAVA_SINGLETON_NULL_V1: &str =
"01000000030000000201fffffffd00000000ffffffff00000001fffffffe";
const JAVA_SINGLETON_NULL_V2: &str = concat!(
@@ -1108,6 +1125,16 @@ mod tests {
);
const JAVA_EMPTY_V1: &str = "01000000000000000000";
const JAVA_EMPTY_V2: &str = "020000000000000000000000000000000000";
+ const JAVA_FLOAT_SINGLETONS_V2: &str = concat!(
+ "02000000040000000301fffffffc00000012000000018000000000000000000000",
+ "280000000380000000fffffffdffffffff00000000fffffffeffffffff7fc00000",
+ "ffffffffffffffff"
+ );
+ const JAVA_DOUBLE_SINGLETONS_V2: &str = concat!(
+ "02000000040000000301fffffffc00000012000000018000000000000000000000",
+ "0000000034000000038000000000000000fffffffdffffffff0000000000000000",
+ "fffffffeffffffff7ff8000000000000ffffffffffffffff"
+ );
struct Fixture {
name: &'static str,
@@ -1268,6 +1295,322 @@ mod tests {
]
}
+ #[test]
+ fn test_v2_writer_matches_java_golden_payloads() {
+ for fixture in fixtures() {
+ let mut writer =
+ writer::BitmapFileIndexWriter::try_new(fixture.data_type,
&Options::new())
+ .unwrap_or_else(|error| panic!("{} writer failed:
{error}", fixture.name));
+ for datum in [
+ Some(&fixture.repeated),
+ Some(&fixture.singleton),
+ None,
+ Some(&fixture.repeated),
+ None,
+ ] {
+ writer
+ .write(datum)
+ .unwrap_or_else(|error| panic!("{} write failed: {error}",
fixture.name));
+ }
+ assert_eq!(
+ writer.serialized_bytes().unwrap(),
+ bytes(fixture.v2),
+ "{} V2 writer",
+ fixture.name
+ );
+ }
+ }
+
+ #[test]
+ fn test_v2_writer_matches_java_floating_value_encoding() {
+ let float_values = [
+ Datum::Float(f32::from_bits(0xffa1_2345)),
+ Datum::Float(0.0),
+ Datum::Float(-0.0),
+ ];
+ let mut float_writer = writer::BitmapFileIndexWriter::try_new(
+ DataType::Float(FloatType::new()),
+ &Options::new(),
+ )
+ .unwrap();
+ for value in &float_values {
+ float_writer.write(Some(value)).unwrap();
+ }
+ float_writer.write(None).unwrap();
+ assert_eq!(
+ float_writer.serialized_bytes().unwrap(),
+ bytes(JAVA_FLOAT_SINGLETONS_V2)
+ );
+
+ let double_values = [
+ Datum::Double(f64::from_bits(0xfff0_1234_5678_9abc)),
+ Datum::Double(0.0),
+ Datum::Double(-0.0),
+ ];
+ let mut double_writer = writer::BitmapFileIndexWriter::try_new(
+ DataType::Double(DoubleType::new()),
+ &Options::new(),
+ )
+ .unwrap();
+ for value in &double_values {
+ double_writer.write(Some(value)).unwrap();
+ }
+ double_writer.write(None).unwrap();
+ assert_eq!(
+ double_writer.serialized_bytes().unwrap(),
+ bytes(JAVA_DOUBLE_SINGLETONS_V2)
+ );
+ }
+
+ #[test]
+ fn test_v2_writer_round_trip_null_singleton_and_multiple_values() {
+ let data_type = DataType::Int(IntType::new());
+ let repeated = Datum::Int(-123_456_789);
+ let singleton = Datum::Int(42);
+ let mut writer =
+ writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&Options::new()).unwrap();
+ for datum in [
+ Some(&repeated),
+ Some(&singleton),
+ None,
+ Some(&repeated),
+ None,
+ ] {
+ writer.write(datum).unwrap();
+ }
+
+ let serialized = writer.serialized_bytes().unwrap();
+ assert_eq!(serialized, bytes(JAVA_INT_V2));
+ let reader = BitmapFileIndexReader::try_new(data_type.clone(),
serialized).unwrap();
+ assert!(matches!(&reader.index, BitmapIndex::V2(_)));
+ assert_eq!(
+ evaluate(&reader, &data_type, PredicateOperator::Eq, &[repeated]),
+ selection([0, 3])
+ );
+ assert_eq!(
+ evaluate(&reader, &data_type, PredicateOperator::Eq, &[singleton]),
+ selection([1])
+ );
+ assert_eq!(
+ evaluate(&reader, &data_type, PredicateOperator::IsNull, &[]),
+ selection([2, 4])
+ );
+ }
+
+ #[test]
+ fn test_v2_writer_empty_and_singleton_null_payloads() {
+ let data_type = DataType::Int(IntType::new());
+ let mut empty =
+ writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&Options::new()).unwrap();
+ assert_eq!(empty.serialized_bytes().unwrap(), bytes(JAVA_EMPTY_V2));
+
+ let mut singleton =
+ writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&Options::new()).unwrap();
+ for datum in [Some(&Datum::Int(0)), Some(&Datum::Int(1)), None] {
+ singleton.write(datum).unwrap();
+ }
+ let serialized = singleton.serialized_bytes().unwrap();
+ assert_eq!(serialized, bytes(JAVA_SINGLETON_NULL_V2));
+ let reader = BitmapFileIndexReader::try_new(data_type.clone(),
serialized).unwrap();
+ assert_eq!(
+ evaluate(&reader, &data_type, PredicateOperator::IsNull, &[]),
+ selection([2])
+ );
+
+ let mut all_null =
+ writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&Options::new()).unwrap();
+ for _ in 0..3 {
+ all_null.write(None).unwrap();
+ }
+ let reader =
+ BitmapFileIndexReader::try_new(data_type.clone(),
all_null.serialized_bytes().unwrap())
+ .unwrap();
+ assert_eq!(
+ evaluate(&reader, &data_type, PredicateOperator::IsNull, &[]),
+ selection(0..3)
+ );
+ assert_eq!(
+ evaluate(&reader, &data_type, PredicateOperator::IsNotNull, &[]),
+ selection([])
+ );
+ }
+
+ #[test]
+ fn test_v2_writer_multiple_index_blocks() {
+ let data_type = DataType::Int(IntType::new());
+ let mut options = Options::new();
+ options.set("index-block-size", "16");
+ let mut writer =
+ writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&options).unwrap();
+ for value in [4, 1, 3, 2, 4] {
+ writer.write(Some(&Datum::Int(value))).unwrap();
+ }
+
+ let serialized = writer.serialized_bytes().unwrap();
+ assert_eq!(serialized, bytes(JAVA_INT_MULTIBLOCK_V2));
+ let reader = BitmapFileIndexReader::try_new(data_type.clone(),
serialized).unwrap();
+ let blocks = match &reader.index {
+ BitmapIndex::V2(index) => &index.blocks,
+ BitmapIndex::V1(_) => panic!("expected V2 index"),
+ };
+ assert_eq!(blocks.len(), 4);
+ 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])
+ );
+ }
+
+ #[test]
+ fn test_v2_writer_multiple_bitmap_bodies_across_index_blocks() {
+ let data_type = DataType::Int(IntType::new());
+ let mut options = Options::new();
+ options.set("index-block-size", "28");
+ let mut writer =
+ writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&options).unwrap();
+ for value in [
+ Some(2),
+ Some(1),
+ None,
+ Some(3),
+ Some(2),
+ None,
+ Some(1),
+ Some(3),
+ ] {
+ let datum = value.map(Datum::Int);
+ writer.write(datum.as_ref()).unwrap();
+ }
+
+ let serialized = writer.serialized_bytes().unwrap();
+ assert_eq!(serialized, bytes(JAVA_INT_MULTIPLE_BODIES_MULTIBLOCK_V2));
+ let reader = BitmapFileIndexReader::try_new(data_type.clone(),
serialized).unwrap();
+ let blocks = match &reader.index {
+ BitmapIndex::V2(index) => &index.blocks,
+ BitmapIndex::V1(_) => panic!("expected V2 index"),
+ };
+ assert_eq!(blocks.len(), 2);
+ assert_eq!(
+ evaluate(&reader, &data_type, PredicateOperator::Eq,
&[Datum::Int(1)]),
+ selection([1, 6])
+ );
+ assert_eq!(
+ evaluate(&reader, &data_type, PredicateOperator::Eq,
&[Datum::Int(2)]),
+ selection([0, 4])
+ );
+ assert_eq!(
+ evaluate(&reader, &data_type, PredicateOperator::Eq,
&[Datum::Int(3)]),
+ selection([3, 7])
+ );
+ assert_eq!(
+ evaluate(&reader, &data_type, PredicateOperator::IsNull, &[]),
+ selection([2, 5])
+ );
+ }
+
+ #[test]
+ fn test_v2_writer_variable_string_index_block_boundary() {
+ let data_type = DataType::VarChar(VarCharType::new(20).unwrap());
+ let mut options = Options::new();
+ options.set("index-block-size", "33");
+ let mut writer =
+ writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&options).unwrap();
+ for value in ["bbbb", "a", "cc", "a"] {
+ writer
+ .write(Some(&Datum::String(value.to_string())))
+ .unwrap();
+ }
+
+ let serialized = writer.serialized_bytes().unwrap();
+ assert_eq!(serialized, bytes(JAVA_STRING_BLOCK_BOUNDARY_V2));
+ let reader = BitmapFileIndexReader::try_new(data_type.clone(),
serialized).unwrap();
+ let blocks = match &reader.index {
+ BitmapIndex::V2(index) => &index.blocks,
+ BitmapIndex::V1(_) => panic!("expected V2 index"),
+ };
+ // 4-byte header + 13-byte "a" entry + 16-byte "bbbb" entry.
+ assert_eq!(blocks.len(), 2);
+ assert_eq!(
+ evaluate(
+ &reader,
+ &data_type,
+ PredicateOperator::Eq,
+ &[Datum::String("a".to_string())]
+ ),
+ selection([1, 3])
+ );
+ assert_eq!(
+ evaluate(
+ &reader,
+ &data_type,
+ PredicateOperator::In,
+ &[
+ Datum::String("bbbb".to_string()),
+ Datum::String("cc".to_string())
+ ]
+ ),
+ selection([0, 2])
+ );
+ }
+
+ #[test]
+ fn test_v2_writer_rejects_invalid_config_and_unsupported_type() {
+ let data_type = DataType::Int(IntType::new());
+
+ let mut options = Options::new();
+ options.set("version", "2");
+ options.set("index-block-size", "16 kb");
+ assert!(writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&options).is_ok());
+
+ for version in ["invalid", "256"] {
+ let mut options = Options::new();
+ options.set("version", version);
+ assert!(matches!(
+ writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&options),
+ Err(Error::ConfigInvalid { .. })
+ ));
+ }
+ for version in ["1", "3"] {
+ let mut options = Options::new();
+ options.set("version", version);
+ assert!(matches!(
+ writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&options),
+ Err(Error::Unsupported { .. })
+ ));
+ }
+ for block_size in ["invalid", "0", "15", "9223372036854775807 tb"] {
+ let mut options = Options::new();
+ options.set("index-block-size", block_size);
+ assert!(matches!(
+ writer::BitmapFileIndexWriter::try_new(data_type.clone(),
&options),
+ Err(Error::ConfigInvalid { .. })
+ ));
+ }
+
+ let mut options = Options::new();
+ options.set("index-block-size", "16");
+ assert!(matches!(
+
writer::BitmapFileIndexWriter::try_new(DataType::BigInt(BigIntType::new()),
&options),
+ Err(Error::ConfigInvalid { .. })
+ ));
+
+ assert!(matches!(
+ writer::BitmapFileIndexWriter::try_new(
+ DataType::Binary(BinaryType::new(4).unwrap()),
+ &Options::new()
+ ),
+ Err(Error::Unsupported { .. })
+ ));
+ }
+
#[test]
fn test_java_v1_v2_golden_payloads_and_predicates() {
for fixture in fixtures() {
diff --git a/crates/paimon/src/file_index/bitmap/writer.rs
b/crates/paimon/src/file_index/bitmap/writer.rs
new file mode 100644
index 00000000..cc63573e
--- /dev/null
+++ b/crates/paimon/src/file_index/bitmap/writer.rs
@@ -0,0 +1,344 @@
+// 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::collections::HashMap;
+use std::ops::Range;
+
+use bytes::{BufMut, Bytes, BytesMut};
+use roaring::RoaringBitmap;
+
+use crate::common::options::parse_memory_size;
+use crate::common::Options;
+use crate::spec::{DataType, Datum};
+use crate::{Error, Result};
+
+use super::{format_invalid, BitmapValue, BitmapValueCodec, VERSION_2};
+
+const VERSION_OPTION: &str = "version";
+const INDEX_BLOCK_SIZE_OPTION: &str = "index-block-size";
+const DEFAULT_INDEX_BLOCK_SIZE: &str = "16kb";
+const INDEX_BLOCK_HEADER_SIZE: usize = 4;
+const INDEX_ENTRY_FIXED_SIZE: usize = 8;
+
+/// Writer for Java-compatible Bitmap V2 payloads.
+pub(crate) struct BitmapFileIndexWriter {
+ codec: BitmapValueCodec,
+ index_block_size: usize,
+ row_count: u32,
+ null_bitmap: RoaringBitmap,
+ bitmaps: HashMap<BitmapValue, RoaringBitmap>,
+}
+
+impl BitmapFileIndexWriter {
+ pub(crate) fn try_new(data_type: DataType, options: &Options) ->
Result<Self> {
+ let codec = BitmapValueCodec::try_new(&data_type)?;
+ validate_version(options)?;
+ let index_block_size = parse_index_block_size(options, codec)?;
+ Ok(Self {
+ codec,
+ index_block_size,
+ row_count: 0,
+ null_bitmap: RoaringBitmap::new(),
+ bitmaps: HashMap::new(),
+ })
+ }
+
+ pub(crate) fn write(&mut self, datum: Option<&Datum>) -> Result<()> {
+ if self.row_count == i32::MAX as u32 {
+ return Err(Error::DataInvalid {
+ message: "Bitmap row count exceeds i32::MAX".to_string(),
+ source: None,
+ });
+ }
+
+ let value = datum.map(|datum| self.codec.value(datum)).transpose()?;
+ match value {
+ Some(value) => {
+ self.bitmaps
+ .entry(value)
+ .or_default()
+ .insert(self.row_count);
+ }
+ None => {
+ self.null_bitmap.insert(self.row_count);
+ }
+ }
+ self.row_count += 1;
+ Ok(())
+ }
+
+ pub(crate) fn serialized_bytes(&mut self) -> Result<Bytes> {
+ let null_bytes = serialize_bitmap(&mut self.null_bitmap)?;
+ let mut body = Vec::new();
+ let null_entry = if self.null_bitmap.is_empty() {
+ None
+ } else if self.null_bitmap.len() == 1 {
+ Some((
+ singleton_offset(self.null_bitmap.min().unwrap())?,
+ usize_to_i32(null_bytes.len(), "null bitmap length")?,
+ ))
+ } else {
+ let length = usize_to_i32(null_bytes.len(), "null bitmap length")?;
+ body.extend_from_slice(&null_bytes);
+ Some((0, length))
+ };
+
+ let mut bitmaps = self.bitmaps.iter_mut().collect::<Vec<_>>();
+ bitmaps.sort_unstable_by_key(|(key, _)| *key);
+
+ let mut entries = Vec::with_capacity(bitmaps.len());
+ for (key, bitmap) in bitmaps {
+ let (offset, length) = if bitmap.len() == 1 {
+ (singleton_offset(bitmap.min().unwrap())?, -1)
+ } else {
+ let serialized = serialize_bitmap(bitmap)?;
+ let offset = usize_to_i32(body.len(), "bitmap body offset")?;
+ let length = usize_to_i32(serialized.len(), "serialized bitmap
length")?;
+ body.extend_from_slice(&serialized);
+ (offset, length)
+ };
+ entries.push(SerializedEntry {
+ key,
+ offset,
+ length,
+ });
+ }
+ usize_to_i32(body.len(), "bitmap body length")?;
+
+ let blocks = build_index_blocks(&entries, self.index_block_size)?;
+ let mut block_offsets = Vec::with_capacity(blocks.len());
+ let mut index_area_length = 0usize;
+ for block in &blocks {
+ block_offsets.push(index_area_length);
+ index_area_length = index_area_length
+ .checked_add(block_serialized_size(&entries[block.clone()])?)
+ .ok_or_else(|| format_invalid("Bitmap index area length
overflow"))?;
+ }
+
+ let mut output = BytesMut::new();
+ output.put_u8(VERSION_2);
+ output.put_i32(i32::try_from(self.row_count).map_err(|_| {
+ format_invalid(format!(
+ "Bitmap row count exceeds i32::MAX: {}",
+ self.row_count
+ ))
+ })?);
+ output.put_i32(usize_to_i32(entries.len(), "non-null bitmap count")?);
+ output.put_u8(u8::from(null_entry.is_some()));
+ if let Some((offset, length)) = null_entry {
+ output.put_i32(offset);
+ output.put_i32(length);
+ }
+
+ output.put_i32(usize_to_i32(blocks.len(), "bitmap index block
count")?);
+ for (block, offset) in blocks.iter().zip(block_offsets) {
+ write_value(&mut output, entries[block.start].key)?;
+ output.put_i32(usize_to_i32(offset, "bitmap index block offset")?);
+ }
+ output.put_i32(usize_to_i32(index_area_length, "bitmap index area
length")?);
+
+ for block in blocks {
+ output.put_i32(usize_to_i32(block.len(), "bitmap index block entry
count")?);
+ for entry in &entries[block] {
+ write_value(&mut output, entry.key)?;
+ output.put_i32(entry.offset);
+ output.put_i32(entry.length);
+ }
+ }
+ output.extend_from_slice(&body);
+ Ok(output.freeze())
+ }
+}
+
+struct SerializedEntry<'a> {
+ key: &'a BitmapValue,
+ offset: i32,
+ length: i32,
+}
+
+fn validate_version(options: &Options) -> Result<()> {
+ let Some(raw) = options.get(VERSION_OPTION) else {
+ return Ok(());
+ };
+ let version = raw.parse::<u8>().map_err(|error| Error::ConfigInvalid {
+ message: format!("Invalid Bitmap option {VERSION_OPTION}={raw}:
{error}"),
+ })?;
+ if version != VERSION_2 {
+ return Err(Error::Unsupported {
+ message: format!(
+ "Bitmap writer only supports version {VERSION_2}, but found
{version}"
+ ),
+ });
+ }
+ Ok(())
+}
+
+fn parse_index_block_size(options: &Options, codec: BitmapValueCodec) ->
Result<usize> {
+ let raw = options
+ .get(INDEX_BLOCK_SIZE_OPTION)
+ .map(String::as_str)
+ .unwrap_or(DEFAULT_INDEX_BLOCK_SIZE);
+ let size = parse_memory_size(raw).map_err(|error| Error::ConfigInvalid {
+ message: format!("Invalid Bitmap option
{INDEX_BLOCK_SIZE_OPTION}={raw}: {error:?}"),
+ })?;
+ let size = usize::try_from(size).map_err(|_| Error::ConfigInvalid {
+ message: format!("Invalid Bitmap option
{INDEX_BLOCK_SIZE_OPTION}={raw}: out of range"),
+ })?;
+ let minimum = INDEX_BLOCK_HEADER_SIZE + INDEX_ENTRY_FIXED_SIZE +
minimum_value_size(codec);
+ if size < minimum {
+ return Err(Error::ConfigInvalid {
+ message: format!(
+ "Bitmap option {INDEX_BLOCK_SIZE_OPTION} must be at least
{minimum} bytes for {codec:?}, but was {size}"
+ ),
+ });
+ }
+ Ok(size)
+}
+
+fn minimum_value_size(codec: BitmapValueCodec) -> usize {
+ match codec {
+ BitmapValueCodec::Boolean | BitmapValueCodec::TinyInt => 1,
+ BitmapValueCodec::SmallInt => 2,
+ BitmapValueCodec::Int
+ | BitmapValueCodec::Float
+ | BitmapValueCodec::Date
+ | BitmapValueCodec::Time
+ | BitmapValueCodec::String => 4,
+ BitmapValueCodec::BigInt
+ | BitmapValueCodec::Double
+ | BitmapValueCodec::TimestampMillis
+ | BitmapValueCodec::TimestampMicros
+ | BitmapValueCodec::LocalZonedTimestampMillis
+ | BitmapValueCodec::LocalZonedTimestampMicros => 8,
+ }
+}
+
+fn singleton_offset(position: u32) -> Result<i32> {
+ let position = i32::try_from(position)
+ .map_err(|_| format_invalid(format!("Bitmap row position exceeds
i32::MAX: {position}")))?;
+ (-1_i32)
+ .checked_sub(position)
+ .ok_or_else(|| format_invalid(format!("Bitmap singleton offset
overflow: {position}")))
+}
+
+fn serialize_bitmap(bitmap: &mut RoaringBitmap) -> Result<Vec<u8>> {
+ bitmap.optimize();
+ let mut serialized = Vec::with_capacity(bitmap.serialized_size());
+ bitmap
+ .serialize_into(&mut serialized)
+ .map_err(|error| Error::UnexpectedError {
+ message: "Failed to serialize Bitmap RoaringBitmap32".to_string(),
+ source: Some(Box::new(error)),
+ })?;
+ Ok(serialized)
+}
+
+fn build_index_blocks(
+ entries: &[SerializedEntry<'_>],
+ block_size_limit: usize,
+) -> Result<Vec<Range<usize>>> {
+ let mut blocks = Vec::new();
+ let mut block_start = 0usize;
+ let mut block_size = INDEX_BLOCK_HEADER_SIZE;
+
+ for (index, entry) in entries.iter().enumerate() {
+ let entry_size = INDEX_ENTRY_FIXED_SIZE
+ .checked_add(value_serialized_size(entry.key)?)
+ .ok_or_else(|| format_invalid("Bitmap index entry size
overflow"))?;
+ let minimum_block_size = INDEX_BLOCK_HEADER_SIZE
+ .checked_add(entry_size)
+ .ok_or_else(|| format_invalid("Bitmap index block size
overflow"))?;
+ if minimum_block_size > block_size_limit {
+ return Err(Error::ConfigInvalid {
+ message: format!(
+ "Bitmap option
{INDEX_BLOCK_SIZE_OPTION}={block_size_limit} bytes cannot fit a
{minimum_block_size}-byte index block"
+ ),
+ });
+ }
+ if block_size
+ .checked_add(entry_size)
+ .is_none_or(|size| size > block_size_limit)
+ {
+ blocks.push(block_start..index);
+ block_start = index;
+ block_size = INDEX_BLOCK_HEADER_SIZE;
+ }
+ block_size = block_size
+ .checked_add(entry_size)
+ .ok_or_else(|| format_invalid("Bitmap index block size
overflow"))?;
+ }
+
+ if block_start < entries.len() {
+ blocks.push(block_start..entries.len());
+ }
+ Ok(blocks)
+}
+
+fn block_serialized_size(entries: &[SerializedEntry<'_>]) -> Result<usize> {
+ let mut size = INDEX_BLOCK_HEADER_SIZE;
+ for entry in entries {
+ let value_size = value_serialized_size(entry.key)?;
+ size = size
+ .checked_add(INDEX_ENTRY_FIXED_SIZE)
+ .and_then(|size| size.checked_add(value_size))
+ .ok_or_else(|| format_invalid("Bitmap index block size
overflow"))?;
+ }
+ Ok(size)
+}
+
+fn value_serialized_size(value: &BitmapValue) -> Result<usize> {
+ match value {
+ BitmapValue::Boolean(_) | BitmapValue::TinyInt(_) => Ok(1),
+ BitmapValue::SmallInt(_) => Ok(2),
+ BitmapValue::Int(_)
+ | BitmapValue::Float(_)
+ | BitmapValue::Date(_)
+ | BitmapValue::Time(_) => Ok(4),
+ BitmapValue::BigInt(_)
+ | BitmapValue::Double(_)
+ | BitmapValue::Timestamp(_)
+ | BitmapValue::LocalZonedTimestamp(_) => Ok(8),
+ BitmapValue::String(value) => 4usize
+ .checked_add(value.len())
+ .ok_or_else(|| format_invalid("Bitmap string value size
overflow")),
+ }
+}
+
+fn write_value(output: &mut BytesMut, value: &BitmapValue) -> Result<()> {
+ match value {
+ BitmapValue::Boolean(value) => output.put_u8(u8::from(*value)),
+ BitmapValue::TinyInt(value) => output.put_i8(*value),
+ BitmapValue::SmallInt(value) => output.put_i16(*value),
+ BitmapValue::Int(value) | BitmapValue::Date(value) |
BitmapValue::Time(value) => {
+ output.put_i32(*value)
+ }
+ BitmapValue::BigInt(value)
+ | BitmapValue::Timestamp(value)
+ | BitmapValue::LocalZonedTimestamp(value) => output.put_i64(*value),
+ BitmapValue::Float(value) => output.put_u32(value.0),
+ BitmapValue::Double(value) => output.put_u64(value.0),
+ BitmapValue::String(value) => {
+ output.put_i32(usize_to_i32(value.len(), "Bitmap string value
length")?);
+ output.extend_from_slice(value.as_bytes());
+ }
+ }
+ Ok(())
+}
+
+fn usize_to_i32(value: usize, field: &str) -> Result<i32> {
+ i32::try_from(value).map_err(|_| format_invalid(format!("{field} exceeds
i32::MAX: {value}")))
+}