use std::fmt;
use arrow_array::RecordBatch;
use arrow_schema::SchemaRef;
use densa_grid::{Align, CellStyle, CellText, Color, Column, GridSource, Palette, CELL_BYTES};
use crate::reader::{is_numeric, Format, Reader};
const SAMPLE_ROWS: usize = 1_000;
const MAX_SAMPLED_CHARS: usize = 40;
const DEFAULT_CHARS: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CellState {
Value,
Null,
Unsupported,
Invalid,
OutOfRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum TableError {
NoBatches,
ColumnCount {
expected: usize,
found: usize,
},
ColumnMismatch {
column: usize,
},
NoSuchColumn(usize),
}
impl fmt::Display for TableError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoBatches => f.write_str("no record batch was given, so there is no schema"),
Self::ColumnCount { expected, found } => {
write!(f, "the batch has {found} columns, the table {expected}")
}
Self::ColumnMismatch { column } => {
write!(f, "column {column} of the batch has another data type")
}
Self::NoSuchColumn(column) => write!(f, "there is no column {column}"),
}
}
}
impl std::error::Error for TableError {}
#[derive(Debug, Clone)]
pub struct ArrowTable {
schema: SchemaRef,
batches: Vec<Vec<Reader>>,
starts: Vec<usize>,
rows: usize,
formats: Vec<Format>,
sized: bool,
}
impl ArrowTable {
pub fn new(schema: SchemaRef) -> Self {
let formats = schema
.fields()
.iter()
.map(|field| Format {
decimals: None,
max_chars: field.name().chars().count().max(DEFAULT_CHARS),
width_set: false,
})
.collect();
Self {
schema,
batches: Vec::new(),
starts: Vec::new(),
rows: 0,
formats,
sized: false,
}
}
pub fn from_batches(
batches: impl IntoIterator<Item = RecordBatch>,
) -> Result<Self, TableError> {
let mut batches = batches.into_iter();
let first = batches.next().ok_or(TableError::NoBatches)?;
let mut table = Self::new(first.schema());
for batch in std::iter::once(first).chain(batches) {
table.push(batch)?;
}
Ok(table)
}
pub fn push(&mut self, batch: RecordBatch) -> Result<(), TableError> {
self.check(&batch)?;
if batch.num_rows() == 0 {
return Ok(());
}
let readers: Vec<Reader> = batch.columns().iter().cloned().map(Reader::new).collect();
if !self.sized {
self.size_columns(&readers);
}
self.starts.push(self.rows);
self.rows += batch.num_rows();
self.batches.push(readers);
Ok(())
}
pub fn schema(&self) -> &SchemaRef {
&self.schema
}
pub fn rows(&self) -> usize {
self.rows
}
pub fn columns(&self) -> Vec<Column> {
self.schema
.fields()
.iter()
.zip(&self.formats)
.map(|(field, format)| {
let align = if is_numeric(field.data_type()) {
Align::Right
} else {
Align::Left
};
let chars = u32::try_from(format.max_chars).unwrap_or(u32::MAX);
Column::new(field.name().clone(), chars, align)
})
.collect()
}
pub fn set_decimals(
&mut self,
column: usize,
decimals: Option<usize>,
) -> Result<(), TableError> {
self.format_mut(column)?.decimals = decimals;
if let (Some(reader), Some(field)) = (
self.batches.first().and_then(|b| b.get(column)),
self.schema.fields().get(column),
) {
let format = &mut self.formats[column];
size_column(format, reader, field.name());
}
Ok(())
}
pub fn set_column_chars(&mut self, column: usize, chars: usize) -> Result<(), TableError> {
let format = self.format_mut(column)?;
format.max_chars = chars.clamp(1, CELL_BYTES);
format.width_set = true;
Ok(())
}
pub fn write_cell(&self, row: usize, column: usize, text: &mut CellText) -> CellState {
if row >= self.rows {
return CellState::OutOfRange;
}
let batch = self.starts.partition_point(|&start| start <= row) - 1;
self.write_at(batch, row - self.starts[batch], column, text)
}
pub(crate) fn write_at(
&self,
batch: usize,
local: usize,
column: usize,
text: &mut CellText,
) -> CellState {
let reader = self.batches.get(batch).and_then(|b| b.get(column));
let (Some(reader), Some(format)) = (reader, self.formats.get(column)) else {
return CellState::OutOfRange;
};
if local >= reader.len() {
return CellState::OutOfRange;
}
if reader.is_null(local) {
let _ = text.push_str("null");
return CellState::Null;
}
if !reader.is_supported() {
reader.write(local, text, format);
return CellState::Unsupported;
}
let mark = text.len();
if reader.write(local, text, format) {
return CellState::Value;
}
text.truncate(mark);
let _ = text.push_str("#");
CellState::Invalid
}
pub fn view<'a>(&'a self, first_row: usize, palette: &Palette) -> ArrowGrid<'a> {
ArrowGrid {
table: self,
first_row,
text: palette.text,
dim: palette.dim_text,
}
}
fn check(&self, batch: &RecordBatch) -> Result<(), TableError> {
let ours = self.schema.fields();
let theirs = batch.schema_ref().fields();
if ours.len() != theirs.len() {
return Err(TableError::ColumnCount {
expected: ours.len(),
found: theirs.len(),
});
}
match ours
.iter()
.zip(theirs.iter())
.position(|(a, b)| a.data_type() != b.data_type())
{
Some(column) => Err(TableError::ColumnMismatch { column }),
None => Ok(()),
}
}
fn size_columns(&mut self, readers: &[Reader]) {
for ((reader, format), field) in readers
.iter()
.zip(&mut self.formats)
.zip(self.schema.fields().iter())
{
size_column(format, reader, field.name());
}
self.sized = true;
}
pub(crate) fn is_sized(&self) -> bool {
self.sized
}
pub(crate) fn stored_batches(&self) -> usize {
self.batches.len()
}
pub(crate) fn clear_batches(&mut self) {
self.batches.clear();
self.starts.clear();
self.rows = 0;
}
fn format_mut(&mut self, column: usize) -> Result<&mut Format, TableError> {
self.formats
.get_mut(column)
.ok_or(TableError::NoSuchColumn(column))
}
}
fn size_column(format: &mut Format, reader: &Reader, title: &str) {
if format.width_set {
return;
}
let sampling = Format {
max_chars: MAX_SAMPLED_CHARS,
..*format
};
let mut text = CellText::new();
let mut widest = title.chars().count();
for row in 0..reader.len().min(SAMPLE_ROWS) {
text.clear();
if !reader.is_null(row) {
reader.write(row, &mut text, &sampling);
}
widest = widest.max(text.as_str().chars().count());
}
format.max_chars = widest.max(1);
}
#[derive(Debug, Clone, Copy)]
pub struct ArrowGrid<'a> {
table: &'a ArrowTable,
first_row: usize,
text: Color,
dim: Color,
}
impl GridSource for ArrowGrid<'_> {
fn rows(&self) -> usize {
self.table.rows().saturating_sub(self.first_row)
}
fn cell(&self, row: usize, column: usize, text: &mut CellText) -> CellStyle {
let state = self
.table
.write_cell(self.first_row.saturating_add(row), column, text);
let color = if state == CellState::Value {
self.text
} else {
self.dim
};
CellStyle::new(color)
}
}