use std::sync::Mutex;
use rudb_common::{Error, Memory, Reservation, Result, Session};
use rudb_kernels::percentage;
use rudb_pipeline::{Lease, Progress, Sink};
use rudb_vector::{Chunk, Selection};
use crate::buffer::Buffered;
use crate::prepared::{Prepared, Scratch};
use crate::stream::Edge;
#[derive(Debug)]
pub(crate) enum Portion {
Percent(f64),
Read(Box<Prepared>),
}
#[derive(Debug)]
pub(crate) struct LimitPercent {
percent: Portion,
offset: Edge,
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 Portion {
fn scratch(&self) -> Scratch {
match self {
Self::Read(prepared) => prepared.scratch(),
Self::Percent(_) => Scratch::default(),
}
}
fn share(&self, chunk: &Chunk, scratch: &mut Scratch) -> Result<Option<f64>> {
let prepared = match self {
Self::Percent(percent) => return Ok(Some(*percent)),
Self::Read(prepared) => prepared,
};
let value = prepared.evaluate_one(chunk, scratch)?.value_at(0);
if value.is_null() {
return Ok(None);
}
let percent = percentage(&value)?;
if percent < 0.0 {
return Err(Error::binder(format!("Percentage value({percent:.6}) can't be negative")));
}
if percent > 100.0 || percent.is_nan() {
return Err(Error::out_of_range(
"Limit percent out of range, should be between 0% and 100%",
));
}
Ok(Some(percent))
}
}
impl LimitPercent {
pub(crate) fn new(
percent: Portion,
offset: Edge,
memory: &Memory,
session: &Session,
) -> (Self, Buffered) {
let out = Buffered::new();
let limit = Self {
percent: match percent {
Portion::Read(prepared) => Portion::Read(Box::new(prepared.in_session(session))),
settled => settled,
},
offset: offset.in_session(session),
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, share: Option<f64>, rows: u64) -> u64 {
match share {
Some(percent) => (percent / 100.0 * rows as f64) as u64,
None => rows,
}
}
fn skipped(&self, first: Option<&Chunk>) -> Result<u64> {
if let Edge::Rows(rows) = self.offset {
return Ok(rows);
}
let Some(first) = first else {
return Ok(0);
};
let mut scratch = self.offset.scratch();
Ok(self.offset.rows(first, &mut scratch, "OFFSET")?.unwrap_or(0))
}
fn share(&self, first: Option<&Chunk>) -> Result<Option<f64>> {
let Some(first) = first else {
return Ok(Some(0.0));
};
let mut scratch = self.percent.scratch();
self.percent.share(first, &mut scratch)
}
}
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 first = gathered.iter().find(|chunk| !chunk.is_empty());
let from = self.skipped(first)?;
let share = self.share(first)?;
let to = from.saturating_add(self.taken(share, 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());
}
}