linkrs_query/executor/streaming/
partition.rs1use std::ops::Range;
7
8use crate::planning::plan::PartitionSpec;
9use linkrs_core::error::QueryError;
10
11#[derive(Debug, Clone)]
16pub struct PartitionView {
17 pub partition_count: usize,
19 pub partition_ids: Vec<usize>,
21 pub partition_ranges: Vec<Range<i64>>,
23}
24
25impl From<&PartitionSpec> for PartitionView {
29 fn from(spec: &PartitionSpec) -> Self {
30 let partition_count = spec.partition_count();
31 Self {
32 partition_count,
33 partition_ids: (0..partition_count).collect(),
34 partition_ranges: spec.ranges().to_vec(),
35 }
36 }
37}
38
39impl PartitionView {
40 pub fn try_new(
42 partition_count: usize,
43 partition_ranges: Vec<Range<i64>>,
44 ) -> Result<Self, QueryError> {
45 if partition_count == 0 || partition_count != partition_ranges.len() {
46 return Err(QueryError::execution(
47 "Partition count must match a non-empty range list".to_string(),
48 ));
49 }
50
51 let mut previous_end = None;
52 for (index, range) in partition_ranges.iter().enumerate() {
53 if range.start >= range.end {
54 return Err(QueryError::execution(format!(
55 "Partition range at index {index} must not be empty"
56 )));
57 }
58 if previous_end.is_some_and(|end| range.start < end) {
59 return Err(QueryError::execution(format!(
60 "Partition range at index {index} must be ordered and non-overlapping"
61 )));
62 }
63 previous_end = Some(range.end);
64 }
65
66 let partition_ids = (0..partition_count).collect();
67 Ok(Self {
68 partition_count,
69 partition_ids,
70 partition_ranges,
71 })
72 }
73
74 pub fn single(range: Range<i64>) -> Result<Self, QueryError> {
76 Self::try_new(1, vec![range])
77 }
78
79 pub fn from_range(range: Range<i64>, partition_count: usize) -> Result<Self, QueryError> {
81 if partition_count == 0 {
82 return Err(QueryError::execution(
83 "Partition count must be greater than zero".to_string(),
84 ));
85 }
86 if range.start >= range.end {
87 return Err(QueryError::execution(
88 "Partition source range must not be empty".to_string(),
89 ));
90 }
91 let total = range.end - range.start;
92 let per_partition = (total + partition_count as i64 - 1) / partition_count as i64;
93
94 let mut ranges = Vec::new();
95 for i in 0..partition_count {
96 let start = range.start + (i as i64) * per_partition;
97 let end = (start + per_partition).min(range.end);
98 if start < end {
99 ranges.push(start..end);
100 }
101 }
102
103 Self::try_new(ranges.len(), ranges)
104 }
105
106 pub fn get_range(&self, partition_id: usize) -> Option<Range<i64>> {
108 if partition_id < self.partition_ranges.len() {
109 Some(self.partition_ranges[partition_id].clone())
110 } else {
111 None
112 }
113 }
114
115 pub fn partition_size(&self, partition_id: usize) -> i64 {
117 self.get_range(partition_id)
118 .map(|r| r.end - r.start)
119 .unwrap_or(0)
120 }
121
122 pub fn total_size(&self) -> i64 {
124 self.partition_ranges.iter().map(|r| r.end - r.start).sum()
125 }
126}
127
128pub fn flatten_parallel_fallback_reason(visible_rows: usize, batch_size: usize) -> Option<String> {
137 let batch_size = batch_size.max(1);
138 if visible_rows > batch_size {
139 Some(format!(
140 "Flatten: single-partition batch execution (rows={visible_rows} > batch={batch_size}); morsel parallelism deferred"
141 ))
142 } else {
143 None
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_single_partition() {
153 let view = PartitionView::single(0i64..1000).expect("valid single partition");
154 assert_eq!(view.partition_count, 1);
155 assert_eq!(view.total_size(), 1000);
156 assert_eq!(view.get_range(0), Some(0i64..1000));
157 }
158
159 #[test]
160 fn test_split_partitions() {
161 let view = PartitionView::from_range(0i64..1000, 4).expect("valid split");
162 assert_eq!(view.partition_count, 4);
163 assert_eq!(view.total_size(), 1000);
164
165 let mut prev_end = 0i64;
167 for (i, range) in view.partition_ranges.iter().enumerate() {
168 assert_eq!(range.start, prev_end, "Partition {} is not contiguous", i);
169 prev_end = range.end;
170 }
171 assert_eq!(prev_end, 1000);
172 }
173
174 #[test]
175 fn test_uneven_split() {
176 let view = PartitionView::from_range(0i64..1001, 4).expect("valid uneven split");
177 assert_eq!(view.partition_count, 4);
178 assert_eq!(view.total_size(), 1001);
179 }
180
181 #[test]
182 fn rejects_invalid_partition_layout() {
183 let empty = PartitionView::try_new(0, Vec::new()).expect_err("empty layout must fail");
184 assert!(empty.to_string().contains("non-empty"));
185
186 let overlapping = PartitionView::try_new(2, vec![0i64..10, 5i64..20])
187 .expect_err("overlapping ranges must fail");
188 assert!(overlapping.to_string().contains("non-overlapping"));
189 }
190
191 #[test]
192 fn flatten_parallel_fallback_reason_reports_multi_batch() {
193 assert!(super::flatten_parallel_fallback_reason(10, 2048).is_none());
194 let reason = super::flatten_parallel_fallback_reason(5000, 2048)
195 .expect("multi-batch flatten must report a fallback reason");
196 assert!(reason.contains("single-partition"));
197 assert!(reason.contains("morsel parallelism deferred"));
198 }
199}