use std::collections::HashMap;
use arrow::array::ArrayRef;
use arrow::record_batch::RecordBatch;
use std::sync::Arc;
use crate::query::{ColumnInfo, QueryRow};
use crate::types::Value;
use super::arrow_convert::{build_arrow_schema, convert_column_to_array, ArrowConvertError};
use super::arrow_size::PreparedColumns;
const RETAINED_SLOT_SLACK: usize = 2;
const RETAINED_SLOT_FLOOR: usize = 1024;
pub struct ArrowRowAccumulator<'a> {
columns: &'a [ColumnInfo],
prepared: PreparedColumns<'a>,
name_to_canonical: HashMap<&'a str, usize>,
canonical: Vec<usize>,
cells: Vec<Vec<(usize, Value)>>,
staged: Vec<Option<Value>>,
has_staged: bool,
rows: usize,
peak_cells: usize,
staged_idx: Vec<usize>,
last_used: Vec<usize>,
}
impl<'a> ArrowRowAccumulator<'a> {
pub fn new(columns: &'a [ColumnInfo]) -> Self {
let mut name_to_canonical: HashMap<&'a str, usize> = HashMap::with_capacity(columns.len());
for (idx, col) in columns.iter().enumerate() {
name_to_canonical.entry(col.name.as_str()).or_insert(idx);
}
let canonical: Vec<usize> = columns
.iter()
.enumerate()
.map(|(idx, col)| {
name_to_canonical
.get(col.name.as_str())
.copied()
.unwrap_or(idx)
})
.collect();
Self {
columns,
prepared: PreparedColumns::new(columns),
name_to_canonical,
canonical,
cells: (0..columns.len()).map(|_| Vec::new()).collect(),
staged: vec![None; columns.len()],
has_staged: false,
rows: 0,
peak_cells: 0,
staged_idx: Vec::new(),
last_used: vec![0; columns.len()],
}
}
pub fn with_capacity(columns: &'a [ColumnInfo], capacity: usize) -> Self {
let _ = capacity;
Self::new(columns)
}
pub fn len(&self) -> usize {
self.rows
}
pub fn is_empty(&self) -> bool {
self.rows == 0
}
pub fn stage(&mut self, row: QueryRow) -> usize {
debug_assert!(
!self.has_staged,
"ArrowRowAccumulator::stage called with a row still staged — the \
contract is stage -> (flush) -> commit per row (issue #3552)"
);
debug_assert_eq!(
self.staged.len(),
self.columns.len(),
"staging arity must equal the projected column count"
);
if self.has_staged {
for &idx in &self.staged_idx {
if let Some(slot) = self.staged.get_mut(idx) {
*slot = None;
}
}
} else {
debug_assert!(
self.staged.iter().all(Option::is_none),
"staging slots must be empty when no row is staged — `commit` takes \
every slot, so a non-empty slot here means that contract broke"
);
}
self.staged_idx.clear();
let staged = &mut self.staged;
let staged_idx = &mut self.staged_idx;
let name_to_canonical = &self.name_to_canonical;
for (name, value) in row.values {
if let Some(&canonical_idx) = name_to_canonical.get(name.as_ref()) {
staged[canonical_idx] = Some(value);
staged_idx.push(canonical_idx);
}
}
self.has_staged = true;
self.prepared.row_bytes(&self.staged, &self.canonical)
}
pub fn commit(&mut self) {
if !self.has_staged {
return;
}
let row_idx = self.rows;
let Self {
cells,
staged,
canonical,
staged_idx,
..
} = self;
for &idx in staged_idx.iter() {
let Some(slot) = staged.get_mut(idx) else {
continue;
};
let value = slot.take();
match (canonical.get(idx), cells.get_mut(idx)) {
(Some(&c), Some(store)) if c == idx => {
if let Some(value) = value {
store.push((row_idx, value));
}
}
_ => {}
}
}
self.staged_idx.clear();
self.rows = self.rows.saturating_add(1);
self.has_staged = false;
}
pub fn clear(&mut self) {
let mut used = 0usize;
for (idx, column) in self.cells.iter().enumerate() {
let n = column.len();
used = used.saturating_add(n);
if let Some(slot) = self.last_used.get_mut(idx) {
*slot = n;
}
}
self.peak_cells = self.peak_cells.max(used);
for column in &mut self.cells {
column.clear();
}
let allowance = self
.peak_cells
.saturating_mul(RETAINED_SLOT_SLACK)
.max(RETAINED_SLOT_FLOOR);
let retained: usize = self.cells.iter().map(Vec::capacity).sum();
if retained > allowance {
let Self {
cells, last_used, ..
} = self;
for (idx, column) in cells.iter_mut().enumerate() {
let keep = last_used
.get(idx)
.copied()
.unwrap_or(0)
.saturating_mul(RETAINED_SLOT_SLACK);
if column.capacity() > keep {
column.shrink_to(keep);
}
}
}
self.rows = 0;
}
#[cfg(test)]
pub(crate) fn retained_cell_slots(&self) -> usize {
self.cells.iter().map(Vec::capacity).sum()
}
#[cfg(test)]
pub(crate) fn retained_cell_slots_for(&self, column: usize) -> usize {
self.cells.get(column).map_or(0, Vec::capacity)
}
pub fn recomputed_payload(&self) -> usize {
(0..self.rows).fold(0usize, |acc, row| {
acc.saturating_add(
self.prepared
.row_bytes_columnar(&self.cells, &self.canonical, row),
)
})
}
pub fn to_record_batch(&self) -> Result<RecordBatch, ArrowConvertError> {
let schema = Arc::new(build_arrow_schema(self.columns)?);
if self.cells.len() != self.columns.len()
|| self.canonical.len() != self.columns.len()
|| self.prepared.len() != self.columns.len()
{
return Err(ArrowConvertError::InvalidValue(format!(
"columnar accumulator arity {} does not match the {} projected columns",
self.cells.len(),
self.columns.len()
)));
}
let mut view: Vec<Option<&Value>> = Vec::with_capacity(self.rows);
let mut arrays: Vec<ArrayRef> = Vec::with_capacity(self.columns.len());
for (idx, col) in self.columns.iter().enumerate() {
let store = match self.canonical.get(idx).and_then(|&c| self.cells.get(c)) {
Some(store) => store,
None => {
return Err(ArrowConvertError::InvalidValue(format!(
"column {idx} has no canonical cell store"
)))
}
};
if let Some(&(last_row, _)) = store.last() {
if last_row >= self.rows {
return Err(ArrowConvertError::InvalidValue(format!(
"column {idx} holds row index {last_row} outside the {} \
committed rows",
self.rows
)));
}
}
view.clear();
view.resize(self.rows, None);
for (row_idx, value) in store {
view[*row_idx] = Some(value);
}
arrays.push(convert_column_to_array(col, &view)?);
}
Ok(RecordBatch::try_new(schema, arrays)?)
}
}
#[cfg(test)]
#[path = "arrow_row_accumulator_tests.rs"]
mod arrow_row_accumulator_tests;