use std::cmp::Ordering;
use std::sync::Mutex;
use rudb_common::{Error, Field, LogicalType, Memory, Reservation, Result, Session, Value};
use rudb_kernels::Accumulator;
use rudb_pipeline::{Progress, Sink};
use rudb_plan::{
ColumnBinding, Expr, ExprRef, Plan, Slice, SortKey, WindowBound, WindowExclude, WindowFrame,
WindowUnit,
};
use rudb_vector::Chunk;
use crate::buffer::Buffered;
use crate::prepared::{Prepared, Scratch};
use crate::rows;
use crate::schema::Schema;
use crate::sort::{Arrival, Place, compare};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Reads {
Frame,
Position(Ranking),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Ranking {
RowNumber,
Rank,
DenseRank,
PercentRank,
CumeDist,
Ntile,
}
impl Ranking {
fn of(name: &str) -> Option<Self> {
Some(match name {
"row_number" => Self::RowNumber,
"rank" => Self::Rank,
"dense_rank" | "rank_dense" => Self::DenseRank,
"percent_rank" => Self::PercentRank,
"cume_dist" => Self::CumeDist,
"ntile" => Self::Ntile,
_ => return None,
})
}
}
#[derive(Debug)]
struct Call {
name: String,
reads: Reads,
returns: LogicalType,
args_at: usize,
args: usize,
filter_at: Option<usize>,
distinct: bool,
ignore_nulls: bool,
}
type Windowed = (Vec<Value>, Vec<Value>, Arrival);
#[derive(Debug, Clone, Copy)]
struct Offsets {
start: Option<usize>,
end: Option<usize>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Written {
pub(crate) index: u32,
pub(crate) partition: Slice,
pub(crate) order: Slice,
pub(crate) frame: WindowFrame,
pub(crate) expressions: Slice,
}
#[derive(Debug)]
pub(crate) struct Window {
values: Prepared,
partitions: usize,
order: Vec<SortKey>,
sorting: Vec<SortKey>,
calls: Vec<Call>,
frame: WindowFrame,
offsets: Offsets,
types: Vec<LogicalType>,
schema: Schema,
memory: Memory,
rows: Mutex<Vec<Windowed>>,
charged: Mutex<Vec<Reservation>>,
held: Mutex<Reservation>,
out: Buffered,
}
#[derive(Debug)]
pub(crate) struct Gathered {
rows: Vec<Windowed>,
scratch: Scratch,
charged: Reservation,
place: Place,
}
impl Window {
#[must_use]
pub(crate) fn in_session(mut self, session: &Session) -> Self {
self.values = self.values.in_session(session);
self
}
pub(crate) fn schema(&self) -> &Schema {
&self.schema
}
pub(crate) fn new(
plan: &Plan,
input: &Schema,
written: &Written,
memory: &Memory,
) -> Result<(Self, Buffered)> {
let Written { index, partition, order, frame, expressions } = *written;
refuse_unanswerable(frame)?;
let order = plan.sort_key_list(order).to_vec();
let mut gathered: Vec<ExprRef> = plan.expr_list(partition).to_vec();
let partitions = gathered.len();
let mut sorting: Vec<SortKey> = gathered
.iter()
.map(|&expr| SortKey { expr, descending: false, nulls_first: false })
.collect();
sorting.extend(order.iter().copied());
gathered.extend(order.iter().map(|key| key.expr));
let mut calls = Vec::new();
for &expr in plan.expr_list(expressions) {
let Expr::Window { name, args, distinct, filter, ignore_nulls } = plan.expr(expr)
else {
return Err(Error::internal("a window node listing an expression that is not one"));
};
let arguments = plan.expr_list(*args).to_vec();
let args_at = gathered.len();
gathered.extend(arguments.iter().copied());
let filter_at = filter.map(|predicate| {
gathered.push(predicate);
gathered.len() - 1
});
let written = plan.string(*name);
calls.push(Call {
reads: Ranking::of(written).map_or(Reads::Frame, Reads::Position),
name: written.to_string(),
returns: plan.expr_type(expr).clone(),
args_at,
args: arguments.len(),
filter_at,
distinct: *distinct,
ignore_nulls: *ignore_nulls,
});
}
let offsets = Offsets {
start: distance(frame.start).map(|expr| {
gathered.push(expr);
gathered.len() - 1
}),
end: distance(frame.end).map(|expr| {
gathered.push(expr);
gathered.len() - 1
}),
};
let mut fields = input.fields().to_vec();
let mut bindings = input.bindings().to_vec();
for (at, call) in calls.iter().enumerate() {
fields.push(Field::new(call.name.clone(), call.returns.clone()));
let at = u32::try_from(at).expect("a window node this wide cannot be built");
bindings.push(ColumnBinding::new(index, at));
}
let schema = Schema::new(fields, bindings)?;
let mut types = input.types();
types.extend(calls.iter().map(|call| call.returns.clone()));
let out = Buffered::new();
let window = Self {
values: Prepared::new(plan, &gathered, input)?,
partitions,
order,
sorting,
calls,
frame,
offsets,
types,
schema,
memory: memory.clone(),
rows: Mutex::new(Vec::new()),
charged: Mutex::new(Vec::new()),
held: Mutex::new(memory.reservation()),
out: out.clone(),
};
Ok((window, out))
}
fn same_partition(&self, left: &Windowed, right: &Windowed) -> Result<bool> {
for at in 0..self.partitions {
if rudb_kernels::order_with_nulls(&left.0[at], &right.0[at], false)? != Ordering::Equal
{
return Ok(false);
}
}
Ok(true)
}
fn peers(&self, left: &Windowed, right: &Windowed) -> Result<bool> {
for at in 0..self.order.len() {
let at = self.partitions + at;
if rudb_kernels::order_with_nulls(&left.0[at], &right.0[at], false)? != Ordering::Equal
{
return Ok(false);
}
}
Ok(true)
}
}
fn distance(bound: WindowBound) -> Option<ExprRef> {
match bound {
WindowBound::Preceding(expr) | WindowBound::Following(expr) => Some(expr),
_ => None,
}
}
fn refuse_unanswerable(frame: WindowFrame) -> Result<()> {
if frame.unit != WindowUnit::Range {
return Ok(());
}
if distance(frame.start).is_some() || distance(frame.end).is_some() {
return Err(Error::not_implemented("a RANGE frame with an offset"));
}
Ok(())
}
impl Sink for Window {
type Local = Gathered;
fn local(&self) -> Gathered {
Gathered {
rows: Vec::new(),
scratch: self.values.scratch(),
charged: self.memory.reservation(),
place: Place::default(),
}
}
fn at(&self, morsel: &rudb_pipeline::Morsel, local: &mut Gathered) -> Result<()> {
local.place.start(morsel.index());
Ok(())
}
fn sink(&self, chunk: &Chunk, local: &mut Gathered) -> Result<Progress> {
let mut gathered = Vec::new();
self.values.evaluate(chunk, &mut local.scratch, &mut gathered)?;
let mut taken = 0;
for row in 0..chunk.len() {
let held: Vec<Value> =
gathered.iter().map(|column| column.try_value_at(row)).collect::<Result<_>>()?;
let values: Vec<Value> = (0..chunk.width())
.map(|column| chunk.try_value_at(row, column))
.collect::<Result<_>>()?;
taken += rows::footprint(&held) + rows::footprint(&values);
local.rows.push((held, values, local.place.of(row)));
}
local.place.past(chunk.len());
local.charged.grow(taken)?;
Ok(Progress::More)
}
fn combine(&self, local: Gathered) -> Result<()> {
let mut rows = self.rows.lock().map_err(poisoned)?;
rows.extend(local.rows);
self.charged.lock().map_err(poisoned)?.push(local.charged);
Ok(())
}
fn finalize(&self) -> Result<()> {
let mut gathered = std::mem::take(&mut *self.rows.lock().map_err(poisoned)?);
let keys = self.sorting.len();
let mut failure: Option<Error> = None;
gathered.sort_by(|left, right| {
let ordering = compare(&self.sorting, &left.0[..keys], &right.0[..keys], &mut failure);
match ordering {
Ordering::Equal => left.2.cmp(&right.2),
ordering => ordering,
}
});
if let Some(error) = failure {
return Err(error);
}
let mut answered: Vec<Vec<Value>> = Vec::with_capacity(gathered.len());
let mut start = 0;
while start < gathered.len() {
let mut end = start + 1;
while end < gathered.len() && self.same_partition(&gathered[start], &gathered[end])? {
end += 1;
}
self.over(&gathered[start..end], &mut answered)?;
start = end;
}
let mut held = self.held.lock().map_err(poisoned)?;
let chunks = rows::chunks(&self.types, &answered, &mut held)?;
self.out.fill(chunks)?;
self.charged.lock().map_err(poisoned)?.clear();
Ok(())
}
}
impl Window {
fn over(&self, rows: &[Windowed], answered: &mut Vec<Vec<Value>>) -> Result<()> {
let peers = self.peer_groups(rows)?;
for at in 0..rows.len() {
let frame = self.frame_of(rows, &peers, at)?;
let mut row = rows[at].1.clone();
for call in &self.calls {
row.push(self.answer(call, rows, &peers, at, frame.clone())?);
}
answered.push(row);
}
Ok(())
}
fn peer_groups(&self, rows: &[Windowed]) -> Result<Vec<usize>> {
let mut groups = Vec::with_capacity(rows.len());
let mut group = 0;
for at in 0..rows.len() {
if at > 0 && !self.peers(&rows[at - 1], &rows[at])? {
group += 1;
}
groups.push(group);
}
Ok(groups)
}
fn frame_of(
&self,
rows: &[Windowed],
peers: &[usize],
at: usize,
) -> Result<std::ops::Range<usize>> {
let last = rows.len();
let from = match self.frame.start {
WindowBound::UnboundedPreceding => 0,
WindowBound::CurrentRow => match self.frame.unit {
WindowUnit::Rows => at,
_ => first_of(peers, peers[at]),
},
WindowBound::Preceding(_) => {
self.away(rows, peers, at, self.offsets.start, true, false)?
}
WindowBound::Following(_) => {
self.away(rows, peers, at, self.offsets.start, false, false)?
}
WindowBound::UnboundedFollowing => {
return Err(Error::internal("a frame starting after every row"));
}
};
let to = match self.frame.end {
WindowBound::UnboundedFollowing => last,
WindowBound::CurrentRow => match self.frame.unit {
WindowUnit::Rows => at + 1,
_ => last_of(peers, peers[at]) + 1,
},
WindowBound::Preceding(_) => {
self.away(rows, peers, at, self.offsets.end, true, true)?
}
WindowBound::Following(_) => {
self.away(rows, peers, at, self.offsets.end, false, true)?
}
WindowBound::UnboundedPreceding => {
return Err(Error::internal("a frame ending before every row"));
}
};
Ok(from..to.min(last).max(from))
}
fn away(
&self,
rows: &[Windowed],
peers: &[usize],
at: usize,
column: Option<usize>,
back: bool,
after: bool,
) -> Result<usize> {
let offset = self.distance_at(rows, at, column, back)?;
let signed = if back { offset.saturating_neg() } else { offset };
let landed = |from: usize| -> Option<usize> {
let from = i64::try_from(from).unwrap_or(i64::MAX);
usize::try_from(from.saturating_add(signed)).ok()
};
Ok(match self.frame.unit {
WindowUnit::Rows => {
let Some(landed) = landed(at) else { return Ok(0) };
if after { landed.saturating_add(1) } else { landed }
}
_ => {
let Some(group) = landed(peers[at]) else { return Ok(0) };
if after {
peers.iter().rposition(|&held| held <= group).map_or(0, |end| end + 1)
} else {
peers.iter().position(|&held| held >= group).unwrap_or(rows.len())
}
}
})
}
fn distance_at(
&self,
rows: &[Windowed],
at: usize,
column: Option<usize>,
back: bool,
) -> Result<i64> {
let column =
column.ok_or_else(|| Error::internal("a frame distance the window did not gather"))?;
let value = &rows[at].0[column];
let named = || {
let unit = match self.frame.unit {
WindowUnit::Rows => "ROWS",
WindowUnit::Range => "RANGE",
WindowUnit::Groups => "GROUPS",
};
let end = if back { "PRECEDING" } else { "FOLLOWING" };
format!("Window {unit} {end} expression")
};
if value.is_null() {
return Err(Error::invalid_input(format!("{} cannot be NULL", named())));
}
value.as_i64().ok_or_else(|| Error::invalid_input(format!("{} must be a number", named())))
}
fn answer(
&self,
call: &Call,
rows: &[Windowed],
peers: &[usize],
at: usize,
frame: std::ops::Range<usize>,
) -> Result<Value> {
if let Reads::Position(ranking) = call.reads {
return ranked(ranking, call, rows, peers, at);
}
let mut accumulator = Accumulator::new(&call.name, &call.returns)?;
let mut seen: Vec<Vec<Value>> = Vec::new();
for row in frame {
if self.excluded(peers, at, row) {
continue;
}
if let Some(filter) = call.filter_at {
if rows[row].0[filter].as_bool() != Some(true) {
continue;
}
}
let args: Vec<Value> = rows[row].0[call.args_at..call.args_at + call.args].to_vec();
if call.ignore_nulls && args.iter().any(Value::is_null) {
continue;
}
if call.distinct {
if seen.contains(&args) {
continue;
}
seen.push(args.clone());
}
accumulator.update(&args)?;
}
accumulator.finish()
}
fn excluded(&self, peers: &[usize], at: usize, row: usize) -> bool {
match self.frame.exclude {
WindowExclude::NoOthers => false,
WindowExclude::CurrentRow => row == at,
WindowExclude::Group => peers[row] == peers[at],
WindowExclude::Ties => peers[row] == peers[at] && row != at,
}
}
}
fn ranked(
ranking: Ranking,
call: &Call,
rows: &[Windowed],
peers: &[usize],
at: usize,
) -> Result<Value> {
let total = rows.len();
let first = first_of(peers, peers[at]);
let last = last_of(peers, peers[at]);
let count = |held: usize| {
i64::try_from(held).map_err(|_| Error::internal("a partition longer than a BIGINT"))
};
Ok(match ranking {
Ranking::RowNumber => Value::BigInt(count(at + 1)?),
Ranking::Rank => Value::BigInt(count(first + 1)?),
Ranking::DenseRank => Value::BigInt(count(peers[at] + 1)?),
Ranking::PercentRank => {
Value::Double(if total <= 1 { 0.0 } else { first as f64 / (total - 1) as f64 })
}
Ranking::CumeDist => Value::Double((last + 1) as f64 / total as f64),
Ranking::Ntile => ntile(call, rows, at, total)?,
})
}
fn ntile(call: &Call, rows: &[Windowed], at: usize, total: usize) -> Result<Value> {
let written = &rows[at].0[call.args_at];
if written.is_null() {
return Ok(Value::Null);
}
let buckets = written
.as_i64()
.ok_or_else(|| Error::invalid_input("Argument for ntile must be a number"))?;
if buckets <= 0 {
return Err(Error::invalid_input("Argument for ntile must be greater than zero"));
}
let buckets = usize::try_from(buckets).unwrap_or(total).min(total.max(1));
let each = total / buckets;
let wide = total % buckets;
let bucket = if at < wide * (each + 1) {
at / (each + 1)
} else {
wide + (at - wide * (each + 1)) / each.max(1)
};
let bucket = i64::try_from(bucket + 1).map_err(|_| Error::internal("too many buckets"))?;
Ok(Value::BigInt(bucket))
}
fn first_of(peers: &[usize], group: usize) -> usize {
peers.iter().position(|&held| held == group).unwrap_or(0)
}
fn last_of(peers: &[usize], group: usize) -> usize {
peers.iter().rposition(|&held| held == group).unwrap_or(0)
}
fn poisoned<T>(_: std::sync::PoisonError<T>) -> Error {
Error::internal("a window lock a panicking thread left behind")
}