use std::mem::size_of;
use std::sync::{Arc, Mutex, OnceLock};
use rudb_common::{Error, LogicalType, Memory, Reservation, Result, Stage, Value, stage};
use rudb_pipeline::Lease;
use rudb_vector::{Chunk, Vector};
use crate::group::signed_value;
use crate::pairs::{
self, Counted, Grouped, Held, PARTITIONS, Run, distinct_pairs, in_parallel, scatter,
};
use crate::rows;
use crate::signed::SignedBlock;
const EMPTY: u32 = u32::MAX;
#[derive(Debug)]
enum Column {
Signed(LogicalType),
Dictionary(Arc<Vector>),
}
impl Column {
fn kind(&self) -> LogicalType {
match self {
Self::Signed(kind) => kind.clone(),
Self::Dictionary(_) => LogicalType::Varchar,
}
}
fn low(&self) -> Option<i64> {
match self {
Self::Signed(LogicalType::TinyInt) => Some(i64::from(i8::MIN)),
Self::Signed(LogicalType::SmallInt) => Some(i64::from(i16::MIN)),
Self::Signed(LogicalType::Integer) => Some(i64::from(i32::MIN)),
Self::Signed(_) => None,
Self::Dictionary(_) => Some(0),
}
}
fn width(&self) -> Option<i64> {
match self {
Self::Signed(LogicalType::TinyInt) => Some(1 << 8),
Self::Signed(LogicalType::SmallInt) => Some(1 << 16),
Self::Signed(LogicalType::Integer) => Some(1 << 32),
Self::Signed(_) => None,
Self::Dictionary(dictionary) => i64::try_from(dictionary.len()).ok(),
}
}
fn value(&self, code: i64) -> Result<Value> {
match self {
Self::Signed(kind) => signed_value(kind, code),
Self::Dictionary(dictionary) => {
let at = usize::try_from(code)
.map_err(|_| Error::internal("a group code is not a dictionary position"))?;
dictionary.try_value_at(at)
}
}
}
}
#[derive(Debug)]
struct Composite {
columns: Vec<Column>,
lows: Vec<i64>,
spans: Vec<i64>,
strides: Vec<i64>,
}
impl Composite {
fn plan(columns: Vec<Column>) -> Option<Self> {
let lows = columns.iter().map(Column::low).collect::<Option<Vec<_>>>()?;
let spans = columns
.iter()
.map(|column| column.width()?.checked_add(1))
.collect::<Option<Vec<_>>>()?;
let mut strides = vec![1_i64; spans.len()];
let mut width = 1_i64;
for at in (0..spans.len()).rev() {
strides[at] = width;
width = width.checked_mul(spans[at])?;
}
if width > i64::from(i32::MAX) {
return None;
}
Some(Self { columns, lows, spans, strides })
}
fn values(&self, code: i32) -> Result<Vec<Value>> {
let code = i64::from(code);
let mut out = Vec::with_capacity(self.columns.len());
for (at, column) in self.columns.iter().enumerate() {
let here = code / self.strides[at] % self.spans[at];
out.push(match here {
0 => Value::Null,
held => column.value(held - 1 + self.lows[at])?,
});
}
Ok(out)
}
}
#[derive(Debug)]
enum Shape {
Alone(Column),
Many(Composite),
}
impl Shape {
fn plan(keys: &[Key<'_>]) -> Option<Self> {
let mut columns = Vec::with_capacity(keys.len());
for key in keys {
columns.push(match key.codes {
Codes::Loose => return None,
Codes::Signed => Column::Signed(key.kind.clone()),
Codes::Dictionary(_, dictionary) => Column::Dictionary(Arc::clone(dictionary)),
});
}
match columns.len() {
0 => None,
1 => columns.pop().map(Self::Alone),
_ => Composite::plan(columns).map(Self::Many),
}
}
fn columns(&self) -> &[Column] {
match self {
Self::Alone(column) => std::slice::from_ref(column),
Self::Many(composite) => &composite.columns,
}
}
fn kinds(&self) -> Vec<LogicalType> {
self.columns().iter().map(Column::kind).collect()
}
fn values(&self, group: Grouped) -> Result<Vec<Value>> {
match self {
Self::Alone(_) if !group.valid => Ok(vec![Value::Null]),
Self::Alone(column) => Ok(vec![column.value(i64::from(group.group))?]),
Self::Many(composite) => composite.values(group.group),
}
}
}
#[derive(Debug)]
pub(crate) struct Exchange {
shape: Shape,
partitions: Vec<Mutex<Held>>,
held: Mutex<Vec<Reservation>>,
}
pub(crate) enum Codes<'a> {
Signed,
Dictionary(&'a [u32], &'a Arc<Vector>),
Loose,
}
pub(crate) struct Key<'a> {
pub(crate) vector: &'a Vector,
pub(crate) kind: &'a LogicalType,
pub(crate) codes: Codes<'a>,
}
enum ColumnReader<'a> {
Signed(&'a [i64]),
Dictionary(&'a [u32]),
}
impl ColumnReader<'_> {
#[inline]
fn at(&self, row: usize) -> i64 {
match self {
Self::Signed(values) => values[row],
Self::Dictionary(codes) => i64::from(codes[row]),
}
}
}
enum GroupReader<'a> {
Alone(ColumnReader<'a>),
Many(&'a [i32]),
}
impl GroupReader<'_> {
#[inline]
fn at(&self, row: usize) -> i32 {
match self {
Self::Alone(column) => column.at(row) as i32,
Self::Many(codes) => codes[row],
}
}
}
fn lay(
codes: &mut [i32],
reader: &ColumnReader<'_>,
nulls: Option<&Vector>,
at: usize,
composite: &Composite,
) -> Result<()> {
let (low, span, stride) = (composite.lows[at], composite.spans[at], composite.strides[at]);
for (row, slot) in codes.iter_mut().enumerate() {
let here = match nulls {
Some(nulls) if nulls.is_null_at(row) => 0,
_ => reader.at(row) - low + 1,
};
if !(0..span).contains(&here) {
return Err(Error::internal("a group key is wider than the type it was read under"));
}
*slot += (here * stride) as i32;
}
Ok(())
}
#[derive(Debug, Default)]
struct Scratch {
keys: Vec<SignedBlock>,
user: SignedBlock,
codes: Vec<i32>,
}
#[derive(Debug)]
pub(crate) struct Local {
used: bool,
partitions: Vec<Run>,
memory: Reservation,
scratch: Scratch,
}
impl Local {
pub(crate) fn new(memory: &Memory) -> Self {
Self {
used: false,
partitions: (0..PARTITIONS).map(|_| Run::default()).collect(),
memory: memory.reservation(),
scratch: Scratch::default(),
}
}
pub(crate) fn used(&self) -> bool {
self.used
}
}
impl Exchange {
pub(crate) fn buffer(
slot: &OnceLock<Option<Self>>,
keys: &[Key<'_>],
user: &Vector,
rows: usize,
local: &mut Local,
) -> Result<bool> {
let state = slot.get_or_init(|| Shape::plan(keys).map(Self::new));
let Some(state) = state else { return Ok(false) };
let mut scratch = std::mem::take(&mut local.scratch);
let outcome = state.scatter_blocks(keys, user, rows, &mut scratch, local);
local.scratch = scratch;
outcome?;
Ok(true)
}
fn scatter_blocks(
&self,
keys: &[Key<'_>],
user: &Vector,
rows: usize,
scratch: &mut Scratch,
local: &mut Local,
) -> Result<()> {
let Scratch { keys: blocks, user: values, codes } = scratch;
values.read(rows, user)?;
let nulled = values.nulled();
let held_user = values.cut(rows)?;
let reader = self.reader(keys, rows, blocks, codes)?;
let group_nulls = match &self.shape {
Shape::Alone(_) => nulls_of(keys[0].vector, rows),
Shape::Many(_) => None,
};
let before = local.partitions.iter().map(Run::footprint).sum::<usize>();
let shift = pairs::shift();
if !nulled {
match group_nulls {
None => {
for (row, &user) in held_user.iter().enumerate() {
scatter(&mut local.partitions, shift, reader.at(row), true, user);
}
}
Some(nulls) => {
for (row, &user) in held_user.iter().enumerate() {
let valid = !nulls.is_null_at(row);
let group = if valid { reader.at(row) } else { 0 };
scatter(&mut local.partitions, shift, group, valid, user);
}
}
}
} else {
for (row, &held) in held_user.iter().enumerate() {
if user.is_null_at(row) {
continue;
}
let valid = group_nulls.is_none_or(|nulls| !nulls.is_null_at(row));
let group = if valid { reader.at(row) } else { 0 };
scatter(&mut local.partitions, shift, group, valid, held);
}
}
let after = local.partitions.iter().map(Run::footprint).sum::<usize>();
local.memory.grow(width(after.saturating_sub(before)))?;
local.used = true;
Ok(())
}
fn reader<'a>(
&self,
keys: &[Key<'a>],
rows: usize,
blocks: &'a mut Vec<SignedBlock>,
codes: &'a mut Vec<i32>,
) -> Result<GroupReader<'a>> {
if keys.len() != self.shape.columns().len() {
return Err(Error::internal(
"a grouped distinct exchange received the wrong key width",
));
}
blocks.resize_with(keys.len(), SignedBlock::default);
for ((key, column), block) in keys.iter().zip(self.shape.columns()).zip(blocks.iter_mut()) {
if matches!(column, Column::Signed(_)) {
block.read(rows, key.vector)?;
}
}
let blocks: &[SignedBlock] = blocks;
let mut readers = Vec::with_capacity(keys.len());
for ((key, column), block) in keys.iter().zip(self.shape.columns()).zip(blocks) {
readers.push(column_reader(key, column, rows, block)?);
}
match &self.shape {
Shape::Alone(_) => readers
.pop()
.map(GroupReader::Alone)
.ok_or_else(|| Error::internal("a grouped distinct exchange received no key")),
Shape::Many(composite) => {
codes.clear();
codes.resize(rows, 0);
for (at, (reader, key)) in readers.iter().zip(keys).enumerate() {
lay(codes, reader, nulls_of(key.vector, rows), at, composite)?;
}
Ok(GroupReader::Many(codes))
}
}
}
fn new(shape: Shape) -> Self {
Self {
shape,
partitions: (0..PARTITIONS).map(|_| Mutex::new(Held::default())).collect(),
held: Mutex::new(Vec::new()),
}
}
pub(crate) fn combine(&self, mut local: Local) -> Result<()> {
for (at, run) in local.partitions.iter_mut().enumerate() {
if !run.rows.is_empty() {
let run = std::mem::take(run);
self.partitions[at].lock().map_err(poisoned)?.runs.push(run);
}
}
self.held.lock().map_err(poisoned)?.push(local.memory);
Ok(())
}
pub(crate) fn finish(
&self,
threads: &Lease<'_>,
bound: usize,
memory: &Memory,
) -> Result<Vec<Chunk>> {
let input = self
.partitions
.iter()
.map(|partition| partition.lock().map(|held| held.rows()).map_err(poisoned))
.sum::<Result<usize>>()?;
let degree =
input.div_ceil(pairs::ROWS_PER_PARTITION).clamp(1, PARTITIONS).min(threads.degree());
let used = pairs::used(input, degree);
let splits = if degree > 1 { used } else { 1 };
let counted = in_parallel(
threads,
used,
degree,
"deduplicated the pairs of radix partition",
|at| {
let mut partition = Held::default();
for from in pairs::merged(at, used) {
let mut held = self.partitions[from].lock().map_err(poisoned)?;
partition.runs.append(&mut held.runs);
}
distinct_pairs(&mut partition, splits, memory)
},
)?;
let merged = in_parallel(threads, splits, degree, "counted the groups of split", |at| {
count_groups(&counted, at, &self.shape, bound, memory)
})?;
for part in counted {
drop(part.held);
}
let mut chunks = Vec::new();
let mut held = self.held.lock().map_err(poisoned)?;
held.clear();
for Output { chunks: mut part, held: charge } in merged {
chunks.append(&mut part);
held.push(charge);
}
Ok(chunks)
}
}
fn column_reader<'a>(
key: &Key<'a>,
column: &Column,
rows: usize,
block: &'a SignedBlock,
) -> Result<ColumnReader<'a>> {
match (column, &key.codes) {
(Column::Signed(_), Codes::Signed) => Ok(ColumnReader::Signed(block.cut(rows)?)),
(Column::Dictionary(held), Codes::Dictionary(codes, dictionary))
if Arc::ptr_eq(held, dictionary) =>
{
let width = dictionary.len();
if i32::try_from(width).is_err() {
return Err(Error::internal(
"a stable dictionary has more codes than a group record holds",
));
}
let loose = codes[..rows]
.iter()
.enumerate()
.any(|(row, &code)| code as usize >= width && !key.vector.is_null_at(row));
if loose {
return Err(Error::internal("a stable dictionary code is out of range"));
}
Ok(ColumnReader::Dictionary(codes))
}
_ => Err(Error::internal("a grouped distinct exchange received two group code spaces")),
}
}
fn nulls_of(vector: &Vector, rows: usize) -> Option<&Vector> {
vector.validity().has_nulls(rows).then_some(vector)
}
struct Output {
chunks: Vec<Chunk>,
held: Reservation,
}
fn count_groups(
counted: &[Counted],
split: usize,
shape: &Shape,
bound: usize,
memory: &Memory,
) -> Result<Output> {
let timing = stage::Timing::start(Stage::Fold);
let input = counted.iter().map(|part| part.splits[split].len()).sum::<usize>();
let capacity = input.saturating_mul(2).max(64).next_power_of_two();
let mut working = memory.reservation();
working.grow(width(capacity * size_of::<u32>()))?;
let mut buckets = vec![EMPTY; capacity];
let mask = capacity - 1;
let mut groups: Vec<Grouped> = Vec::new();
let mut counts: Vec<i64> = Vec::new();
for part in counted {
for pair in &part.splits[split] {
let mut at = pair.group_hash as usize & mask;
loop {
let slot = buckets[at];
if slot == EMPTY {
buckets[at] = u32::try_from(groups.len()).map_err(|_| {
Error::out_of_memory("a grouped distinct radix split is too large")
})?;
groups.push(*pair);
counts.push(1);
break;
}
let slot = slot as usize;
if groups[slot].group_hash == pair.group_hash
&& groups[slot].group == pair.group
&& groups[slot].valid == pair.valid
{
counts[slot] = counts[slot]
.checked_add(1)
.ok_or_else(|| Error::out_of_range("COUNT(DISTINCT BIGINT) overflowed"))?;
break;
}
at = (at + 1) & mask;
}
}
}
working.grow(width(
groups.capacity() * size_of::<Grouped>() + counts.capacity() * size_of::<i64>(),
))?;
timing.stop(0);
let timing = stage::Timing::start(Stage::Emit);
let mut best: Vec<usize> = Vec::with_capacity(bound.min(groups.len()));
for slot in 0..groups.len() {
let at = best.partition_point(|&kept| counts[kept] >= counts[slot]);
if at < bound {
best.insert(at, slot);
best.truncate(bound);
}
}
best.sort_unstable();
let mut output = Vec::with_capacity(best.len());
for slot in best {
let mut row = shape.values(groups[slot])?;
row.push(Value::BigInt(counts[slot]));
output.push(row);
}
let mut kinds = shape.kinds();
kinds.push(LogicalType::BigInt);
let mut held = memory.reservation();
let chunks = rows::chunks(&kinds, &output, &mut held)?;
timing.stop(0);
Ok(Output { chunks, held })
}
fn width(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
fn poisoned<T>(_: T) -> Error {
Error::internal("a grouped distinct radix lock was poisoned")
}
#[cfg(test)]
mod tests {
use std::mem::size_of;
use std::sync::Arc;
use rudb_common::{LogicalType, Memory, Value};
use rudb_vector::Vector;
use crate::pairs::{Held, Record, Run, distinct_pairs};
use super::{Column, Composite, Shape, count_groups};
#[test]
fn one_partition_deduplicates_pairs_and_counts_groups_across_the_runs_it_was_handed() {
let row = |group, user, pair_hash| Record { user, group, pair_hash };
let mut first = Run::default();
first.push(row(3, 10, 5), true);
first.push(row(3, 10, 5), true);
let mut second = Run::default();
second.push(row(3, 11, 5), true);
second.push(row(3, 10, 5), true);
let mut third = Run::default();
third.push(row(4, 10, 5), true);
third.push(row(0, 10, 5), false);
let mut partition = Held { runs: vec![first, Run::default(), second, third] };
let rows = finished(&mut partition, &signed());
assert_eq!(
rows,
[
vec![Value::Integer(3), Value::BigInt(2)],
vec![Value::Integer(4), Value::BigInt(1)],
vec![Value::Null, Value::BigInt(1)],
]
);
assert_eq!(size_of::<Record>(), 16);
}
#[test]
fn a_group_held_as_a_dictionary_code_comes_out_as_the_string_the_code_stands_for() {
let row = |group, user, pair_hash| Record { user, group, pair_hash };
let mut run = Run::default();
run.push(row(2, 10, 5), true);
run.push(row(2, 11, 5), true);
run.push(row(2, 10, 5), true);
run.push(row(1, 10, 5), true);
run.push(row(0, 10, 5), false);
let mut partition = Held { runs: vec![run] };
let rows = finished(&mut partition, &Shape::Alone(Column::Dictionary(words())));
assert_eq!(
rows,
[
vec![Value::Null, Value::BigInt(1)],
vec![Value::Varchar("one".to_string()), Value::BigInt(1)],
vec![Value::Varchar("two".to_string()), Value::BigInt(2)],
]
);
}
#[test]
fn two_columns_composed_into_one_code_come_back_out_as_the_pair_they_were() {
let shape = two_columns();
let code = |phone: i64, word: i64| {
let Shape::Many(composite) = &shape else { panic!("a composite") };
((phone - i64::from(i16::MIN) + 1) * composite.strides[0] + word + 1) as i32
};
let row = |group, user, pair_hash| Record { user, group, pair_hash };
let mut run = Run::default();
run.push(row(code(7, 2), 10, 5), true);
run.push(row(code(7, 2), 11, 5), true);
run.push(row(code(7, 2), 10, 5), true);
run.push(row(code(7, 1), 10, 5), true);
run.push(row(code(-3, 1), 10, 5), true);
let mut partition = Held { runs: vec![run] };
assert_eq!(
finished(&mut partition, &shape),
[
vec![Value::SmallInt(-3), Value::Varchar("one".into()), Value::BigInt(1)],
vec![Value::SmallInt(7), Value::Varchar("one".into()), Value::BigInt(1)],
vec![Value::SmallInt(7), Value::Varchar("two".into()), Value::BigInt(2)],
]
);
}
#[test]
fn a_null_in_one_column_of_a_composite_is_a_group_of_its_own_per_other_column() {
let shape = two_columns();
let code = |word: i64| (word + 1) as i32;
let (first, second) = (code(1), code(2));
let row = |group, user, pair_hash| Record { user, group, pair_hash };
let mut run = Run::default();
run.push(row(first, 10, 5), true);
run.push(row(second, 10, 5), true);
run.push(row(second, 11, 5), true);
let mut partition = Held { runs: vec![run] };
assert_eq!(
finished(&mut partition, &shape),
[
vec![Value::Null, Value::Varchar("one".into()), Value::BigInt(1)],
vec![Value::Null, Value::Varchar("two".into()), Value::BigInt(2)],
]
);
}
#[test]
fn two_wide_columns_do_not_fit_in_a_group_code() {
assert!(
Composite::plan(vec![
Column::Signed(LogicalType::Integer),
Column::Dictionary(words()),
])
.is_none()
);
assert!(
Composite::plan(
vec![Column::Signed(LogicalType::BigInt), Column::Dictionary(words()),]
)
.is_none()
);
}
#[test]
fn a_group_whose_pairs_landed_in_different_partitions_comes_out_with_one_count() {
let row = |group, user, pair_hash| Record { user, group, pair_hash };
for splits in [1, SPLITS] {
let mut first = Run::default();
first.push(row(3, 10, 5), true);
first.push(row(3, 11, 5), true);
let mut second = Run::default();
second.push(row(3, 12, 9), true);
second.push(row(4, 12, 9), true);
let mut left = Held { runs: vec![first] };
let mut right = Held { runs: vec![second] };
let memory = Memory::unlimited();
let counted = vec![
distinct_pairs(&mut left, splits, &memory).expect("a pair partition"),
distinct_pairs(&mut right, splits, &memory).expect("a pair partition"),
];
assert_eq!(
rows_of(&counted, splits, &signed()),
[
vec![Value::Integer(3), Value::BigInt(3)],
vec![Value::Integer(4), Value::BigInt(1)],
]
);
}
}
const SPLITS: usize = 4;
fn signed() -> Shape {
Shape::Alone(Column::Signed(LogicalType::Integer))
}
fn words() -> Arc<Vector> {
let words = ["zero", "one", "two"].map(|word| Value::Varchar(word.to_string()));
Arc::new(Vector::from_values(LogicalType::Varchar, &words).expect("a dictionary"))
}
fn two_columns() -> Shape {
Shape::Many(
Composite::plan(vec![
Column::Signed(LogicalType::SmallInt),
Column::Dictionary(words()),
])
.expect("a composite"),
)
}
fn finished(partition: &mut Held, shape: &Shape) -> Vec<Vec<Value>> {
let counted = vec![
distinct_pairs(partition, SPLITS, &Memory::unlimited()).expect("a pair partition"),
];
rows_of(&counted, SPLITS, shape)
}
fn rows_of(counted: &[crate::pairs::Counted], splits: usize, shape: &Shape) -> Vec<Vec<Value>> {
let mut rows: Vec<Vec<Value>> = Vec::new();
for split in 0..splits {
let output = count_groups(counted, split, shape, 10, &Memory::unlimited())
.expect("a grouped distinct split");
for chunk in output.chunks {
for row in 0..chunk.len() {
rows.push(
(0..chunk.width()).map(|column| chunk.value_at(row, column)).collect(),
);
}
}
}
rows.sort_by_key(|row| format!("{row:?}"));
rows
}
}