use crate::basic::Type;
use crate::column::writer::LevelDataRef;
use crate::column::writer::encoder::ColumnValueEncoder;
use crate::file::properties::ResolvedColumnProperties;
use crate::schema::types::ColumnDescriptor;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SubBatchStrategy {
Values(usize),
Levels(usize),
}
pub(crate) struct ByteBudgetChunker {
page_byte_limit: usize,
max_def_level: i16,
static_always_fits: bool,
dict_page_byte_limit: usize,
static_dict_always_fits: bool,
}
impl ByteBudgetChunker {
#[inline]
pub(crate) fn new(
descr: &ColumnDescriptor,
column_props: &ResolvedColumnProperties,
base_batch_size: usize,
) -> Self {
let page_byte_limit = column_props.data_page_size_limit;
let dict_page_byte_limit = column_props.dictionary_page_size_limit;
let static_bytes_per_value = match descr.physical_type() {
Type::BOOLEAN => Some(1),
Type::INT32 | Type::FLOAT => Some(std::mem::size_of::<i32>()),
Type::INT64 | Type::DOUBLE => Some(std::mem::size_of::<i64>()),
Type::INT96 => Some(12),
Type::FIXED_LEN_BYTE_ARRAY => Some(descr.type_length().max(0) as usize),
Type::BYTE_ARRAY => None,
};
let static_fits = |limit: usize| {
static_bytes_per_value
.map(|b| b.saturating_mul(base_batch_size) <= limit)
.unwrap_or(false)
};
Self {
page_byte_limit,
max_def_level: descr.max_def_level(),
static_always_fits: static_fits(page_byte_limit),
dict_page_byte_limit,
static_dict_always_fits: static_fits(dict_page_byte_limit),
}
}
#[inline]
pub(crate) fn pick_sub_batch<E: ColumnValueEncoder>(
&self,
encoder: &E,
values: &E::Values,
value_indices: Option<&[usize]>,
chunk_def: LevelDataRef<'_>,
values_offset: usize,
chunk_size: usize,
) -> Option<SubBatchStrategy> {
if chunk_size == 0 {
return None;
}
let (budget, value_exact) = if encoder.has_dictionary() {
if self.static_dict_always_fits {
return None;
}
let used = encoder.estimated_dict_page_size()?;
(self.dict_page_byte_limit.saturating_sub(used), false)
} else {
if self.static_always_fits {
return None;
}
(
self.page_byte_limit,
encoder.compresses_against_previous_value(),
)
};
self.byte_budget_sub_batch::<E>(
values,
value_indices,
chunk_def,
values_offset,
chunk_size,
budget,
value_exact,
)
}
#[inline(never)]
#[expect(clippy::too_many_arguments)]
fn byte_budget_sub_batch<E: ColumnValueEncoder>(
&self,
values: &E::Values,
value_indices: Option<&[usize]>,
chunk_def: LevelDataRef<'_>,
values_offset: usize,
chunk_size: usize,
budget: usize,
value_exact: bool,
) -> Option<SubBatchStrategy> {
let vals_in_chunk = chunk_def.value_count(chunk_size, self.max_def_level);
if vals_in_chunk == 0 {
return None;
}
let fit = match value_indices {
Some(idx) => {
let end = (values_offset + vals_in_chunk).min(idx.len());
let start = values_offset.min(end);
E::count_values_within_byte_budget_gather(values, &idx[start..end], budget)
}
None => {
E::count_values_within_byte_budget(values, values_offset, vals_in_chunk, budget)
}
};
match fit {
None => None,
Some(values_per_subbatch) if values_per_subbatch >= vals_in_chunk => None,
Some(values_per_subbatch) => {
let values_per_subbatch = values_per_subbatch.max(1);
Some(if value_exact {
SubBatchStrategy::Values(values_per_subbatch)
} else {
let levels_per_subbatch = if vals_in_chunk == chunk_size {
values_per_subbatch
} else {
(values_per_subbatch * chunk_size)
.div_ceil(vals_in_chunk)
.max(1)
};
SubBatchStrategy::Levels(chunk_size.min(levels_per_subbatch))
})
}
}
}
}