use std::ops::Range;
use crate::planning::plan::PartitionSpec;
use linkrs_core::error::QueryError;
#[derive(Debug, Clone)]
pub struct PartitionView {
pub partition_count: usize,
pub partition_ids: Vec<usize>,
pub partition_ranges: Vec<Range<i64>>,
}
impl From<&PartitionSpec> for PartitionView {
fn from(spec: &PartitionSpec) -> Self {
let partition_count = spec.partition_count();
Self {
partition_count,
partition_ids: (0..partition_count).collect(),
partition_ranges: spec.ranges().to_vec(),
}
}
}
impl PartitionView {
pub fn try_new(
partition_count: usize,
partition_ranges: Vec<Range<i64>>,
) -> Result<Self, QueryError> {
if partition_count == 0 || partition_count != partition_ranges.len() {
return Err(QueryError::execution(
"Partition count must match a non-empty range list".to_string(),
));
}
let mut previous_end = None;
for (index, range) in partition_ranges.iter().enumerate() {
if range.start >= range.end {
return Err(QueryError::execution(format!(
"Partition range at index {index} must not be empty"
)));
}
if previous_end.is_some_and(|end| range.start < end) {
return Err(QueryError::execution(format!(
"Partition range at index {index} must be ordered and non-overlapping"
)));
}
previous_end = Some(range.end);
}
let partition_ids = (0..partition_count).collect();
Ok(Self {
partition_count,
partition_ids,
partition_ranges,
})
}
pub fn single(range: Range<i64>) -> Result<Self, QueryError> {
Self::try_new(1, vec![range])
}
pub fn from_range(range: Range<i64>, partition_count: usize) -> Result<Self, QueryError> {
if partition_count == 0 {
return Err(QueryError::execution(
"Partition count must be greater than zero".to_string(),
));
}
if range.start >= range.end {
return Err(QueryError::execution(
"Partition source range must not be empty".to_string(),
));
}
let total = range.end - range.start;
let per_partition = (total + partition_count as i64 - 1) / partition_count as i64;
let mut ranges = Vec::new();
for i in 0..partition_count {
let start = range.start + (i as i64) * per_partition;
let end = (start + per_partition).min(range.end);
if start < end {
ranges.push(start..end);
}
}
Self::try_new(ranges.len(), ranges)
}
pub fn get_range(&self, partition_id: usize) -> Option<Range<i64>> {
if partition_id < self.partition_ranges.len() {
Some(self.partition_ranges[partition_id].clone())
} else {
None
}
}
pub fn partition_size(&self, partition_id: usize) -> i64 {
self.get_range(partition_id)
.map(|r| r.end - r.start)
.unwrap_or(0)
}
pub fn total_size(&self) -> i64 {
self.partition_ranges.iter().map(|r| r.end - r.start).sum()
}
}
pub fn flatten_parallel_fallback_reason(visible_rows: usize, batch_size: usize) -> Option<String> {
let batch_size = batch_size.max(1);
if visible_rows > batch_size {
Some(format!(
"Flatten: single-partition batch execution (rows={visible_rows} > batch={batch_size}); morsel parallelism deferred"
))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_partition() {
let view = PartitionView::single(0i64..1000).expect("valid single partition");
assert_eq!(view.partition_count, 1);
assert_eq!(view.total_size(), 1000);
assert_eq!(view.get_range(0), Some(0i64..1000));
}
#[test]
fn test_split_partitions() {
let view = PartitionView::from_range(0i64..1000, 4).expect("valid split");
assert_eq!(view.partition_count, 4);
assert_eq!(view.total_size(), 1000);
let mut prev_end = 0i64;
for (i, range) in view.partition_ranges.iter().enumerate() {
assert_eq!(range.start, prev_end, "Partition {} is not contiguous", i);
prev_end = range.end;
}
assert_eq!(prev_end, 1000);
}
#[test]
fn test_uneven_split() {
let view = PartitionView::from_range(0i64..1001, 4).expect("valid uneven split");
assert_eq!(view.partition_count, 4);
assert_eq!(view.total_size(), 1001);
}
#[test]
fn rejects_invalid_partition_layout() {
let empty = PartitionView::try_new(0, Vec::new()).expect_err("empty layout must fail");
assert!(empty.to_string().contains("non-empty"));
let overlapping = PartitionView::try_new(2, vec![0i64..10, 5i64..20])
.expect_err("overlapping ranges must fail");
assert!(overlapping.to_string().contains("non-overlapping"));
}
#[test]
fn flatten_parallel_fallback_reason_reports_multi_batch() {
assert!(super::flatten_parallel_fallback_reason(10, 2048).is_none());
let reason = super::flatten_parallel_fallback_reason(5000, 2048)
.expect("multi-batch flatten must report a fallback reason");
assert!(reason.contains("single-partition"));
assert!(reason.contains("morsel parallelism deferred"));
}
}