use arrow::array::RecordBatch;
use arrow::datatypes::SchemaRef;
use arrow::ipc::writer::{IpcWriteOptions, StreamWriter};
use polyc_query_model::{DataFrame, ErrorClass, ModelError, SchemaFrame};
#[derive(Debug, thiserror::Error)]
pub enum FrameEncodeError {
#[error("the result could not be encoded")]
Encode,
#[error("the schema needs {schema_bytes} bytes, over the {ceiling_bytes} byte frame ceiling")]
SchemaTooLarge {
schema_bytes: usize,
ceiling_bytes: usize,
},
#[error("one row needs {row_bytes} bytes, over the {ceiling_bytes} byte frame ceiling")]
RowTooLarge {
row_bytes: usize,
ceiling_bytes: usize,
},
#[error("the encoded frame is outside its protocol bound")]
Model(#[from] ModelError),
}
impl FrameEncodeError {
#[must_use]
pub const fn class(&self) -> ErrorClass {
match self {
Self::SchemaTooLarge { .. } | Self::RowTooLarge { .. } => ErrorClass::Bounds,
Self::Encode | Self::Model(_) => ErrorClass::Internal,
}
}
}
pub(crate) struct FrameEncoder {
schema: SchemaRef,
ceiling_bytes: usize,
next_sequence: u64,
cursor: Option<(RecordBatch, usize)>,
#[cfg(test)]
built: u64,
}
impl std::fmt::Debug for FrameEncoder {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("FrameEncoder")
.field("ceiling_bytes", &self.ceiling_bytes)
.field("next_sequence", &self.next_sequence)
.finish_non_exhaustive()
}
}
impl FrameEncoder {
pub(crate) fn start(
schema: SchemaRef,
ceiling_bytes: usize,
) -> Result<(Self, SchemaFrame), FrameEncodeError> {
let encoded = encode(&schema, None)?;
if encoded.len() > ceiling_bytes {
return Err(FrameEncodeError::SchemaTooLarge {
schema_bytes: encoded.len(),
ceiling_bytes,
});
}
let frame = SchemaFrame::try_new(encoded)?;
Ok((
Self {
schema,
ceiling_bytes,
next_sequence: 0,
cursor: None,
#[cfg(test)]
built: 0,
},
frame,
))
}
pub(crate) fn begin(&mut self, batch: RecordBatch) {
if batch.num_rows() > 0 {
self.cursor = Some((batch, 0));
}
}
pub(crate) fn next(&mut self) -> Result<Option<DataFrame>, FrameEncodeError> {
let Some((batch, offset)) = self.cursor.take() else {
return Ok(None);
};
let remaining = batch.num_rows() - offset;
let (encoded, rows) = self.fit(&batch, offset, remaining)?;
let sequence = self.next_sequence;
self.next_sequence = self.next_sequence.saturating_add(1);
#[cfg(test)]
{
self.built = self.built.saturating_add(1);
}
let advanced = offset + rows;
if advanced < batch.num_rows() {
self.cursor = Some((batch, advanced));
}
let row_count = u64::try_from(rows).unwrap_or(u64::MAX);
DataFrame::try_new(sequence, row_count, encoded)
.map(Some)
.map_err(FrameEncodeError::from)
}
pub(crate) const fn has_rows(&self) -> bool {
self.cursor.is_some()
}
pub(crate) fn discard(&mut self) {
self.cursor = None;
}
#[cfg(test)]
pub(crate) const fn built(&self) -> u64 {
self.built
}
fn fit(
&self,
batch: &RecordBatch,
offset: usize,
remaining: usize,
) -> Result<(Vec<u8>, usize), FrameEncodeError> {
let mut length = 1;
let mut fitting: Option<(Vec<u8>, usize)> = None;
loop {
let candidate = batch.slice(offset, length);
let encoded = encode(&self.schema, Some(&candidate))?;
if encoded.len() > self.ceiling_bytes {
return fitting.ok_or(FrameEncodeError::RowTooLarge {
row_bytes: encoded.len(),
ceiling_bytes: self.ceiling_bytes,
});
}
if length == remaining {
return Ok((encoded, length));
}
fitting = Some((encoded, length));
length = length.saturating_mul(2).min(remaining);
}
}
}
fn encode(schema: &SchemaRef, batch: Option<&RecordBatch>) -> Result<Vec<u8>, FrameEncodeError> {
let options = IpcWriteOptions::default();
let mut writer = StreamWriter::try_new_with_options(Vec::new(), schema, options)
.map_err(|_arrow| FrameEncodeError::Encode)?;
if let Some(batch) = batch {
writer
.write(batch)
.map_err(|_arrow| FrameEncodeError::Encode)?;
}
writer.finish().map_err(|_arrow| FrameEncodeError::Encode)?;
writer
.into_inner()
.map_err(|_arrow| FrameEncodeError::Encode)
}