use crate::array::{Array, ScalarValue};
use crate::batch::RecordBatch;
use crate::format::constants::{
BLOCK_COLUMN_DESCRIPTOR_SIZE, BLOCK_STREAM_DESCRIPTOR_SIZE, DATA_BLOCK_HEADER_SIZE,
FRAME_ALIGNMENT, PREFIX_SIZE, TRAILER_SIZE,
};
use crate::limits::Limits;
use crate::schema::{Column, LogicalType, Schema};
use super::api::WriterStatistics;
const LENGTH_ENTRY_BYTES: u64 = 4;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct ColumnFootprint {
dense_count: u64,
null_count: u64,
variable_bytes: u64,
}
impl ColumnFootprint {
pub(super) fn add_row(&mut self, array: &Array, row: usize) {
match array.value_at(row) {
None => self.null_count += 1,
Some(value) => {
self.dense_count += 1;
self.variable_bytes += variable_byte_length(value);
}
}
}
pub(super) fn has_implicit_validity(&self) -> bool {
self.null_count == 0 || self.dense_count == 0
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct BlockSize {
pub(super) header_bytes: u64,
pub(super) payload_bytes: u64,
pub(super) frame_bytes: u64,
}
impl BlockSize {
pub(super) fn within_format_limits(&self, rows: u64) -> bool {
let limits = Limits::default();
rows <= limits.max_rows_per_block()
&& rows <= u64::from(u32::MAX)
&& self.header_bytes <= limits.max_frame_header_length()
&& self.payload_bytes <= limits.max_frame_payload_length()
}
}
pub(super) fn block_size_with_statistics(
schema: &Schema,
footprints: &[ColumnFootprint],
rows: u64,
statistics: WriterStatistics,
) -> BlockSize {
let mut payload_bytes = 0_u64;
let mut stream_count = 0_u64;
let mut statistics_bytes = 0_u64;
for (column, footprint) in schema.columns().iter().zip(footprints) {
let (bytes, streams) = column_size(column, footprint, rows);
payload_bytes = payload_bytes.saturating_add(bytes);
stream_count = stream_count.saturating_add(streams);
if should_reserve_statistics(schema, column, footprint, statistics) {
statistics_bytes = statistics_bytes
.saturating_add(statistics_width(column.logical_type()).saturating_mul(2));
}
}
if statistics_bytes != 0 {
statistics_bytes =
statistics_bytes.saturating_add(FRAME_ALIGNMENT - 1) & !(FRAME_ALIGNMENT - 1);
}
let column_count = schema.column_count() as u64;
let header_bytes = DATA_BLOCK_HEADER_SIZE
.saturating_add(column_count.saturating_mul(BLOCK_COLUMN_DESCRIPTOR_SIZE))
.saturating_add(stream_count.saturating_mul(BLOCK_STREAM_DESCRIPTOR_SIZE))
.saturating_add(statistics_bytes);
let frame_bytes = (PREFIX_SIZE as u64)
.saturating_add(header_bytes)
.saturating_add(payload_bytes)
.saturating_add(TRAILER_SIZE as u64);
BlockSize {
header_bytes,
payload_bytes,
frame_bytes,
}
}
fn should_reserve_statistics(
schema: &Schema,
column: &Column,
footprint: &ColumnFootprint,
statistics: WriterStatistics,
) -> bool {
if footprint.dense_count == 0 || !statistics_width_supported(column.logical_type()) {
return false;
}
if matches!(statistics, WriterStatistics::MinMax) {
return true;
}
if !matches!(statistics, WriterStatistics::Automatic)
|| schema.primary_column_id() == Some(column.id())
|| footprint.dense_count < 64
{
return false;
}
let width = statistics_width(column.logical_type());
let value_bytes = if matches!(column.logical_type(), LogicalType::Bool) {
bitmap_bytes(footprint.dense_count)
} else {
width.saturating_mul(footprint.dense_count)
};
width.saturating_mul(2).saturating_mul(8) <= value_bytes
}
fn statistics_width_supported(logical_type: &LogicalType) -> bool {
statistics_width(logical_type) != 0
}
fn statistics_width(logical_type: &LogicalType) -> u64 {
match logical_type {
LogicalType::Bool | LogicalType::Int8 | LogicalType::UInt8 => 1,
LogicalType::Int16 | LogicalType::UInt16 => 2,
LogicalType::Int32 | LogicalType::UInt32 | LogicalType::Float32 | LogicalType::Date32 => 4,
LogicalType::Int64
| LogicalType::UInt64
| LogicalType::Float64
| LogicalType::Decimal { .. }
| LogicalType::Timestamp { .. } => 8,
LogicalType::FixedBinary { byte_width } => u64::from(*byte_width),
LogicalType::Utf8 | LogicalType::Categorical { .. } | LogicalType::Binary => 0,
}
}
fn column_size(column: &Column, footprint: &ColumnFootprint, rows: u64) -> (u64, u64) {
let mut bytes = 0_u64;
let mut streams = 0_u64;
if !footprint.has_implicit_validity() {
bytes = bytes.saturating_add(stream_payload_bytes(bitmap_bytes(rows)));
streams += 1;
}
if footprint.dense_count != 0 {
let values = values_bytes(column.logical_type(), footprint);
bytes = bytes.saturating_add(stream_payload_bytes(values));
streams += 1;
if is_variable_width(column.logical_type()) {
let lengths = LENGTH_ENTRY_BYTES.saturating_mul(footprint.dense_count);
bytes = bytes.saturating_add(stream_payload_bytes(lengths));
streams += 1;
}
}
(bytes, streams)
}
fn values_bytes(logical_type: &LogicalType, footprint: &ColumnFootprint) -> u64 {
let value_width = match logical_type {
LogicalType::Bool => return bitmap_bytes(footprint.dense_count),
LogicalType::Utf8 | LogicalType::Categorical { .. } | LogicalType::Binary => {
return footprint.variable_bytes;
}
LogicalType::Int8 | LogicalType::UInt8 => 1,
LogicalType::Int16 | LogicalType::UInt16 => 2,
LogicalType::Int32 | LogicalType::UInt32 | LogicalType::Float32 | LogicalType::Date32 => 4,
LogicalType::Int64
| LogicalType::UInt64
| LogicalType::Float64
| LogicalType::Decimal { .. }
| LogicalType::Timestamp { .. } => 8,
LogicalType::FixedBinary { byte_width } => u64::from(*byte_width),
};
value_width.saturating_mul(footprint.dense_count)
}
fn variable_byte_length(value: ScalarValue<'_>) -> u64 {
match value {
ScalarValue::Utf8(value) | ScalarValue::Categorical(value) => value.len() as u64,
ScalarValue::Binary(value) => value.len() as u64,
_ => 0,
}
}
pub(super) fn is_variable_width(logical_type: &LogicalType) -> bool {
matches!(
logical_type,
LogicalType::Utf8 | LogicalType::Categorical { .. } | LogicalType::Binary
)
}
pub(super) fn stream_payload_bytes(length: u64) -> u64 {
if length == 0 {
return FRAME_ALIGNMENT;
}
length.saturating_add(FRAME_ALIGNMENT - 1) & !(FRAME_ALIGNMENT - 1)
}
pub(super) fn bitmap_bytes(bits: u64) -> u64 {
bits.div_ceil(8)
}
#[derive(Debug)]
pub(super) struct BlockBuffer {
chunks: Vec<RecordBatch>,
rows: usize,
footprints: Vec<ColumnFootprint>,
size: BlockSize,
statistics: WriterStatistics,
}
impl BlockBuffer {
pub(super) fn new_with_statistics(column_count: usize, statistics: WriterStatistics) -> Self {
Self {
chunks: Vec::new(),
rows: 0,
footprints: vec![ColumnFootprint::default(); column_count],
size: BlockSize::default(),
statistics,
}
}
pub(super) fn is_empty(&self) -> bool {
self.rows == 0
}
pub(super) fn row_count(&self) -> u64 {
self.rows as u64
}
pub(super) fn frame_bytes(&self) -> u64 {
self.size.frame_bytes
}
pub(super) fn footprints(&self) -> &[ColumnFootprint] {
&self.footprints
}
pub(super) fn rows(&self) -> BlockRows<'_> {
BlockRows {
chunks: &self.chunks,
row_count: self.rows,
}
}
pub(super) fn push(&mut self, schema: &Schema, chunk: RecordBatch) {
for (footprint, array) in self.footprints.iter_mut().zip(chunk.columns()) {
for row in 0..chunk.row_count() {
footprint.add_row(array, row);
}
}
self.rows += chunk.row_count();
self.size =
block_size_with_statistics(schema, &self.footprints, self.row_count(), self.statistics);
self.chunks.push(chunk);
}
pub(super) fn clear(&mut self) {
self.chunks.clear();
self.rows = 0;
self.footprints.fill(ColumnFootprint::default());
self.size = BlockSize::default();
}
}
pub(super) struct BlockRows<'a> {
pub(super) chunks: &'a [RecordBatch],
pub(super) row_count: usize,
}
impl<'a> BlockRows<'a> {
pub(super) fn column(&self, index: usize) -> impl Iterator<Item = (&'a Array, usize)> {
self.chunks
.iter()
.filter_map(move |chunk| chunk.column(index))
.flat_map(|array| (0..array.len()).map(move |row| (array, row)))
}
}