use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Mutex, TryLockError};
use rudb_common::{
Error, Field, LogicalType, Memory, Reservation, Result, Spent, Stage, Value, stage,
};
use rudb_kernels::{Accumulator, NOWHERE, is_true, update_scattered};
use rudb_pipeline::{Progress, Sink};
use rudb_plan::{Expr, ExprRef, Plan, Slice};
use rudb_vector::{Chunk, VECTOR_SIZE, Vector};
use crate::buffer::Buffered;
use crate::key::{BigIntSet, Key, RowSet};
use crate::prepared::{Prepared, Scratch};
use crate::rows;
use crate::schema::Schema;
use crate::spill::{Reader, Spill};
use crate::table::{Probe, Table, Walk};
#[derive(Debug, Clone)]
struct Call {
name: String,
args: Vec<ExprRef>,
distinct: bool,
filter: Option<ExprRef>,
returns: LogicalType,
affine: Option<(usize, i64)>,
}
fn integer_constant(plan: &Plan, expr: ExprRef) -> Option<i64> {
let Expr::Constant(value) = *plan.expr(expr) else { return None };
let Value::Integer(offset) = *plan.value(value) else { return None };
Some(i64::from(offset))
}
fn mark_affine_sums(plan: &Plan, calls: &mut [Call]) {
for at in 0..calls.len() {
if calls[at].name != "sum" || calls[at].distinct || calls[at].filter.is_some() {
continue;
}
let [argument] = calls[at].args.as_slice() else { continue };
let Expr::Function { name, args } = *plan.expr(*argument) else { continue };
if plan.string(name) != "+" || plan.expr_type(*argument) != &LogicalType::Integer {
continue;
}
let [left, right] = plan.expr_list(args) else { continue };
let Expr::Cast { input: base, try_cast: false } = *plan.expr(*left) else { continue };
if plan.expr_type(base) != &LogicalType::SmallInt {
continue;
}
let Some(offset) = integer_constant(plan, *right) else { continue };
let source = (0..at).find(|&source| {
calls[source].name == "sum"
&& !calls[source].distinct
&& calls[source].filter.is_none()
&& calls[source].returns == LogicalType::HugeInt
&& calls[source].args.as_slice() == [base]
});
if let Some(source) = source {
calls[at].affine = Some((source, offset));
}
}
}
#[derive(Debug)]
pub(crate) struct Aggregate<'a> {
plan: &'a Plan,
keys: Vec<ExprRef>,
groups: Vec<ExprRef>,
constants: Vec<Option<Value>>,
calls: Vec<Call>,
inputs: Prepared,
schema: Schema,
alone: bool,
sets: bool,
by_vector: Vec<bool>,
every: bool,
count_only: bool,
max_groups: Option<usize>,
memory: Memory,
built: Mutex<Built>,
merged: Vec<Mutex<Partition>>,
started: AtomicUsize,
locally: AtomicBool,
out: Buffered,
}
#[derive(Debug)]
struct Built {
chunks: Vec<Chunk>,
held: Vec<Reservation>,
instances: usize,
partitioning: bool,
local: bool,
}
#[derive(Debug, Default)]
struct Partition {
table: Option<Building>,
carried: Option<Building>,
pending: Vec<Building>,
}
const RADIX_PARTITIONS: usize = 16;
const PARTITION_FROM: usize = 4_096;
impl<'a> Aggregate<'a> {
pub(crate) fn new(
plan: &'a Plan,
input: &Schema,
index: u32,
groups: Slice,
aggregates: Slice,
memory: &Memory,
) -> Result<(Self, Buffered)> {
let input_schema = input.clone();
let groups: Vec<ExprRef> = plan.expr_list(groups).to_vec();
let constants: Vec<Option<Value>> = groups
.iter()
.map(|&group| match *plan.expr(group) {
Expr::Constant(value) => Some(plan.value(value).clone()),
_ => None,
})
.collect();
let keys: Vec<ExprRef> = groups
.iter()
.zip(&constants)
.filter_map(|(&group, value)| value.is_none().then_some(group))
.collect();
let mut calls = Vec::new();
for &reference in plan.expr_list(aggregates) {
let Expr::Aggregate { name, args, distinct, filter } = *plan.expr(reference) else {
return Err(Error::internal(format!(
"expression {reference} is in the aggregate list of an Aggregate and is not an aggregate"
)));
};
calls.push(Call {
name: plan.string(name).to_string(),
args: plan.expr_list(args).to_vec(),
distinct,
filter,
returns: plan.expr_type(reference).clone(),
affine: None,
});
}
if groups.is_empty() {
mark_affine_sums(plan, &mut calls);
}
let mut fields = Vec::with_capacity(groups.len() + calls.len());
for (at, &group) in groups.iter().enumerate() {
fields.push(Field::new(
group_name(plan, group, &input_schema, at),
plan.expr_type(group).clone(),
));
}
for call in &calls {
fields.push(Field::new(call.name.clone(), call.returns.clone()));
}
let schema = Schema::numbered(fields, index);
let mut inputs = keys.clone();
for call in &calls {
if call.affine.is_none() {
inputs.extend_from_slice(&call.args);
}
inputs.extend(call.filter);
}
let inputs = Prepared::shared(plan, &inputs, &input_schema)?;
let alone = groups.is_empty();
let by_vector: Vec<bool> =
calls.iter().map(|call| alone && !call.distinct && call.filter.is_none()).collect();
let out = Buffered::new();
let aggregate = Self {
plan,
keys,
constants,
alone,
sets: calls.iter().any(|call| call.distinct),
every: by_vector.iter().all(|&yes| yes),
count_only: !alone
&& calls.len() == 1
&& calls[0].name == "count_star"
&& !calls[0].distinct
&& calls[0].filter.is_none(),
max_groups: None,
by_vector,
groups,
calls,
inputs,
schema,
memory: memory.clone(),
built: Mutex::new(Built {
chunks: Vec::new(),
held: Vec::new(),
instances: 0,
partitioning: false,
local: true,
}),
merged: (0..RADIX_PARTITIONS).map(|_| Mutex::new(Partition::default())).collect(),
started: AtomicUsize::new(0),
locally: AtomicBool::new(true),
out: out.clone(),
};
Ok((aggregate, out))
}
pub(crate) fn limit_groups(mut self, max_groups: usize) -> Self {
self.max_groups = Some(max_groups);
self
}
pub(crate) fn schema(&self) -> &Schema {
&self.schema
}
fn read(&self, chunk: &Chunk, scratch: &mut Scratch) -> Result<Rows> {
let rows = chunk.len();
let mut evaluated = Vec::new();
self.inputs.evaluate(chunk, scratch, &mut evaluated)?;
let mut values = evaluated.into_iter();
let keys = values.by_ref().take(self.keys.len()).collect();
let mut arguments = Vec::with_capacity(self.calls.len());
let mut filters = Vec::with_capacity(self.calls.len());
for call in &self.calls {
arguments.push(if call.affine.is_none() {
values.by_ref().take(call.args.len()).collect()
} else {
Vec::new()
});
filters
.push(call.filter.map(|_| values.next().expect("a prepared filter has a value")));
}
Ok(Rows { keys, arguments, filters, rows })
}
fn again(
&self,
file: &mut Spill,
carried: Option<Building>,
chunks: &mut Vec<Chunk>,
held: &mut Reservation,
) -> Result<Option<Spill>> {
let mut spilled = Spilled::new(file.read()?, self.spilled_types());
let mut local = self.start();
if let Some(error) = local.failure.take() {
return Err(error);
}
if let Some(carried) = carried {
self.merge(carried, &mut local)?;
}
while let Some(rows) = spilled.next(self)? {
let timing = stage::Timing::start(Stage::Fold);
let folded = self.fold(&rows, &mut local, None);
timing.stop(0);
folded?;
}
self.finish(local, chunks, held)
}
fn start(&self) -> Building {
let calls = self.calls.len();
let mut local = Building {
scratch: self.memory.reservation(),
containers: self.memory.reservation(),
charged: 0,
charged_keys: 0,
table: Table::new(
&self.keys.iter().map(|&key| self.plan.expr_type(key).clone()).collect::<Vec<_>>(),
),
states: Vec::new(),
counts: Vec::new(),
seen: Vec::new(),
groups: 0,
given: vec![Key(Vec::new()); calls],
hashes: Vec::new(),
slots: Vec::new(),
walk: Walk::default(),
kept: Vec::new(),
affine_rows: vec![0; calls],
over: None,
away: Vec::new(),
failure: None,
};
if self.alone {
local.groups = 1;
if let Err(error) = self.fresh(&mut local.states, &mut local.counts) {
local.failure = Some(error);
}
if self.sets {
self.fresh_seen(&mut local.seen);
}
}
local
}
fn fold(
&self,
seen_rows: &Rows,
local: &mut Building,
prehashed: Option<&[u64]>,
) -> Result<()> {
let Building {
scratch,
containers,
charged,
charged_keys,
table,
states,
counts,
seen,
groups,
given,
hashes,
slots,
walk,
kept,
affine_rows,
over,
away,
failure: _,
} = local;
let calls = self.calls.len();
let alone = self.alone;
let Rows { keys, arguments, filters, rows: length } = seen_rows;
let mut aside = 0;
for at in 0..calls {
if self.calls[at].affine.is_some() {
continue;
}
if self.by_vector[at] {
states[at].update_run(&arguments[at], *length)?;
if self.calls.iter().any(|call| call.affine.is_some_and(|(source, _)| source == at))
{
affine_rows[at] +=
i64::try_from(arguments[at][0].validity().count_valid(*length))
.map_err(|_| Error::out_of_range("too many rows in an aggregate"))?;
}
}
}
if alone && self.every {
return Ok(());
}
if !alone {
match prehashed {
Some(prehashed) => {
hashes.clear();
hashes.extend_from_slice(prehashed);
}
None => crate::table::hash(keys, *length, hashes),
}
}
slots.clear();
slots.resize(*length, if alone { 0 } else { NOWHERE });
let mut from = 0;
while !alone && from < *length {
let upto = (from + crate::table::BATCH).min(*length);
table.probe_run(hashes, keys, from, upto, slots, walk);
from = upto;
for &row in walk.pending() {
let bucket = match table.probe(hashes[row], keys, row) {
Probe::Found(slot) => {
slots[row] = slot;
continue;
}
Probe::Vacant(bucket) => bucket,
};
if self.max_groups.is_some_and(|limit| table.len() >= limit) {
continue;
}
if let Some(file) = over.as_mut() {
put_away(file, seen_rows, row, away)?;
continue;
}
slots[row] = table.insert(bucket, hashes[row], keys, row)?;
*groups = table.len();
self.fresh(states, counts)?;
if self.sets {
self.fresh_seen(seen);
}
}
}
if self.count_only {
for &slot in slots.iter() {
if slot != NOWHERE {
counts[slot] += 1;
}
}
}
for (at, call) in self.calls.iter().enumerate() {
if self.count_only {
break;
}
if self.by_vector[at] || call.affine.is_some() {
continue;
}
if call.distinct {
aside += self.distinct(states, seen, seen_rows, slots, at, given)?;
continue;
}
let picked = match &filters[at] {
None => &*slots,
Some(flags) => {
kept.clear();
kept.extend(slots.iter().enumerate().map(|(row, &slot)| {
if slot != NOWHERE && is_true(&flags.value_at(row)) {
slot
} else {
NOWHERE
}
}));
&*kept
}
};
update_scattered(states, picked, calls, at, arguments[at].first(), *length)?;
}
rows::capacity(table.owned(), charged_keys, scratch)?;
containers.grow(aside)?;
let now = tables(table, states, counts, seen);
rows::capacity(now, charged, containers)?;
match over.as_ref() {
None if !alone && crowded(&self.memory) => {
*over = Some(Spill::new("aggregate", self.spilled_types())?);
}
Some(file) => hopeless(file, *groups)?,
None => {}
}
Ok(())
}
fn finish(
&self,
local: Building,
chunks: &mut Vec<Chunk>,
held: &mut Reservation,
) -> Result<Option<Spill>> {
let timing = stage::Timing::start(Stage::Emit);
let finished = self.finishing(local, chunks, held);
timing.stop(0);
finished
}
fn finishing(
&self,
local: Building,
chunks: &mut Vec<Chunk>,
held: &mut Reservation,
) -> Result<Option<Spill>> {
let Building {
mut scratch,
mut containers,
table,
states,
counts,
seen,
groups,
affine_rows,
over,
..
} = local;
let calls = self.calls.len();
drop(seen);
let alive = table.footprint()
+ width_of(states.capacity() * size_of::<Accumulator>())
+ width_of(counts.capacity() * size_of::<i64>());
containers.shrink(containers.bytes().saturating_sub(alive));
let types = self.schema.types();
let width = self.groups.len();
scratch.grow(width_of(VECTOR_SIZE.min(groups) * size_of::<Value>()))?;
let mut results: Vec<Value> = Vec::new();
for start in (0..groups).step_by(VECTOR_SIZE) {
let end = (start + VECTOR_SIZE).min(groups);
let mut columns = Vec::with_capacity(width + calls);
let mut key = 0;
for (at, ty) in types.iter().take(width).enumerate() {
if let Some(value) = &self.constants[at] {
columns.push(Vector::constant(ty.clone(), value.clone(), end - start));
} else {
columns.push(table.column(key, ty, start..end)?);
key += 1;
}
}
for (at, ty) in types.iter().skip(width).enumerate() {
let mut taken = 0;
results.clear();
for slot in start..end {
let value = if self.count_only {
Ok(Value::BigInt(counts[slot]))
} else {
match self.calls[at].affine {
Some((source, offset)) => states[slot * calls + source]
.finish_offset(offset, affine_rows[source]),
None => states[slot * calls + at].finish(),
}
}?;
taken += rows::owned(&value);
results.push(value);
}
scratch.grow(taken)?;
columns.push(Vector::from_values(ty.clone(), &results)?);
scratch.shrink(taken);
}
let chunk = Chunk::with_rows(columns, end - start)?;
held.grow(width_of(chunk.footprint()))?;
chunks.push(chunk);
}
drop(results);
drop(states);
drop(counts);
drop(table);
containers.release();
match over {
Some(file) if groups == 0 && file.rows() > 0 => Err(Error::out_of_memory(format!(
"the memory limit does not leave room for a single group of this aggregate, \
{} rows and {} bytes went to a spill file and none of them could be finished",
file.rows(),
file.bytes()
))),
Some(file) if file.rows() > 0 => Ok(Some(file)),
_ => Ok(None),
}
}
fn merge(&self, from: Building, into: &mut Building) -> Result<()> {
let timing = stage::Timing::start(Stage::Merge);
let merged = self.merging(from, into);
timing.stop(0);
merged
}
fn merging(&self, from: Building, into: &mut Building) -> Result<()> {
if from.over.is_some() || into.over.is_some() {
return Err(Error::internal(
"two tables of an aggregate were merged with a spill file between them, where a \
key can be in one table and in the other's file at once, which the callers avoid \
by partitioning instead",
));
}
let Building {
scratch,
containers,
table: source,
states: taken,
counts: tallies,
seen: mut watched,
groups: found,
affine_rows: counted,
..
} = from;
let calls = self.calls.len();
for (at, rows) in counted.iter().enumerate() {
into.affine_rows[at] += rows;
}
let distinct: Vec<bool> = self.calls.iter().map(|call| call.distinct).collect();
let mut coming = Folding {
count_only: self.count_only,
calls,
distinct: &distinct,
taken: &taken,
tallies: &tallies,
watched: &mut watched,
};
let mut aside = 0;
if self.alone {
aside += merge_slot(&mut coming, 0, 0, into)?;
} else {
let types: Vec<LogicalType> =
self.keys.iter().map(|&key| self.plan.expr_type(key).clone()).collect();
let mut run: Vec<usize> = Vec::with_capacity(VECTOR_SIZE);
for start in (0..found).step_by(VECTOR_SIZE) {
let end = (start + VECTOR_SIZE).min(found);
let mut keys = Vec::with_capacity(types.len());
for (at, ty) in types.iter().enumerate() {
keys.push(source.column(at, ty, start..end)?);
}
run.clear();
run.extend(start..end);
aside += self.fold_slots(&mut coming, &source, &keys, start, &run, into)?;
}
}
into.containers.grow(aside)?;
drop(watched);
drop(taken);
drop(tallies);
drop(source);
drop(scratch);
drop(containers);
rows::capacity(into.table.owned(), &mut into.charged_keys, &mut into.scratch)?;
let now = tables(&into.table, &into.states, &into.counts, &into.seen);
rows::capacity(now, &mut into.charged, &mut into.containers)
}
fn fold_slots(
&self,
coming: &mut Folding<'_>,
source: &Table,
keys: &[Vector],
start: usize,
slots: &[usize],
into: &mut Building,
) -> Result<u64> {
let mut aside = 0;
for &slot in slots {
let row = slot - start;
let hash = source.hash_of(slot);
let target = match into.table.probe(hash, keys, row) {
Probe::Found(target) => target,
Probe::Vacant(bucket) => {
if self.max_groups.is_some_and(|limit| into.table.len() >= limit) {
continue;
}
let target = into.table.insert(bucket, hash, keys, row)?;
into.groups = into.table.len();
self.fresh(&mut into.states, &mut into.counts)?;
if self.sets {
self.fresh_seen(&mut into.seen);
}
target
}
};
aside += merge_slot(coming, slot, target, into)?;
}
Ok(aside)
}
fn scatter(&self, from: Building, spin: usize) -> Result<()> {
let timing = stage::Timing::start(Stage::Scatter);
let scattered = self.scattering(from, spin);
timing.stop(0);
scattered
}
fn scattering(&self, from: Building, spin: usize) -> Result<()> {
debug_assert!(from.over.is_none(), "a spill file is drained by hand_over, not scattered");
let Building {
scratch,
containers,
table: source,
states: taken,
counts: tallies,
seen: mut watched,
groups: found,
..
} = from;
let calls = self.calls.len();
let distinct: Vec<bool> = self.calls.iter().map(|call| call.distinct).collect();
let mut coming = Folding {
count_only: self.count_only,
calls,
distinct: &distinct,
taken: &taken,
tallies: &tallies,
watched: &mut watched,
};
let types: Vec<LogicalType> =
self.keys.iter().map(|&key| self.plan.expr_type(key).clone()).collect();
let shift = u64::BITS - RADIX_PARTITIONS.ilog2();
let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); RADIX_PARTITIONS];
let mut here: Vec<usize> = Vec::new();
let mut late: Vec<usize> = Vec::new();
for start in (0..found).step_by(VECTOR_SIZE) {
let end = (start + VECTOR_SIZE).min(found);
let mut keys = Vec::with_capacity(types.len());
for (at, ty) in types.iter().enumerate() {
keys.push(source.column(at, ty, start..end)?);
}
for bucket in &mut buckets {
bucket.clear();
}
for slot in start..end {
buckets[(source.hash_of(slot) >> shift) as usize].push(slot);
}
for step in 0..RADIX_PARTITIONS {
let at = (step + spin) % RADIX_PARTITIONS;
if buckets[at].is_empty() {
continue;
}
let mut held = self.merged[at].lock().map_err(poisoned)?;
let Partition { table, carried, .. } = &mut *held;
let into = table.get_or_insert_with(|| self.start());
if into.over.is_none() {
let grown =
self.fold_slots(&mut coming, &source, &keys, start, &buckets[at], into)?;
charge(into, grown)?;
continue;
}
here.clear();
late.clear();
for &slot in &buckets[at] {
match into.table.probe(source.hash_of(slot), &keys, slot - start) {
Probe::Found(_) => here.push(slot),
Probe::Vacant(_) => late.push(slot),
}
}
let grown = self.fold_slots(&mut coming, &source, &keys, start, &here, into)?;
charge(into, grown)?;
let waiting = carried.get_or_insert_with(|| self.start());
let grown = self.fold_slots(&mut coming, &source, &keys, start, &late, waiting)?;
charge(waiting, grown)?;
}
}
drop(watched);
drop(taken);
drop(tallies);
drop(source);
drop(scratch);
drop(containers);
Ok(())
}
fn ought_to_partition(&self, table: &Building) -> bool {
!self.alone
&& self.max_groups.is_none()
&& self.started.load(Ordering::Relaxed) > 1
&& (table.groups >= PARTITION_FROM || crowded(&self.memory))
}
fn begin_partitioning(
&self,
spreading: &mut Spreading,
own: &mut [Option<Building>],
) -> Result<()> {
let mut built = self.built.lock().map_err(poisoned)?;
if built.partitioning {
return Ok(());
}
built.partitioning = true;
let seeded = self.merged[0].lock().map_err(poisoned)?.table.take();
drop(built);
if !self.worth_local() {
self.give_up_local(spreading)?;
}
match seeded {
Some(seeded) => self.hand(seeded, spreading, own),
None => Ok(()),
}
}
fn hand(
&self,
from: Building,
spreading: &mut Spreading,
own: &mut [Option<Building>],
) -> Result<()> {
if from.over.is_some() || crowded(&self.memory) {
self.give_up_local(spreading)?;
self.hand_all(spreading, own)?;
}
if self.locally.load(Ordering::Relaxed) {
return self.scatter_own(from, own);
}
self.hand_over(from, spreading)
}
fn hand_all(&self, spreading: &mut Spreading, own: &mut [Option<Building>]) -> Result<()> {
for held in own.iter_mut() {
let Some(table) = held.take() else { continue };
self.hand_over(table, spreading)?;
}
Ok(())
}
fn still_local(&self, spreading: &mut Spreading, own: &mut [Option<Building>]) -> Result<bool> {
let spilled = own.iter().flatten().any(|table| table.over.is_some());
if !spilled && !crowded(&self.memory) && self.room_for_local(own) {
return Ok(true);
}
self.give_up_local(spreading)?;
self.hand_all(spreading, own)?;
Ok(false)
}
fn worth_local(&self) -> bool {
let Some(limit) = self.memory.limit() else { return true };
let instances = self.started.load(Ordering::Relaxed).max(self.merged.len()) as u64;
self.memory.used().saturating_mul(instances) < limit / 2
}
fn room_for_local(&self, own: &[Option<Building>]) -> bool {
let Some(limit) = self.memory.limit() else { return true };
let mine: u64 = own
.iter()
.flatten()
.map(|table| table.scratch.bytes() + table.containers.bytes())
.sum();
let instances = self.started.load(Ordering::Relaxed) as u64;
mine.saturating_mul(instances) < limit / 4
}
fn give_up_local(&self, spreading: &mut Spreading) -> Result<()> {
let mut built = self.built.lock().map_err(poisoned)?;
if !built.local {
return Ok(());
}
built.local = false;
self.locally.store(false, Ordering::Relaxed);
let mut handed = Vec::new();
for partition in &self.merged {
handed.append(&mut partition.lock().map_err(poisoned)?.pending);
}
drop(built);
for table in handed {
self.hand_over(table, spreading)?;
}
Ok(())
}
fn deposit(&self, own: &mut [Option<Building>], spreading: &mut Spreading) -> Result<()> {
if own.iter().flatten().next().is_none() {
return Ok(());
}
let built = self.built.lock().map_err(poisoned)?;
if built.local {
for (at, held) in own.iter_mut().enumerate() {
let Some(table) = held.take() else { continue };
self.merged[at].lock().map_err(poisoned)?.pending.push(table);
}
return Ok(());
}
drop(built);
self.hand_all(spreading, own)
}
fn hand_over(&self, mut from: Building, spreading: &mut Spreading) -> Result<()> {
let leftover = from.over.take();
self.scatter(from, spreading.spin)?;
let Some(mut file) = leftover else { return Ok(()) };
let mut spilled = Spilled::new(file.read()?, self.spilled_types());
while let Some(rows) = spilled.next(self)? {
self.spread(&rows, spreading)?;
}
Ok(())
}
fn split(&self, rows: &Rows, spreading: &mut Spreading) -> Result<Vec<Option<Rows>>> {
let Spreading { hashes, picks, keyed, spin, .. } = spreading;
crate::table::hash(&rows.keys, rows.rows, hashes);
for pick in picks.iter_mut() {
pick.clear();
}
for hashed in keyed.iter_mut() {
hashed.clear();
}
let shift = u64::BITS - RADIX_PARTITIONS.ilog2();
for (row, &hash) in hashes.iter().enumerate() {
let partition = (hash >> shift) as usize;
picks[partition].push(row as u32);
keyed[partition].push(hash);
}
let mut ready: Vec<Option<Rows>> = Vec::with_capacity(RADIX_PARTITIONS);
for pick in picks.iter() {
ready.push(if pick.is_empty() { None } else { Some(rows.gather(pick)?) });
}
*spin = (*spin + 1) % RADIX_PARTITIONS;
Ok(ready)
}
fn spread_own(
&self,
rows: &Rows,
spreading: &mut Spreading,
own: &mut [Option<Building>],
) -> Result<()> {
let timing = stage::Timing::start(Stage::Fold);
let spread = self.spreading_own(rows, spreading, own);
timing.stop(0);
spread
}
fn spreading_own(
&self,
rows: &Rows,
spreading: &mut Spreading,
own: &mut [Option<Building>],
) -> Result<()> {
let ready = self.split(rows, spreading)?;
for (partition, selected) in ready.iter().enumerate() {
let Some(selected) = selected else { continue };
let table = own[partition].get_or_insert_with(|| self.start());
if let Some(error) = table.failure.take() {
return Err(error);
}
self.fold(selected, table, Some(&spreading.keyed[partition]))?;
}
Ok(())
}
fn scatter_own(&self, from: Building, own: &mut [Option<Building>]) -> Result<()> {
let timing = stage::Timing::start(Stage::Scatter);
let scattered = self.scattering_own(from, own);
timing.stop(0);
scattered
}
fn scattering_own(&self, from: Building, own: &mut [Option<Building>]) -> Result<()> {
debug_assert!(from.over.is_none(), "a spilled table is never scattered locally");
let Building {
scratch,
containers,
table: source,
states: taken,
counts: tallies,
seen: mut watched,
groups: found,
..
} = from;
let calls = self.calls.len();
let distinct: Vec<bool> = self.calls.iter().map(|call| call.distinct).collect();
let mut coming = Folding {
count_only: self.count_only,
calls,
distinct: &distinct,
taken: &taken,
tallies: &tallies,
watched: &mut watched,
};
let types: Vec<LogicalType> =
self.keys.iter().map(|&key| self.plan.expr_type(key).clone()).collect();
let shift = u64::BITS - RADIX_PARTITIONS.ilog2();
let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); RADIX_PARTITIONS];
for start in (0..found).step_by(VECTOR_SIZE) {
let end = (start + VECTOR_SIZE).min(found);
let mut keys = Vec::with_capacity(types.len());
for (at, ty) in types.iter().enumerate() {
keys.push(source.column(at, ty, start..end)?);
}
for bucket in &mut buckets {
bucket.clear();
}
for slot in start..end {
buckets[(source.hash_of(slot) >> shift) as usize].push(slot);
}
for (at, bucket) in buckets.iter().enumerate() {
if bucket.is_empty() {
continue;
}
let into = own[at].get_or_insert_with(|| self.start());
let grown = self.fold_slots(&mut coming, &source, &keys, start, bucket, into)?;
charge(into, grown)?;
}
}
drop(watched);
drop(taken);
drop(tallies);
drop(source);
drop(scratch);
drop(containers);
Ok(())
}
fn spread(&self, rows: &Rows, spreading: &mut Spreading) -> Result<()> {
let timing = stage::Timing::start(Stage::Fold);
let spread = self.spreading(rows, spreading);
timing.stop(0);
spread
}
fn spreading(&self, rows: &Rows, spreading: &mut Spreading) -> Result<()> {
let ready = self.split(rows, spreading)?;
let Spreading { keyed, spin, waiting, .. } = spreading;
let spin = &*spin;
waiting.clear();
for step in 0..RADIX_PARTITIONS {
let partition = (step + *spin) % RADIX_PARTITIONS;
let Some(selected) = &ready[partition] else { continue };
match self.merged[partition].try_lock() {
Ok(mut held) => {
let table = held.table.get_or_insert_with(|| self.start());
self.fold(selected, table, Some(&keyed[partition]))?;
}
Err(TryLockError::WouldBlock) => waiting.push(partition),
Err(TryLockError::Poisoned(error)) => return Err(poisoned(error)),
}
}
for &partition in waiting.iter() {
let selected =
ready[partition].as_ref().expect("only a filled partition was put aside");
let mut held = self.merged[partition].lock().map_err(poisoned)?;
let table = held.table.get_or_insert_with(|| self.start());
self.fold(selected, table, Some(&keyed[partition]))?;
}
Ok(())
}
fn distinct(
&self,
states: &mut [Accumulator],
seen: &mut [DistinctSet],
rows: &Rows,
slots: &[usize],
at: usize,
given: &mut [Key],
) -> Result<u64> {
let calls = self.calls.len();
let mut aside = 0;
for (row, &slot) in slots.iter().enumerate() {
if slot == NOWHERE {
continue;
}
if let Some(flags) = &rows.filters[at] {
if !is_true(&flags.value_at(row)) {
continue;
}
}
if let (DistinctSet::BigInt(set), [column]) =
(&mut seen[slot * calls + at], rows.arguments[at].as_slice())
{
match column.value_at(row) {
Value::Null => continue,
Value::BigInt(value) => {
if set.insert(value) {
aside += width_of(size_of::<i64>() * 2);
states[slot * calls + at].update(&[Value::BigInt(value)])?;
}
continue;
}
value => {
return Err(Error::internal(format!(
"a BIGINT distinct set was given {value:?}"
)));
}
}
}
let args = &mut given[at];
fill(args, &rows.arguments[at], row);
let DistinctSet::Row(set) = &mut seen[slot * calls + at] else {
return Err(Error::internal("a distinct set did not match its argument"));
};
if set.contains(args) {
continue;
}
let stored = args.clone();
aside += rows::footprint(&stored.0);
set.insert(stored);
states[slot * calls + at].update(&args.0)?;
}
Ok(aside)
}
fn fresh(&self, states: &mut Vec<Accumulator>, counts: &mut Vec<i64>) -> Result<()> {
if self.count_only {
counts.push(0);
return Ok(());
}
for call in &self.calls {
states.push(Accumulator::new(&call.name, &call.returns)?);
}
Ok(())
}
fn fresh_seen(&self, seen: &mut Vec<DistinctSet>) {
for call in &self.calls {
let big_int = call.distinct
&& call.args.len() == 1
&& self.plan.expr_type(call.args[0]) == &LogicalType::BigInt;
if big_int {
seen.push(DistinctSet::BigInt(BigIntSet::default()));
} else {
seen.push(DistinctSet::Row(RowSet::default()));
}
}
}
fn spilled_types(&self) -> Vec<LogicalType> {
let mut types = Vec::new();
for &group in &self.keys {
types.push(self.plan.expr_type(group).clone());
}
for call in &self.calls {
for &argument in &call.args {
types.push(self.plan.expr_type(argument).clone());
}
}
for call in &self.calls {
if let Some(filter) = call.filter {
types.push(self.plan.expr_type(filter).clone());
}
}
types
}
}
struct Rows {
keys: Vec<Vector>,
arguments: Vec<Vec<Vector>>,
filters: Vec<Option<Vector>>,
rows: usize,
}
impl Rows {
fn width(&self) -> usize {
self.keys.len()
+ self.arguments.iter().map(Vec::len).sum::<usize>()
+ self.filters.iter().flatten().count()
}
fn gather(&self, rows: &[u32]) -> Result<Self> {
Ok(Self {
keys: self.keys.iter().map(|column| column.gather(rows)).collect::<Result<_>>()?,
arguments: self
.arguments
.iter()
.map(|arguments| {
arguments.iter().map(|column| column.gather(rows)).collect::<Result<_>>()
})
.collect::<Result<_>>()?,
filters: self
.filters
.iter()
.map(|filter| filter.as_ref().map(|column| column.gather(rows)).transpose())
.collect::<Result<_>>()?,
rows: rows.len(),
})
}
}
#[derive(Debug)]
pub(crate) struct Partitioned {
single: Option<Building>,
expressions: Scratch,
spreading: Spreading,
own: Vec<Option<Building>>,
}
#[derive(Debug)]
struct Spreading {
hashes: Vec<u64>,
picks: Vec<Vec<u32>>,
keyed: Vec<Vec<u64>>,
spin: usize,
waiting: Vec<usize>,
}
impl Spreading {
fn new() -> Self {
Self {
hashes: Vec::new(),
picks: vec![Vec::new(); RADIX_PARTITIONS],
keyed: vec![Vec::new(); RADIX_PARTITIONS],
spin: 0,
waiting: Vec::new(),
}
}
}
#[derive(Debug)]
pub(crate) struct Building {
scratch: Reservation,
containers: Reservation,
charged: u64,
charged_keys: u64,
table: Table,
states: Vec<Accumulator>,
counts: Vec<i64>,
seen: Vec<DistinctSet>,
groups: usize,
given: Vec<Key>,
hashes: Vec<u64>,
slots: Vec<usize>,
walk: Walk,
kept: Vec<usize>,
affine_rows: Vec<i64>,
over: Option<Spill>,
away: Vec<Value>,
failure: Option<Error>,
}
struct Spilled<'s> {
reader: Reader<'s>,
types: Vec<LogicalType>,
row: Vec<Value>,
columns: Vec<Vec<Value>>,
}
#[derive(Debug)]
enum DistinctSet {
BigInt(BigIntSet),
Row(RowSet),
}
impl<'s> Spilled<'s> {
fn new(reader: Reader<'s>, types: Vec<LogicalType>) -> Self {
let columns = vec![Vec::new(); types.len()];
Self { reader, types, row: Vec::new(), columns }
}
fn next(&mut self, pass: &Aggregate<'_>) -> Result<Option<Rows>> {
let Self { reader, types, row, columns } = self;
for column in columns.iter_mut() {
column.clear();
}
let mut rows = 0;
while rows < VECTOR_SIZE && reader.next_into(row)? {
for (at, value) in row.iter_mut().enumerate() {
columns[at].push(std::mem::replace(value, Value::Null));
}
rows += 1;
}
if rows == 0 {
return Ok(None);
}
let mut built = Vec::with_capacity(columns.len());
for (values, ty) in columns.iter().zip(&*types) {
built.push(Vector::from_values(ty.clone(), values)?);
}
let mut taking = built.into_iter();
let keys: Vec<Vector> = taking.by_ref().take(pass.keys.len()).collect();
let mut arguments = Vec::with_capacity(pass.calls.len());
for call in &pass.calls {
arguments.push(taking.by_ref().take(call.args.len()).collect());
}
let mut filters = Vec::with_capacity(pass.calls.len());
for call in &pass.calls {
filters.push(if call.filter.is_some() { taking.next() } else { None });
}
Ok(Some(Rows { keys, arguments, filters, rows }))
}
}
fn put_away(file: &mut Spill, seen: &Rows, row: usize, away: &mut Vec<Value>) -> Result<()> {
let columns = seen
.keys
.iter()
.chain(seen.arguments.iter().flatten())
.chain(seen.filters.iter().flatten());
away.truncate(seen.width());
for (at, column) in columns.enumerate() {
match away.get_mut(at) {
Some(slot) => set(slot, column, row),
None => away.push(column.value_at(row)),
}
}
file.write(away)
}
const PASSES: u64 = 64;
fn hopeless(file: &Spill, groups: usize) -> Result<()> {
let left = file.rows() / width_of(groups).max(1);
if left > PASSES {
return Err(Error::out_of_memory(format!(
"the memory limit leaves room for {groups} groups at a time and {} rows have already \
gone to a spill file, which is more passes over it than this will finish in",
file.rows()
)));
}
Ok(())
}
fn crowded(memory: &Memory) -> bool {
match memory.limit() {
Some(limit) => memory.used() >= limit / 2,
None => false,
}
}
fn fill(key: &mut Key, columns: &[Vector], row: usize) {
key.0.truncate(columns.len());
for (at, column) in columns.iter().enumerate() {
match key.0.get_mut(at) {
Some(slot) => set(slot, column, row),
None => key.0.push(column.value_at(row)),
}
}
}
fn set(slot: &mut Value, column: &Vector, row: usize) {
if let (Value::Varchar(buffer), Some(text)) = (&mut *slot, column.text_at(row)) {
buffer.clear();
buffer.push_str(text);
return;
}
*slot = column.value_at(row);
}
impl Sink for Aggregate<'_> {
type Local = Partitioned;
fn local(&self) -> Partitioned {
self.started.fetch_add(1, Ordering::Relaxed);
Partitioned {
single: Some(self.start()),
expressions: self.inputs.scratch(),
spreading: Spreading::new(),
own: (0..RADIX_PARTITIONS).map(|_| None).collect(),
}
}
fn parallel(&self) -> bool {
self.max_groups.is_none()
}
fn sink(&self, chunk: &Chunk, local: &mut Partitioned) -> Result<Progress> {
let Partitioned { single, expressions, spreading, own } = local;
let rows = self.read(chunk, expressions)?;
if let Some(table) = single {
if let Some(error) = table.failure.take() {
return Err(error);
}
let timing = stage::Timing::start(Stage::Fold);
let folded = self.fold(&rows, table, None);
timing.stop(0);
folded?;
if !self.ought_to_partition(table) {
return Ok(Progress::More);
}
let handing = single.take().expect("the table was there a moment ago");
self.begin_partitioning(spreading, own)?;
self.hand(handing, spreading, own)?;
return Ok(Progress::More);
}
if self.locally.load(Ordering::Relaxed) && self.still_local(spreading, own)? {
self.spread_own(&rows, spreading, own)?;
return Ok(Progress::More);
}
self.spread(&rows, spreading)?;
Ok(Progress::More)
}
fn combine(&self, local: Partitioned) -> Result<()> {
let Partitioned { single, mut spreading, mut own, .. } = local;
self.deposit(&mut own, &mut spreading)?;
let mut built = self.built.lock().map_err(poisoned)?;
built.instances += 1;
let Some(arriving) = single else { return Ok(()) };
if let Some(error) = arriving.failure {
return Err(error);
}
if built.partitioning {
drop(built);
self.hand(arriving, &mut spreading, &mut own)?;
return self.deposit(&mut own, &mut spreading);
}
let mut kept = self.merged[0].lock().map_err(poisoned)?;
if kept.table.is_none() {
kept.table = Some(arriving);
return Ok(());
}
let spilled =
arriving.over.is_some() || kept.table.as_ref().is_some_and(|held| held.over.is_some());
if spilled {
let seeded = kept.table.take().expect("the table was there a moment ago");
built.partitioning = true;
drop(kept);
drop(built);
self.hand_over(seeded, &mut spreading)?;
return self.hand_over(arriving, &mut spreading);
}
self.merge(arriving, kept.table.as_mut().expect("the table was there a moment ago"))?;
Ok(())
}
fn finalize(&self) -> Result<()> {
let mut built = self.built.lock().map_err(poisoned)?;
let degree = built.instances.clamp(1, self.merged.len());
let closed = if degree > 1 { self.close_together(degree)? } else { self.close_in_turn()? };
for part in closed {
let Part { mut chunks, held } = part?;
built.chunks.append(&mut chunks);
built.held.push(held);
}
let chunks = std::mem::take(&mut built.chunks);
drop(built);
self.out.fill(chunks)
}
}
#[derive(Debug)]
struct Part {
chunks: Vec<Chunk>,
held: Reservation,
}
impl Aggregate<'_> {
fn close_in_turn(&self) -> Result<Vec<Result<Part>>> {
Ok((0..self.merged.len()).map(|at| self.close(at)).collect())
}
fn close_together(&self, degree: usize) -> Result<Vec<Result<Part>>> {
let next = AtomicUsize::new(0);
let slots: Vec<Mutex<Option<Result<Part>>>> =
(0..self.merged.len()).map(|_| Mutex::new(None)).collect();
let mut theirs = Spent::none();
std::thread::scope(|scope| {
let mut handles = Vec::with_capacity(degree - 1);
for _ in 1..degree {
handles.push(scope.spawn(|| {
self.closing(&next, &slots);
stage::here()
}));
}
self.closing(&next, &slots);
for handle in handles {
if let Ok(spent) = handle.join() {
theirs.add(spent);
}
}
});
stage::gained(theirs);
let mut closed = Vec::with_capacity(slots.len());
for (at, slot) in slots.into_iter().enumerate() {
closed.push(slot.into_inner().map_err(poisoned)?.unwrap_or_else(|| {
Err(Error::internal(format!("nothing finished partition {at} of an aggregate")))
}));
}
Ok(closed)
}
fn closing(&self, next: &AtomicUsize, slots: &[Mutex<Option<Result<Part>>>]) {
loop {
let at = next.fetch_add(1, Ordering::Relaxed);
let Some(slot) = slots.get(at) else { return };
let done = self.close(at);
if let Ok(mut slot) = slot.lock() {
*slot = Some(done);
}
}
}
fn close(&self, at: usize) -> Result<Part> {
let mut part = Part { chunks: Vec::new(), held: self.memory.reservation() };
let mut partition = self.merged[at].lock().map_err(poisoned)?;
let Partition { table, carried, pending } = &mut *partition;
let mut kept = table.take();
for arriving in std::mem::take(pending) {
match &mut kept {
Some(into) => self.merge(arriving, into)?,
None => kept = Some(arriving),
}
}
let Some(kept) = kept else {
debug_assert!(carried.is_none(), "nothing is set aside from a partition with no table");
return Ok(part);
};
let Part { chunks, held } = &mut part;
let mut left = self.finish(kept, chunks, held)?;
if left.is_none() {
if let Some(whole) = carried.take() {
left = self.finish(whole, chunks, held)?;
}
}
while let Some(mut file) = left {
left = self.again(&mut file, carried.take(), chunks, held)?;
}
Ok(part)
}
}
fn charge(into: &mut Building, grown: u64) -> Result<()> {
into.containers.grow(grown)?;
rows::capacity(into.table.owned(), &mut into.charged_keys, &mut into.scratch)?;
let now = tables(&into.table, &into.states, &into.counts, &into.seen);
rows::capacity(now, &mut into.charged, &mut into.containers)
}
struct Folding<'a> {
count_only: bool,
calls: usize,
distinct: &'a [bool],
taken: &'a [Accumulator],
tallies: &'a [i64],
watched: &'a mut [DistinctSet],
}
fn merge_slot(
from: &mut Folding<'_>,
slot: usize,
target: usize,
into: &mut Building,
) -> Result<u64> {
if from.count_only {
into.counts[target] += from.tallies[slot];
return Ok(0);
}
let calls = from.calls;
let mut aside = 0;
for at in 0..calls {
if !from.distinct[at] {
into.states[target * calls + at].combine(&from.taken[slot * calls + at])?;
continue;
}
let arriving = std::mem::replace(
&mut from.watched[slot * calls + at],
DistinctSet::Row(RowSet::default()),
);
let state = &mut into.states[target * calls + at];
match (&mut into.seen[target * calls + at], arriving) {
(DistinctSet::BigInt(kept), DistinctSet::BigInt(arriving)) => {
for value in arriving {
if kept.insert(value) {
aside += width_of(size_of::<i64>() * 2);
state.update(&[Value::BigInt(value)])?;
}
}
}
(DistinctSet::Row(kept), DistinctSet::Row(arriving)) => {
for key in arriving {
if kept.contains(&key) {
continue;
}
state.update(&key.0)?;
aside += rows::footprint(&key.0);
kept.insert(key);
}
}
_ => {
return Err(Error::internal(
"two instances of one DISTINCT aggregate disagree about what their sets hold",
));
}
}
}
Ok(aside)
}
fn tables(
table: &Table,
states: &Vec<Accumulator>,
counts: &Vec<i64>,
seen: &Vec<DistinctSet>,
) -> u64 {
let width = |count: usize, size: usize| {
u64::try_from(count).unwrap_or(u64::MAX).saturating_mul(width_of(size))
};
table.footprint()
+ width(states.capacity(), size_of::<Accumulator>())
+ width(counts.capacity(), size_of::<i64>())
+ width(seen.capacity(), size_of::<DistinctSet>())
}
fn width_of(size: usize) -> u64 {
u64::try_from(size).unwrap_or(u64::MAX)
}
fn group_name(plan: &Plan, group: ExprRef, input: &Schema, at: usize) -> String {
if let Expr::Column(binding) = *plan.expr(group) {
if let Some(position) = input.position_of(binding) {
return input.fields()[position].name.clone();
}
}
format!("group{at}")
}
#[derive(Debug)]
pub(crate) struct Distinct {
on: Prepared,
whole: bool,
types: Vec<LogicalType>,
memory: Memory,
global: Mutex<Held>,
charged: Mutex<Vec<Reservation>>,
held: Mutex<Reservation>,
out: Buffered,
}
#[derive(Debug)]
struct Held {
seen: RowSet,
kept: Vec<Vec<Value>>,
counted: u64,
table: Reservation,
counted_rows: u64,
slots: Reservation,
}
#[derive(Debug)]
pub(crate) struct Keeping {
seen: RowSet,
kept: Vec<(Key, Vec<Value>)>,
scratch: Scratch,
key: Key,
rows: Reservation,
table: Reservation,
counted: u64,
counted_table: u64,
}
impl Distinct {
pub(crate) fn new(
plan: &Plan,
input: &Schema,
on: Slice,
memory: &Memory,
) -> Result<(Self, Buffered)> {
let on = plan.expr_list(on).to_vec();
let out = Buffered::new();
let distinct = Self {
whole: on.is_empty(),
on: Prepared::new(plan, &on, input)?,
types: input.types(),
memory: memory.clone(),
global: Mutex::new(Held {
seen: RowSet::default(),
kept: Vec::new(),
counted: 0,
table: memory.reservation(),
counted_rows: 0,
slots: memory.reservation(),
}),
charged: Mutex::new(Vec::new()),
held: Mutex::new(memory.reservation()),
out: out.clone(),
};
Ok((distinct, out))
}
}
impl Sink for Distinct {
type Local = Keeping;
fn parallel(&self) -> bool {
self.whole
}
fn local(&self) -> Keeping {
Keeping {
seen: RowSet::default(),
kept: Vec::new(),
scratch: self.on.scratch(),
key: Key(Vec::new()),
rows: self.memory.reservation(),
table: self.memory.reservation(),
counted: 0,
counted_table: 0,
}
}
fn sink(&self, chunk: &Chunk, local: &mut Keeping) -> Result<Progress> {
let mut keys = Vec::with_capacity(self.on.len());
self.on.evaluate(chunk, &mut local.scratch, &mut keys)?;
let mut taken = 0;
let mut aside = 0;
for row in 0..chunk.len() {
if self.whole {
local.key.0.clear();
local.key.0.extend(chunk.row(row));
} else {
fill(&mut local.key, &keys, row);
}
if local.seen.contains(&local.key) {
continue;
}
let values: Vec<Value> =
if self.whole { local.key.0.clone() } else { chunk.row(row).collect() };
let stored = local.key.clone();
taken += rows::heap(&values) + rows::heap(&stored.0);
aside += rows::heap(&stored.0);
local.seen.insert(stored.clone());
local.kept.push((stored, values));
}
local.rows.grow(taken)?;
local.table.grow(aside)?;
let held = width_of(local.kept.capacity() * size_of::<(Key, Vec<Value>)>());
rows::capacity(held, &mut local.counted, &mut local.rows)?;
let now = rows::buckets(local.seen.capacity()) * (width_of(size_of::<Key>()) + 1);
rows::capacity(now, &mut local.counted_table, &mut local.table)?;
Ok(Progress::More)
}
fn combine(&self, local: Keeping) -> Result<()> {
let Keeping { seen, kept, rows, mut table, .. } = local;
drop(seen);
table.release();
let mut global = self.global.lock().map_err(poisoned)?;
let global = &mut *global;
let mut aside = 0;
for (key, values) in kept {
let cost = rows::heap(&key.0);
if global.seen.insert(key) {
aside += cost;
global.kept.push(values);
}
}
global.table.grow(aside)?;
let now = rows::buckets(global.seen.capacity()) * (width_of(size_of::<Key>()) + 1);
rows::capacity(now, &mut global.counted, &mut global.table)?;
let held = width_of(global.kept.capacity() * size_of::<Vec<Value>>());
rows::capacity(held, &mut global.counted_rows, &mut global.slots)?;
self.charged.lock().map_err(poisoned)?.push(rows);
Ok(())
}
fn finalize(&self) -> Result<()> {
let mut global = self.global.lock().map_err(poisoned)?;
let kept = std::mem::take(&mut global.kept);
global.seen = RowSet::default();
global.counted = 0;
global.table.release();
let mut held = self.held.lock().map_err(poisoned)?;
let chunks = rows::chunks(&self.types, &kept, &mut held)?;
self.out.fill(chunks)?;
global.counted_rows = 0;
global.slots.release();
self.charged.lock().map_err(poisoned)?.clear();
Ok(())
}
}
fn poisoned<T>(_: T) -> Error {
Error::internal("a thread panicked while holding the rows a distinct is keeping")
}
#[cfg(test)]
mod tests {
use rudb_common::{Field, LogicalType, Memory, Value};
use rudb_pipeline::Sink;
use rudb_plan::{Plan, Slice};
use rudb_vector::{Chunk, Data, Vector};
use super::{Aggregate, Distinct};
use crate::buffer::Buffered;
use crate::schema::Schema;
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 distinct() -> (Distinct, Buffered) {
let schema = Schema::numbered(vec![Field::new("a", LogicalType::Integer)], 0);
Distinct::new(&Plan::new(), &schema, Slice::EMPTY, &Memory::unlimited())
.expect("there are no expressions to resolve")
}
fn column(out: &Buffered) -> Vec<Value> {
let chunk = out.at(0).expect("readable").expect("one chunk");
(0..chunk.len()).map(|row| chunk.value_at(row, 0)).collect()
}
#[test]
fn one_instance_keeps_the_first_of_each_row() {
let (distinct, out) = distinct();
let mut local = distinct.local();
distinct.sink(&chunk(&[1, 2, 1, 3, 2]), &mut local).expect("five rows");
distinct.combine(local).expect("the one instance");
distinct.finalize().expect("the answer");
assert_eq!(column(&out), [Value::Integer(1), Value::Integer(2), Value::Integer(3)]);
}
#[test]
fn two_instances_that_both_kept_a_row_keep_one_of_it_between_them() {
let (distinct, out) = distinct();
let mut left = distinct.local();
let mut right = distinct.local();
distinct.sink(&chunk(&[1, 2]), &mut left).expect("two rows");
distinct.sink(&chunk(&[2, 3]), &mut right).expect("two rows");
distinct.combine(left).expect("the first instance");
distinct.combine(right).expect("the second instance");
distinct.finalize().expect("the answer");
assert_eq!(column(&out), [Value::Integer(1), Value::Integer(2), Value::Integer(3)]);
}
#[test]
fn an_ungrouped_aggregate_answers_one_row_from_one_instance() {
let plan = Plan::new();
let schema = Schema::numbered(vec![Field::new("a", LogicalType::Integer)], 0);
let (aggregate, out) =
Aggregate::new(&plan, &schema, 1, Slice::EMPTY, Slice::EMPTY, &Memory::unlimited())
.expect("no aggregates to take apart");
aggregate.combine(aggregate.local()).expect("the one instance");
aggregate.finalize().expect("the answer");
assert_eq!(out.at(0).expect("readable").expect("one chunk").len(), 1);
}
fn parsed(text: &str) -> Plan {
Plan::parse(&format!("{text}\n Get memory.main.t AS t #0 [x::INTEGER]"))
.expect("a plan this crate's own notation describes")
}
fn aggregate(plan: &Plan) -> (Aggregate<'_>, Buffered) {
let schema = Schema::numbered(vec![Field::new("x", LogicalType::Integer)], 0);
let (groups, aggregates) = match *plan.node(plan.root()) {
rudb_plan::Node::Aggregate { groups, aggregates, .. } => (groups, aggregates),
ref other => panic!("the plan's root is {other:?} and not an aggregate"),
};
Aggregate::new(plan, &schema, 1, groups, aggregates, &Memory::unlimited())
.expect("the aggregates are ones this crate implements")
}
fn answer(out: &Buffered) -> Vec<Vec<Value>> {
let mut rows = Vec::new();
for at in 0.. {
let Some(chunk) = out.at(at).expect("readable") else { break };
for row in 0..chunk.len() {
rows.push((0..chunk.width()).map(|column| chunk.value_at(row, column)).collect());
}
}
rows
}
#[test]
fn two_instances_of_a_grouped_aggregate_answer_one_row_a_group() {
let plan = parsed("Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count_star()::BIGINT]");
let (aggregate, out) = aggregate(&plan);
let mut left = aggregate.local();
let mut right = aggregate.local();
aggregate.sink(&chunk(&[1, 2, 1]), &mut left).expect("three rows");
aggregate.sink(&chunk(&[2, 3, 2]), &mut right).expect("three rows");
aggregate.combine(left).expect("the first instance");
aggregate.combine(right).expect("the second instance");
aggregate.finalize().expect("the answer");
let mut rows = answer(&out);
rows.sort_by_key(|row| format!("{:?}", row[0]));
assert_eq!(
rows,
[
vec![Value::Integer(1), Value::BigInt(2)],
vec![Value::Integer(2), Value::BigInt(3)],
vec![Value::Integer(3), Value::BigInt(1)],
]
);
}
#[test]
fn a_partitioned_aggregate_answers_every_group_once() {
let plan = parsed("Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count_star()::BIGINT]");
let (aggregate, out) = aggregate(&plan);
let mut left = aggregate.local();
let mut right = aggregate.local();
let values: Vec<i32> = (0..5_000).collect();
for part in values.chunks(1_024) {
aggregate.sink(&chunk(part), &mut left).expect("a chunk of groups");
aggregate.sink(&chunk(part), &mut right).expect("the same groups again");
}
aggregate.combine(left).expect("the first instance");
aggregate.combine(right).expect("the second instance");
assert!(
aggregate.built.lock().expect("readable").partitioning,
"five thousand groups on two instances is meant to take the partitioned path"
);
aggregate.finalize().expect("the answer");
let mut seen: Vec<i32> = Vec::new();
for row in answer(&out) {
assert_eq!(row[1], Value::BigInt(2), "{row:?} was counted on both instances");
match row[0] {
Value::Integer(key) => seen.push(key),
ref other => panic!("the group is {other:?} and not an integer"),
}
}
seen.sort_unstable();
assert_eq!(seen, values);
}
#[test]
fn two_instances_of_an_ungrouped_aggregate_add_up_to_one_total() {
let plan = parsed(
"Aggregate #1 groups=[] aggregates=[sum(#0.0::INTEGER)::HUGEINT, min(#0.0::INTEGER)::INTEGER, max(#0.0::INTEGER)::INTEGER, count_star()::BIGINT]",
);
let (aggregate, out) = aggregate(&plan);
let mut left = aggregate.local();
let mut right = aggregate.local();
aggregate.sink(&chunk(&[4, 7]), &mut left).expect("two rows");
aggregate.sink(&chunk(&[2, 9]), &mut right).expect("two rows");
aggregate.combine(left).expect("the first instance");
aggregate.combine(right).expect("the second instance");
aggregate.finalize().expect("the answer");
assert_eq!(
answer(&out),
[vec![Value::HugeInt(22), Value::Integer(2), Value::Integer(9), Value::BigInt(4)]]
);
}
#[test]
fn an_instance_that_saw_no_rows_changes_nothing_when_it_combines() {
let plan = parsed("Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count_star()::BIGINT]");
let (aggregate, out) = aggregate(&plan);
let mut seen = aggregate.local();
aggregate.sink(&chunk(&[5, 5]), &mut seen).expect("two rows");
aggregate.combine(aggregate.local()).expect("an instance that saw nothing");
aggregate.combine(seen).expect("the one that saw something");
aggregate.combine(aggregate.local()).expect("another that saw nothing");
aggregate.finalize().expect("the answer");
assert_eq!(answer(&out), [vec![Value::Integer(5), Value::BigInt(2)]]);
}
#[test]
fn two_instances_of_a_distinct_aggregate_count_a_shared_value_once() {
let plan = parsed(
"Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count(DISTINCT #0.0::INTEGER)::BIGINT]",
);
let (aggregate, out) = aggregate(&plan);
let mut left = aggregate.local();
let mut right = aggregate.local();
aggregate.sink(&chunk(&[3, 3]), &mut left).expect("two rows of one group");
aggregate.sink(&chunk(&[3, 4]), &mut right).expect("the same group and another");
aggregate.combine(left).expect("the first instance");
aggregate.combine(right).expect("the second instance");
aggregate.finalize().expect("the answer");
let mut rows = answer(&out);
rows.sort_by_key(|row| format!("{:?}", row[0]));
assert_eq!(
rows,
[vec![Value::Integer(3), Value::BigInt(1)], vec![Value::Integer(4), Value::BigInt(1)]]
);
}
#[test]
fn a_distinct_over_nothing_produces_nothing() {
let (distinct, out) = distinct();
distinct.combine(distinct.local()).expect("an instance that saw no chunks");
distinct.finalize().expect("the answer");
assert_eq!(out.len().expect("readable"), 0);
}
}