use std::sync::Mutex;
use rudb_common::{Error, Memory, Reservation, Result};
use rudb_pipeline::{Lease, Progress, Sink};
use rudb_vector::{Chunk, Selection};
use crate::buffer::Buffered;
#[derive(Debug)]
pub(crate) struct LimitPercent {
percent: f64,
offset: u64,
memory: Memory,
chunks: Mutex<Vec<Chunk>>,
charged: Mutex<Vec<Reservation>>,
held: Mutex<Reservation>,
out: Buffered,
}
#[derive(Debug)]
pub(crate) struct Gathered {
chunks: Vec<Chunk>,
charged: Reservation,
}
impl LimitPercent {
pub(crate) fn new(percent: f64, offset: u64, memory: &Memory) -> (Self, Buffered) {
let out = Buffered::new();
let limit = Self {
percent,
offset,
memory: memory.clone(),
chunks: Mutex::new(Vec::new()),
charged: Mutex::new(Vec::new()),
held: Mutex::new(memory.reservation()),
out: out.clone(),
};
(limit, out)
}
fn taken(&self, rows: u64) -> u64 {
(self.percent / 100.0 * rows as f64) as u64
}
}
impl Sink for LimitPercent {
type Local = Gathered;
fn local(&self) -> Gathered {
Gathered { chunks: Vec::new(), charged: self.memory.reservation() }
}
fn parallel(&self) -> bool {
false
}
fn sink(&self, chunk: &Chunk, local: &mut Gathered) -> Result<Progress> {
local.charged.grow(chunk.footprint() as u64)?;
local.chunks.push(chunk.clone());
Ok(Progress::More)
}
fn combine(&self, local: Gathered) -> Result<()> {
self.chunks.lock().map_err(poisoned)?.extend(local.chunks);
self.charged.lock().map_err(poisoned)?.push(local.charged);
Ok(())
}
fn finalize(&self, _threads: &Lease<'_>) -> Result<()> {
let gathered = std::mem::take(&mut *self.chunks.lock().map_err(poisoned)?);
let rows: u64 = gathered.iter().map(|chunk| chunk.len() as u64).sum();
let from = self.offset;
let to = from.saturating_add(self.taken(rows));
let kept = window(gathered, from, to)?;
let mut held = self.held.lock().map_err(poisoned)?;
held.grow(kept.iter().map(|chunk| chunk.footprint() as u64).sum())?;
self.out.fill(kept)?;
self.charged.lock().map_err(poisoned)?.clear();
Ok(())
}
}
fn window(chunks: Vec<Chunk>, from: u64, to: u64) -> Result<Vec<Chunk>> {
let mut kept = Vec::with_capacity(chunks.len());
let mut seen = 0u64;
for chunk in chunks {
let rows = chunk.len() as u64;
let start = seen;
seen += rows;
if seen <= from || start >= to {
continue;
}
if start >= from && seen <= to {
kept.push(chunk);
continue;
}
let first = from.saturating_sub(start);
let last = to.min(seen) - start;
let mut selection = Selection::with_capacity((last - first) as usize);
for row in first..last {
selection.push(row as usize);
}
kept.push(chunk.select(&selection)?);
}
Ok(kept)
}
fn poisoned<T>(_: T) -> Error {
Error::internal("a thread panicked while holding the rows a percentage limit is gathering")
}
#[cfg(test)]
mod tests {
use rudb_common::{LogicalType, Value};
use rudb_vector::{Chunk, Data, Vector};
use super::window;
fn chunk(values: &[i32]) -> Chunk {
let column = Vector::flat(LogicalType::Integer, Data::Int32(values.to_vec().into()))
.expect("integers are an i32 layout");
Chunk::new(vec![column]).expect("one column is one length")
}
fn numbers(chunks: &[Chunk]) -> Vec<i32> {
let mut found = Vec::new();
for chunk in chunks {
for row in 0..chunk.len() {
match chunk.value_at(row, 0) {
Value::Integer(number) => found.push(number),
other => panic!("{other:?}"),
}
}
}
found
}
#[test]
fn a_window_that_crosses_chunks_takes_the_rows_between_its_ends() {
let chunks = vec![chunk(&[0, 1, 2]), chunk(&[3, 4, 5]), chunk(&[6, 7, 8])];
let kept = window(chunks, 2, 7).expect("a window");
assert_eq!(numbers(&kept), vec![2, 3, 4, 5, 6]);
}
#[test]
fn a_window_over_everything_keeps_every_chunk_as_it_is() {
let chunks = vec![chunk(&[0, 1]), chunk(&[2, 3])];
let kept = window(chunks, 0, 4).expect("a window");
assert_eq!(kept.len(), 2);
assert_eq!(numbers(&kept), vec![0, 1, 2, 3]);
}
#[test]
fn a_window_that_is_empty_keeps_nothing() {
let chunks = vec![chunk(&[0, 1]), chunk(&[2, 3])];
assert!(window(chunks.clone(), 9, 9).expect("a window").is_empty());
assert!(window(chunks, 0, 0).expect("a window").is_empty());
}
}