use crate::{
CompressionType,
checksum::ChecksummedWriter,
encryption::EncryptionProvider,
table::{
index_block::KeyedBlockHandle,
writer::index::{BlockIndexWriter, FullIndexWriter, PartitionedIndexWriter},
},
};
use alloc::sync::Arc;
#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, vec::Vec};
use crate::io::{Seek, Write};
pub const DEFAULT_SPILL_THRESHOLD: u64 = 4 * 1024 * 1024;
pub struct AdaptiveIndexWriter<W: Write + Seek + 'static> {
compression: CompressionType,
restart_interval: u8,
#[cfg(zstd_any)]
zstd_two_pass_seed: bool,
partition_size: u32,
encryption: Option<Arc<dyn EncryptionProvider>>,
table_id: crate::TableId,
ecc: Option<crate::table::block::EccParams>,
spill_threshold: u64,
buffer: Vec<KeyedBlockHandle>,
buffered_bytes: u64,
buffered_encoded: u64,
spilled: Option<Box<dyn BlockIndexWriter<W>>>,
}
impl<W: Write + Seek + 'static> AdaptiveIndexWriter<W> {
#[must_use]
pub fn new(spill_threshold: u64) -> Self {
Self {
compression: CompressionType::None,
restart_interval: 1,
#[cfg(zstd_any)]
zstd_two_pass_seed: true,
partition_size: 4_096,
encryption: None,
table_id: 0,
ecc: None,
spill_threshold,
buffer: Vec::new(),
buffered_bytes: 0,
buffered_encoded: 0,
spilled: None,
}
}
fn configure(&self, inner: Box<dyn BlockIndexWriter<W>>) -> Box<dyn BlockIndexWriter<W>> {
#[cfg(zstd_any)]
let inner = inner.use_zstd_two_pass_seed(self.zstd_two_pass_seed);
inner
.use_compression(self.compression)
.use_restart_interval(self.restart_interval)
.use_partition_size(self.partition_size)
.use_encryption(self.encryption.clone())
.use_table_id(self.table_id)
.use_ecc(self.ecc)
}
fn buffered_block(&self) -> u64 {
self.buffered_encoded + crate::table::block::TRAILER_LEN as u64
}
fn spill(&mut self) -> crate::Result<()> {
let mut partitioned = self.configure(Box::new(PartitionedIndexWriter::new()));
for handle in core::mem::take(&mut self.buffer) {
partitioned.register_data_block(handle)?;
}
self.buffered_bytes = 0;
self.buffered_encoded = 0;
self.spilled = Some(partitioned);
Ok(())
}
}
impl<W: Write + Seek + 'static> BlockIndexWriter<W> for AdaptiveIndexWriter<W> {
fn register_data_block(&mut self, block_handle: KeyedBlockHandle) -> crate::Result<()> {
if let Some(partitioned) = &mut self.spilled {
return partitioned.register_data_block(block_handle);
}
let entry_size =
(block_handle.end_key().len() + core::mem::size_of::<KeyedBlockHandle>()) as u64;
self.buffered_bytes += entry_size;
self.buffered_encoded += block_handle.encoded_len_bound() as u64;
self.buffer.push(block_handle);
if self.buffered_bytes > self.spill_threshold {
self.spill()?;
}
Ok(())
}
fn held_bytes(&self) -> u64 {
match &self.spilled {
Some(partitioned) => partitioned.held_bytes(),
#[expect(clippy::cast_possible_truncation, reason = "bytes held in memory")]
None => super::handles_held(
self.buffered_bytes as usize,
&self.buffer,
self.buffer.capacity(),
) as u64,
}
}
fn finish_scratch_bytes(&self) -> u64 {
if let Some(partitioned) = &self.spilled {
return partitioned.finish_scratch_bytes();
}
let block = self.buffered_block();
block
+ crate::table::block::transform_scratch_bound(
block,
crate::table::block::BlockType::Index,
self.compression,
self.encryption.as_deref(),
self.ecc,
)
}
fn finish_output_bytes(&self) -> u64 {
match &self.spilled {
Some(partitioned) => partitioned.finish_output_bytes(),
None if self.buffer.is_empty() => 0,
None => {
2 * crate::table::block::framed_len_bound(
self.buffered_block(),
crate::table::block::BlockType::Index,
self.compression,
self.encryption.as_deref(),
self.ecc,
)
}
}
}
fn finish(
self: Box<Self>,
file_writer: &mut crate::sfa::Writer<ChecksummedWriter<W>>,
) -> crate::Result<(usize, Vec<u8>)> {
let mut this = *self;
if let Some(partitioned) = this.spilled.take() {
return partitioned.finish(file_writer);
}
#[expect(clippy::cast_possible_truncation, reason = "bytes held in memory")]
let full = FullIndexWriter::with_handles(
core::mem::take(&mut this.buffer),
this.buffered_bytes as usize,
this.buffered_encoded as usize,
);
this.configure(Box::new(full)).finish(file_writer)
}
fn use_compression(
mut self: Box<Self>,
compression: CompressionType,
) -> Box<dyn BlockIndexWriter<W>> {
self.compression = compression;
self
}
#[cfg(zstd_any)]
fn use_zstd_two_pass_seed(mut self: Box<Self>, enabled: bool) -> Box<dyn BlockIndexWriter<W>> {
self.zstd_two_pass_seed = enabled;
if let Some(spilled) = self.spilled.take() {
self.spilled = Some(spilled.use_zstd_two_pass_seed(enabled));
}
self
}
fn use_restart_interval(mut self: Box<Self>, interval: u8) -> Box<dyn BlockIndexWriter<W>> {
self.restart_interval = interval;
self
}
fn use_partition_size(mut self: Box<Self>, size: u32) -> Box<dyn BlockIndexWriter<W>> {
self.partition_size = size;
self
}
fn use_encryption(
mut self: Box<Self>,
encryption: Option<Arc<dyn EncryptionProvider>>,
) -> Box<dyn BlockIndexWriter<W>> {
self.encryption = encryption;
self
}
fn use_table_id(mut self: Box<Self>, table_id: crate::TableId) -> Box<dyn BlockIndexWriter<W>> {
self.table_id = table_id;
self
}
fn use_ecc(
mut self: Box<Self>,
ecc: Option<crate::table::block::EccParams>,
) -> Box<dyn BlockIndexWriter<W>> {
self.ecc = ecc;
self
}
}
#[cfg(test)]
mod tests;