use std::cmp::Ordering;
use arrow_array::{ArrayRef, RecordBatch, UInt32Array};
use arrow_ord::partition::partition;
use arrow_row::{RowConverter, Rows, SortField};
use arrow_schema::{ArrowError, SchemaRef, SortOptions};
use arrow_select::interleave::interleave_record_batch;
use arrow_select::take::take_record_batch;
const ASCENDING: SortOptions = SortOptions { descending: false, nulls_first: true };
#[derive(Debug)]
pub enum Step {
Need(usize),
Output(RecordBatch),
Done,
}
#[derive(Debug, Default)]
struct Input {
batch: Option<RecordBatch>,
rows: Option<Rows>,
next: usize,
done: bool,
source: Option<usize>,
}
impl Input {
fn len(&self) -> usize {
self.batch.as_ref().map_or(0, RecordBatch::num_rows)
}
fn exhausted(&self) -> bool {
self.next >= self.len()
}
}
#[derive(Debug)]
pub struct Merge {
schema: SchemaRef,
keys: Vec<usize>,
unique: usize,
converter: RowConverter,
inputs: Vec<Input>,
sources: Vec<RecordBatch>,
taken: Vec<(usize, usize)>,
batch_rows: usize,
held: Option<RecordBatch>,
rows_in: u64,
rows_out: u64,
runs: u64,
}
impl Merge {
pub fn new(
schema: SchemaRef,
keys: &[&str],
unique: usize,
inputs: usize,
batch_rows: usize,
) -> Result<Self, ArrowError> {
let keys: Vec<usize> = keys.iter().map(|k| schema.index_of(k)).collect::<Result<_, _>>()?;
if unique > keys.len() {
return Err(ArrowError::InvalidArgumentError(format!("dedup on {unique} of {} keys", keys.len())));
}
let fields =
keys.iter().map(|&i| SortField::new_with_options(schema.field(i).data_type().clone(), ASCENDING)).collect();
Ok(Self {
converter: RowConverter::new(fields)?,
schema,
keys,
unique,
inputs: (0..inputs).map(|_| Input::default()).collect(),
sources: vec![],
taken: vec![],
batch_rows: batch_rows.max(1),
held: None,
rows_in: 0,
rows_out: 0,
runs: 0,
})
}
pub fn push(&mut self, i: usize, batch: Option<RecordBatch>) -> Result<(), ArrowError> {
let input = &mut self.inputs[i];
match batch {
None => {
input.done = true;
input.batch = None;
input.rows = None;
}
Some(batch) => {
if batch.schema() != self.schema {
return Err(ArrowError::SchemaError(format!("input {i} has a different schema")));
}
self.rows_in += batch.num_rows() as u64;
let keys: Vec<ArrayRef> = self.keys.iter().map(|&k| batch.column(k).clone()).collect();
input.rows = if keys.is_empty() { None } else { Some(self.converter.convert_columns(&keys)?) };
input.batch = Some(batch);
input.next = 0;
input.source = None;
}
}
Ok(())
}
pub fn step(&mut self) -> Result<Step, ArrowError> {
loop {
if let Some(i) = self.inputs.iter().position(|s| s.exhausted() && !s.done) {
return Ok(Step::Need(i));
}
if self.taken.len() >= self.batch_rows {
return Ok(Step::Output(self.flush(false)?));
}
let Some(first) = self.smallest() else {
if !self.taken.is_empty() || self.held.is_some() {
return Ok(Step::Output(self.flush(true)?));
}
return Ok(Step::Done);
};
self.take_run(first);
}
}
pub fn stats(&self) -> (u64, u64, u64) {
(self.rows_in, self.rows_out, self.runs)
}
fn smallest(&self) -> Option<usize> {
let mut best: Option<usize> = None;
for (i, input) in self.inputs.iter().enumerate() {
if input.exhausted() {
continue;
}
if best.is_none_or(|b| self.compare(i, b) == Ordering::Less) {
best = Some(i);
}
}
best
}
fn compare(&self, a: usize, b: usize) -> Ordering {
let row = |i: usize| self.inputs[i].rows.as_ref().map(|r| r.row(self.inputs[i].next));
row(a).cmp(&row(b)).then(a.cmp(&b))
}
fn take_run(&mut self, i: usize) {
let input = &self.inputs[i];
if input.batch.is_none() {
return;
}
let (start, len) = (input.next, input.len());
let bound = (0..self.inputs.len())
.filter(|&j| j != i && !self.inputs[j].exhausted())
.min_by(|&a, &b| self.compare(a, b))
.and_then(|j| self.inputs[j].rows.as_ref().map(|r| (r.row(self.inputs[j].next), i < j)));
let end = match (bound, &input.rows) {
(None, _) | (_, None) => len,
(Some((other, first_on_ties)), Some(rows)) => {
let before = |r: usize| {
let ord = rows.row(r).cmp(&other);
ord == Ordering::Less || (ord == Ordering::Equal && first_on_ties)
};
let (mut lo, mut probe, mut step) = (start + 1, start + 1, 1);
let mut hi = len;
while probe < len {
if !before(probe) {
hi = probe;
break;
}
lo = probe + 1;
probe += step;
step *= 2;
}
while lo < hi {
let mid = lo + (hi - lo) / 2;
if before(mid) {
lo = mid + 1;
} else {
hi = mid;
}
}
lo
}
};
let end = end.min(start + (self.batch_rows - self.taken.len().min(self.batch_rows)).max(1));
let input = &mut self.inputs[i];
let source = *input.source.get_or_insert_with(|| {
self.sources.push(input.batch.clone().expect("checked above"));
self.sources.len() - 1
});
self.taken.extend((start..end).map(|row| (source, row)));
input.next = end;
self.runs += 1;
}
fn flush(&mut self, end: bool) -> Result<RecordBatch, ArrowError> {
let batch = self.take_rows()?;
let batch = if self.unique == 0 || batch.num_rows() == 0 {
batch
} else {
let mut kept = self.last_per_group(&batch)?;
if !end && kept.num_rows() > 0 {
let n = kept.num_rows();
self.held = Some(kept.slice(n - 1, 1));
kept = kept.slice(0, n - 1);
}
kept
};
self.rows_out += batch.num_rows() as u64;
Ok(batch)
}
fn take_rows(&mut self) -> Result<RecordBatch, ArrowError> {
let mut sources = std::mem::take(&mut self.sources);
let mut rows = std::mem::take(&mut self.taken);
for input in &mut self.inputs {
input.source = None;
}
if let Some(held) = self.held.take() {
sources.push(held);
rows.insert(0, (sources.len() - 1, 0));
}
match sources.as_slice() {
[] => Ok(RecordBatch::new_empty(self.schema.clone())),
[only] if rows.windows(2).all(|w| w[1].1 == w[0].1 + 1) => Ok(only.slice(rows[0].1, rows.len())),
_ => interleave_record_batch(&sources.iter().collect::<Vec<_>>(), &rows),
}
}
fn last_per_group(&self, batch: &RecordBatch) -> Result<RecordBatch, ArrowError> {
let columns: Vec<ArrayRef> = self.keys[..self.unique].iter().map(|&k| batch.column(k).clone()).collect();
let groups = partition(&columns)?.ranges();
if groups.len() == batch.num_rows() {
return Ok(batch.clone());
}
let last: UInt32Array = groups.iter().map(|g| u32::try_from(g.end - 1).unwrap_or(u32::MAX)).collect();
take_record_batch(batch, &last)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use arrow_array::{Int64Array, StringArray};
use arrow_schema::{DataType, Field, Schema};
use super::*;
fn schema() -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("s", DataType::Utf8, false),
Field::new("t", DataType::Int64, false),
Field::new("tag", DataType::Utf8, false),
]))
}
fn batch(rows: &[(&str, i64, &str)]) -> RecordBatch {
let s: StringArray = rows.iter().map(|r| Some(r.0)).collect();
let t: Int64Array = rows.iter().map(|r| r.1).collect();
let tag: StringArray = rows.iter().map(|r| Some(r.2)).collect();
RecordBatch::try_new(schema(), vec![Arc::new(s), Arc::new(t), Arc::new(tag)]).unwrap()
}
fn run(inputs: Vec<Vec<RecordBatch>>, unique: usize, batch_rows: usize) -> Vec<String> {
let mut merge = Merge::new(schema(), &["s", "t"], unique, inputs.len(), batch_rows).unwrap();
let mut feeds: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
let mut out = vec![];
loop {
match merge.step().unwrap() {
Step::Need(i) => merge.push(i, feeds[i].next()).unwrap(),
Step::Output(b) => {
assert!(b.num_rows() <= batch_rows + 1);
let tags = b.column(2).as_any().downcast_ref::<StringArray>().unwrap();
out.extend(tags.iter().map(|t| t.unwrap().to_string()));
}
Step::Done => return out,
}
}
}
#[test]
fn interleaves_inputs_in_key_order() {
let a = vec![batch(&[("a", 1, "a1"), ("a", 3, "a3")]), batch(&[("b", 1, "b1")])];
let b = vec![batch(&[("a", 2, "a2")]), batch(&[("b", 0, "b0"), ("c", 0, "c0")])];
assert_eq!(run(vec![a, b], 0, 1024), ["a1", "a2", "a3", "b0", "b1", "c0"]);
}
#[test]
fn later_inputs_win_on_equal_keys() {
let old = vec![batch(&[("a", 1, "old"), ("a", 2, "old2")])];
let new = vec![batch(&[("a", 1, "new")])];
assert_eq!(run(vec![old.clone(), new.clone()], 2, 1024), ["new", "old2"]);
assert_eq!(run(vec![old, new], 0, 1024), ["old", "new", "old2"]);
}
#[test]
fn dedup_spans_output_batches() {
let a = vec![batch(&[("a", 1, "x"), ("a", 2, "x"), ("a", 3, "x")])];
let b = vec![batch(&[("a", 1, "y"), ("a", 2, "y"), ("a", 3, "y")])];
for rows in [1, 2, 3, 1024] {
assert_eq!(run(vec![a.clone(), b.clone()], 2, rows), ["y", "y", "y"], "batch_rows {rows}");
}
}
#[test]
fn duplicates_within_one_input_keep_the_last() {
let a = vec![batch(&[("a", 1, "first")]), batch(&[("a", 1, "second"), ("a", 2, "z")])];
assert_eq!(run(vec![a], 2, 1024), ["second", "z"]);
}
#[test]
fn disjoint_inputs_are_taken_as_runs() {
let a = vec![batch(&[("a", 1, "1"), ("a", 2, "2"), ("a", 3, "3")])];
let b = vec![batch(&[("b", 1, "4"), ("b", 2, "5")])];
let mut merge = Merge::new(schema(), &["s", "t"], 2, 2, 1024).unwrap();
let mut feeds = [a.into_iter(), b.into_iter()];
loop {
match merge.step().unwrap() {
Step::Need(i) => merge.push(i, feeds[i].next()).unwrap(),
Step::Output(_) => {}
Step::Done => break,
}
}
assert_eq!(merge.stats(), (5, 5, 2));
}
#[test]
fn without_keys_inputs_follow_one_another() {
let mut merge = Merge::new(schema(), &[], 0, 2, 1024).unwrap();
let mut feeds = [vec![batch(&[("b", 1, "1")])].into_iter(), vec![batch(&[("a", 1, "2")])].into_iter()];
let mut out = vec![];
loop {
match merge.step().unwrap() {
Step::Need(i) => merge.push(i, feeds[i].next()).unwrap(),
Step::Output(b) => out.push(b.num_rows()),
Step::Done => break,
}
}
assert_eq!(out.iter().sum::<usize>(), 2);
}
#[test]
fn matches_sort_then_keep_last() {
let mut seed = 42u64;
let mut rand = |n: usize| {
seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
usize::try_from(seed >> 33).unwrap() % n
};
let series = ["a", "b", "c", "d"];
for case in 0..200 {
let k = 1 + rand(5);
let mut inputs = vec![];
let mut all = vec![];
for i in 0..k {
let mut rows: Vec<(&str, i64, String)> = (0..rand(200))
.map(|r| (series[rand(4)], i64::try_from(rand(20)).unwrap(), format!("{i}.{r}")))
.collect();
rows.sort_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1)));
all.extend(rows.iter().cloned());
let mut batches = vec![];
let mut rest = rows.as_slice();
while !rest.is_empty() {
let n = 1 + rand(rest.len());
let part: Vec<_> = rest[..n].iter().map(|r| (r.0, r.1, r.2.as_str())).collect();
batches.push(batch(&part));
rest = &rest[n..];
}
inputs.push(batches);
}
all.sort_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1)));
let mut expected: Vec<(&str, i64, String)> = vec![];
for row in all {
if expected.last().is_some_and(|l| (l.0, l.1) == (row.0, row.1)) {
expected.pop();
}
expected.push(row);
}
let expected: Vec<String> = expected.into_iter().map(|r| r.2).collect();
let batch_rows = 1 + rand(8);
assert_eq!(run(inputs, 2, batch_rows), expected, "case {case}");
}
}
#[test]
fn empty_inputs() {
assert_eq!(run(vec![vec![], vec![batch(&[])]], 2, 8), Vec::<String>::new());
assert_eq!(run(vec![vec![], vec![batch(&[("a", 1, "x")])]], 2, 8), ["x"]);
}
}