use std::sync::Arc;
use arrow_array::{ArrayRef, RecordBatch, UInt32Array};
use arrow_row::{RowConverter, SortField};
use arrow_schema::{SchemaRef, SortOptions};
use arrow_select::concat::concat_batches;
use arrow_select::take::take_record_batch;
use super::super::error::ScandalError;
use super::{MAX_TOP_K, TopK, column, internal, invalid, require_orderable};
#[derive(Clone, Debug)]
pub struct BoundTopK {
rows: SchemaRef,
keys: Vec<(usize, SortOptions)>,
k: usize,
}
impl BoundTopK {
pub fn bind(top_k: &TopK, rows: &SchemaRef) -> Result<Self, ScandalError> {
if !(1..=MAX_TOP_K).contains(&top_k.k) {
return Err(invalid(format!(
"a top-k keeps between 1 and {MAX_TOP_K} rows; order more rows in your own engine"
)));
}
if top_k.keys.is_empty() {
return Err(invalid("a top-k orders by at least one key"));
}
let mut keys = Vec::with_capacity(top_k.keys.len());
for key in &top_k.keys {
let (index, field) = column(rows, &key.column, "top-k key")?;
require_orderable(field, "top-k key")?;
if keys.iter().any(|(existing, _)| *existing == index) {
return Err(invalid(format!("top-k key `{}` repeats", key.column)));
}
let descending = key.descending.unwrap_or(false);
keys.push((
index,
SortOptions {
descending,
nulls_first: key.nulls_first.unwrap_or(descending),
},
));
}
Ok(Self {
rows: Arc::clone(rows),
keys,
k: top_k.k as usize,
})
}
pub fn schema(&self) -> &SchemaRef {
&self.rows
}
pub fn k(&self) -> usize {
self.k
}
pub fn key_columns(&self) -> impl Iterator<Item = &str> {
self.keys
.iter()
.map(|(index, _)| self.rows.field(*index).name().as_str())
}
pub fn first_key(&self) -> Option<(&str, SortOptions)> {
self.keys
.first()
.map(|(index, options)| (self.rows.field(*index).name().as_str(), *options))
}
pub fn is_key(&self, name: &str) -> bool {
self.key_columns().any(|key| key == name)
}
pub fn converter(&self) -> Result<RowConverter, ScandalError> {
RowConverter::new(
self.keys
.iter()
.map(|(index, options)| {
SortField::new_with_options(
self.rows.field(*index).data_type().clone(),
*options,
)
})
.collect(),
)
.map_err(internal)
}
pub fn merge(&self, results: &[RecordBatch]) -> Result<RecordBatch, ScandalError> {
let batch = concat_batches(&self.rows, results).map_err(internal)?;
let keys = self
.keys
.iter()
.map(|(index, _)| Arc::clone(batch.column(*index)))
.collect::<Vec<ArrayRef>>();
let rows = self.converter()?.convert_columns(&keys).map_err(internal)?;
let mut order = (0..batch.num_rows()).collect::<Vec<_>>();
order.sort_by(|left, right| rows.row(*left).cmp(&rows.row(*right)));
order.truncate(self.k);
let indices = order
.into_iter()
.map(|index| u32::try_from(index).map_err(internal))
.collect::<Result<UInt32Array, _>>()?;
take_record_batch(&batch, &indices).map_err(internal)
}
}
#[cfg(test)]
mod tests {
use arrow_array::cast::AsArray as _;
use arrow_array::{Int64Array, StringArray};
use arrow_schema::{DataType, Field, Schema};
use super::super::super::emit::{BoundEmit, Emit, SortKey};
use super::*;
fn batch(schema: &SchemaRef, scores: Vec<Option<i64>>, labels: Vec<&str>) -> RecordBatch {
RecordBatch::try_new(
Arc::clone(schema),
vec![
Arc::new(Int64Array::from(scores)),
Arc::new(StringArray::from(labels)),
],
)
.unwrap()
}
#[test]
fn merge_orders_by_key_and_breaks_ties_in_split_order() {
let schema = Arc::new(Schema::new(vec![
Field::new("score", DataType::Int64, true),
Field::new("label", DataType::Utf8, false),
]));
let emit = Emit::TopK(TopK {
keys: vec![SortKey {
column: "score".into(),
descending: Some(true),
nulls_first: None,
}],
k: 4,
});
let BoundEmit::TopK(top_k) = emit.bind_results(&schema).unwrap() else {
unreachable!()
};
let merged = top_k
.merge(&[
batch(
&schema,
vec![Some(9), Some(5), Some(1)],
vec!["a", "b", "c"],
),
batch(&schema, vec![None, Some(5), Some(2)], vec!["d", "e", "f"]),
])
.unwrap();
let labels = merged.column(1).as_string::<i32>();
assert_eq!(
labels.iter().flatten().collect::<Vec<_>>(),
["d", "a", "b", "e"]
);
assert!(merged.column(0).is_null(0));
}
}