linkrs_query/planning/plan/
partition_spec.rs1use std::ops::Range;
12use std::{error::Error, fmt};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum PartitionSource {
20 VertexId { tag: String },
22 EdgeId { edge_type: String },
24 Index { index_name: String },
26}
27
28impl fmt::Display for PartitionSource {
29 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::VertexId { tag } => write!(formatter, "vertex tag '{tag}'"),
32 Self::EdgeId { edge_type } => write!(formatter, "edge type '{edge_type}'"),
33 Self::Index { index_name } => write!(formatter, "index '{index_name}'"),
34 }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum PartitionStrategy {
49 Range,
50 Hash { key: String },
51 RoundRobin,
52}
53
54impl fmt::Display for PartitionStrategy {
55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::Range => write!(formatter, "range"),
58 Self::Hash { key } => write!(formatter, "hash(key='{key}')"),
59 Self::RoundRobin => write!(formatter, "round-robin"),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct PartitionSpec {
70 ranges: Vec<Range<i64>>,
71 source: PartitionSource,
73 strategy: PartitionStrategy,
75 partition_count: usize,
78 layout_version: Option<u64>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum PartitionSpecError {
87 Empty,
88 EmptyRange {
89 index: usize,
90 },
91 UnorderedOrOverlapping {
92 index: usize,
93 },
94 TooFewBuckets,
96 EmptyHashKey,
98}
99
100impl fmt::Display for PartitionSpecError {
101 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match self {
103 Self::Empty => write!(
104 formatter,
105 "partition layout must contain at least one range"
106 ),
107 Self::EmptyRange { index } => {
108 write!(
109 formatter,
110 "partition range at index {index} must not be empty"
111 )
112 }
113 Self::UnorderedOrOverlapping { index } => write!(
114 formatter,
115 "partition range at index {index} must be ordered and non-overlapping"
116 ),
117 Self::TooFewBuckets => {
118 write!(formatter, "partition layout requires at least two buckets")
119 }
120 Self::EmptyHashKey => {
121 write!(formatter, "hash partition key must not be empty")
122 }
123 }
124 }
125}
126
127impl Error for PartitionSpecError {}
128
129impl PartitionSpec {
130 pub fn try_new(
136 ranges: Vec<Range<i64>>,
137 source: PartitionSource,
138 layout_version: Option<u64>,
139 ) -> Result<Self, PartitionSpecError> {
140 if ranges.is_empty() {
141 return Err(PartitionSpecError::Empty);
142 }
143
144 let mut previous_end = None;
145 for (index, range) in ranges.iter().enumerate() {
146 if range.start >= range.end {
147 return Err(PartitionSpecError::EmptyRange { index });
148 }
149 if previous_end.is_some_and(|end| range.start < end) {
150 return Err(PartitionSpecError::UnorderedOrOverlapping { index });
151 }
152 previous_end = Some(range.end);
153 }
154
155 let partition_count = ranges.len();
156 Ok(Self {
157 ranges,
158 source,
159 strategy: PartitionStrategy::Range,
160 partition_count,
161 layout_version,
162 })
163 }
164
165 pub fn try_new_hash(
172 key: impl Into<String>,
173 ranges: Vec<Range<i64>>,
174 source: PartitionSource,
175 layout_version: Option<u64>,
176 ) -> Result<Self, PartitionSpecError> {
177 let key = key.into();
178 if key.is_empty() {
179 return Err(PartitionSpecError::EmptyHashKey);
180 }
181 Self::try_new_bucketed(
182 PartitionStrategy::Hash { key },
183 ranges,
184 source,
185 layout_version,
186 )
187 }
188
189 pub fn try_new_round_robin(
194 ranges: Vec<Range<i64>>,
195 source: PartitionSource,
196 layout_version: Option<u64>,
197 ) -> Result<Self, PartitionSpecError> {
198 Self::try_new_bucketed(
199 PartitionStrategy::RoundRobin,
200 ranges,
201 source,
202 layout_version,
203 )
204 }
205
206 fn try_new_bucketed(
207 strategy: PartitionStrategy,
208 ranges: Vec<Range<i64>>,
209 source: PartitionSource,
210 layout_version: Option<u64>,
211 ) -> Result<Self, PartitionSpecError> {
212 let spec = Self::try_new(ranges, source, layout_version)?;
216 Ok(Self {
217 strategy,
218 partition_count: spec.ranges.len(),
219 ..spec
220 })
221 }
222
223 pub fn strategy(&self) -> &PartitionStrategy {
225 &self.strategy
226 }
227
228 pub fn ranges(&self) -> &[Range<i64>] {
229 &self.ranges
230 }
231
232 pub fn partition_count(&self) -> usize {
233 self.partition_count
234 }
235
236 pub fn source(&self) -> &PartitionSource {
237 &self.source
238 }
239
240 pub fn layout_version(&self) -> Option<u64> {
241 self.layout_version
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 fn test_source() -> PartitionSource {
250 PartitionSource::VertexId {
251 tag: "test".to_string(),
252 }
253 }
254
255 #[test]
256 fn partition_spec_rejects_empty_and_overlapping_ranges() {
257 assert_eq!(
258 PartitionSpec::try_new(Vec::new(), test_source(), None),
259 Err(PartitionSpecError::Empty)
260 );
261 assert_eq!(
262 PartitionSpec::try_new(std::iter::once(0..0).collect(), test_source(), None),
263 Err(PartitionSpecError::EmptyRange { index: 0 })
264 );
265 assert_eq!(
266 PartitionSpec::try_new(vec![0..10, 5..20], test_source(), None),
267 Err(PartitionSpecError::UnorderedOrOverlapping { index: 1 })
268 );
269 }
270
271 #[test]
272 fn partition_spec_stores_source_and_layout_version() {
273 let spec = PartitionSpec::try_new(vec![0..10, 10..20], test_source(), Some(42))
274 .expect("valid spec");
275 assert_eq!(spec.source(), &test_source());
276 assert_eq!(spec.layout_version(), Some(42));
277 assert_eq!(spec.partition_count(), 2);
278 assert_eq!(spec.strategy(), &PartitionStrategy::Range);
279 }
280
281 #[test]
282 fn hash_spec_carries_key_and_bucket_count() {
283 let spec = PartitionSpec::try_new_hash("age", vec![0..50, 50..100], test_source(), Some(7))
284 .expect("valid hash spec");
285 assert_eq!(
286 spec.strategy(),
287 &PartitionStrategy::Hash {
288 key: "age".to_string()
289 }
290 );
291 assert_eq!(spec.partition_count(), 2);
292 assert_eq!(spec.layout_version(), Some(7));
293 assert_eq!(spec.ranges().len(), 2);
295 }
296
297 #[test]
298 fn hash_spec_rejects_empty_key() {
299 assert_eq!(
300 PartitionSpec::try_new_hash(
301 "",
302 std::iter::once(0..10i64).collect(),
303 test_source(),
304 None
305 )
306 .map(|_| ())
307 .unwrap_err(),
308 PartitionSpecError::EmptyHashKey
309 );
310 }
311
312 #[test]
313 fn round_robin_spec_is_bucketed() {
314 let spec = PartitionSpec::try_new_round_robin(vec![0..10, 10..20], test_source(), None)
315 .expect("valid round-robin spec");
316 assert_eq!(spec.strategy(), &PartitionStrategy::RoundRobin);
317 assert_eq!(spec.partition_count(), 2);
318 }
319}