1use std::{fmt, ops::RangeInclusive};
4
5use chrono::{DateTime, Duration, SecondsFormat, TimeZone, Utc};
6use snafu::Snafu;
7
8use crate::{
9 coverage::Bucket,
10 metadata::table_metadata::{
11 IndexKind, IndexValue, IndexValueError, TimeBucket, validate_index_range,
12 },
13};
14
15const SIGN_BIT: u64 = 0x8000_0000_0000_0000;
16const SECONDS_PER_MINUTE: u64 = 60;
17const SECONDS_PER_HOUR: u64 = 60 * 60;
18const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
19
20#[derive(Debug, Snafu, PartialEq, Eq)]
22pub enum BucketError {
23 #[snafu(display("Invalid ordered index value: {source}"))]
25 IndexValue {
26 source: IndexValueError,
28 },
29 #[snafu(display("Timestamp bucket width must be nonzero"))]
31 ZeroTimeBucket,
32 #[snafu(display("Ordered range end cannot be adjusted to its predecessor: {end}"))]
34 RangeEndUnderflow {
35 end: IndexValue,
37 },
38 #[snafu(display("Coverage bucket {bucket} is outside the logical {kind} index domain"))]
40 BucketOutsideDomain {
41 kind: &'static str,
43 bucket: Bucket,
45 },
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct LogicalBucketRange {
51 start: IndexValue,
52 end: IndexValue,
53 end_inclusive: bool,
54}
55
56impl LogicalBucketRange {
57 fn new(start: IndexValue, end: IndexValue, end_inclusive: bool) -> Self {
58 Self {
59 start,
60 end,
61 end_inclusive,
62 }
63 }
64
65 pub fn start(&self) -> &IndexValue {
67 &self.start
68 }
69
70 pub fn end(&self) -> &IndexValue {
72 &self.end
73 }
74
75 pub fn end_inclusive(&self) -> bool {
77 self.end_inclusive
78 }
79}
80
81impl fmt::Display for LogicalBucketRange {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 let close = if self.end_inclusive { ']' } else { ')' };
84 match (&self.start, &self.end) {
85 (IndexValue::Timestamp(start), IndexValue::Timestamp(end)) => write!(
86 f,
87 "[{}, {}{close}",
88 start.to_rfc3339_opts(SecondsFormat::AutoSi, true),
89 end.to_rfc3339_opts(SecondsFormat::AutoSi, true)
90 ),
91 (IndexValue::Int64(start), IndexValue::Int64(end)) => {
92 write!(f, "[{start}, {end}{close}")
93 }
94 (IndexValue::UInt64(start), IndexValue::UInt64(end)) => {
95 write!(f, "[{start}, {end}{close}")
96 }
97 _ => unreachable!("logical bucket range endpoints share one index domain"),
98 }
99 }
100}
101
102fn time_bucket_width_seconds(bucket: &TimeBucket) -> Result<u64, BucketError> {
103 let (value, multiplier) = match *bucket {
104 TimeBucket::Seconds(value) => (value, 1),
105 TimeBucket::Minutes(value) => (value, SECONDS_PER_MINUTE),
106 TimeBucket::Hours(value) => (value, SECONDS_PER_HOUR),
107 TimeBucket::Days(value) => (value, SECONDS_PER_DAY),
108 };
109 if value == 0 {
110 return Err(BucketError::ZeroTimeBucket);
111 }
112 Ok(u64::from(value) * multiplier)
113}
114
115fn signed_bucket_id(ordinal: i64) -> Bucket {
116 (ordinal as u64) ^ SIGN_BIT
117}
118
119pub fn bucket_id_from_epoch_secs(bucket: &TimeBucket, seconds: i64) -> Result<Bucket, BucketError> {
123 let width = i128::from(time_bucket_width_seconds(bucket)?);
124 let ordinal = i128::from(seconds).div_euclid(width) as i64;
125 Ok(signed_bucket_id(ordinal))
126}
127
128fn timestamp_bucket_id(bucket: &TimeBucket, value: DateTime<Utc>) -> Result<Bucket, BucketError> {
129 bucket_id_from_epoch_secs(bucket, value.timestamp())
130}
131
132fn int64_bucket_id(value: i64, bucket_width: u64) -> Bucket {
133 let ordinal = i128::from(value).div_euclid(i128::from(bucket_width)) as i64;
134 signed_bucket_id(ordinal)
135}
136
137pub fn bucket_id(kind: &IndexKind, value: &IndexValue) -> Result<Bucket, BucketError> {
139 value
140 .validate_kind(kind)
141 .map_err(|source| BucketError::IndexValue { source })?;
142
143 match (kind, value) {
144 (IndexKind::Timestamp { bucket, .. }, IndexValue::Timestamp(value)) => {
145 timestamp_bucket_id(bucket, *value)
146 }
147 (IndexKind::Int64 { bucket_width }, IndexValue::Int64(value)) => {
148 Ok(int64_bucket_id(*value, bucket_width.get()))
149 }
150 (IndexKind::UInt64 { bucket_width }, IndexValue::UInt64(value)) => {
151 Ok(*value / bucket_width.get())
152 }
153 _ => unreachable!("value domain was validated above"),
154 }
155}
156
157pub fn logical_bucket_range(
159 kind: &IndexKind,
160 bucket: Bucket,
161) -> Result<LogicalBucketRange, BucketError> {
162 let outside_domain = || BucketError::BucketOutsideDomain {
163 kind: kind.name(),
164 bucket,
165 };
166
167 match kind {
168 IndexKind::Timestamp {
169 bucket: time_bucket,
170 ..
171 } => {
172 let ordinal = i128::from((bucket ^ SIGN_BIT) as i64);
173 let width = i128::from(time_bucket_width_seconds(time_bucket)?);
174 let domain_start = i128::from(DateTime::<Utc>::MIN_UTC.timestamp());
175 let domain_end = i128::from(DateTime::<Utc>::MAX_UTC.timestamp()) + 1;
176 let start = (ordinal * width).max(domain_start);
177 let end = ((ordinal + 1) * width).min(domain_end);
178 if start >= end {
179 return Err(outside_domain());
180 }
181
182 let start = Utc
183 .timestamp_opt(start as i64, 0)
184 .single()
185 .ok_or_else(&outside_domain)?;
186 let end_inclusive = end == domain_end;
187 let end = if end_inclusive {
188 DateTime::<Utc>::MAX_UTC
189 } else {
190 Utc.timestamp_opt(end as i64, 0)
191 .single()
192 .ok_or_else(&outside_domain)?
193 };
194 Ok(LogicalBucketRange::new(
195 start.into(),
196 end.into(),
197 end_inclusive,
198 ))
199 }
200 IndexKind::Int64 { bucket_width } => {
201 let ordinal = i128::from((bucket ^ SIGN_BIT) as i64);
202 let width = i128::from(bucket_width.get());
203 let domain_start = i128::from(i64::MIN);
204 let domain_end = i128::from(i64::MAX) + 1;
205 let start = (ordinal * width).max(domain_start);
206 let end = ((ordinal + 1) * width).min(domain_end);
207 if start >= end {
208 return Err(outside_domain());
209 }
210
211 let end_inclusive = end == domain_end;
212 Ok(LogicalBucketRange::new(
213 IndexValue::Int64(start as i64),
214 IndexValue::Int64(if end_inclusive { i64::MAX } else { end as i64 }),
215 end_inclusive,
216 ))
217 }
218 IndexKind::UInt64 { bucket_width } => {
219 let width = u128::from(bucket_width.get());
220 let domain_end = u128::from(u64::MAX) + 1;
221 let start = u128::from(bucket) * width;
222 let end = ((u128::from(bucket) + 1) * width).min(domain_end);
223 if start >= end {
224 return Err(outside_domain());
225 }
226
227 let end_inclusive = end == domain_end;
228 Ok(LogicalBucketRange::new(
229 IndexValue::UInt64(start as u64),
230 IndexValue::UInt64(if end_inclusive { u64::MAX } else { end as u64 }),
231 end_inclusive,
232 ))
233 }
234 }
235}
236
237pub fn bucket_range(
239 kind: &IndexKind,
240 start: &IndexValue,
241 end: &IndexValue,
242) -> Result<RangeInclusive<Bucket>, BucketError> {
243 validate_index_range(kind, start, end).map_err(|source| BucketError::IndexValue { source })?;
244
245 let first = bucket_id(kind, start)?;
246 Ok(first..=bucket_id(kind, &value_before(end)?)?)
247}
248
249pub fn bucket_for_exclusive_end(kind: &IndexKind, end: &IndexValue) -> Result<Bucket, BucketError> {
251 end.validate_kind(kind)
252 .map_err(|source| BucketError::IndexValue { source })?;
253 bucket_id(kind, &value_before(end)?)
254}
255
256fn value_before(end: &IndexValue) -> Result<IndexValue, BucketError> {
257 Ok(match end {
258 IndexValue::Timestamp(end) => {
259 IndexValue::Timestamp(end.checked_sub_signed(Duration::nanoseconds(1)).ok_or(
260 BucketError::RangeEndUnderflow {
261 end: IndexValue::Timestamp(*end),
262 },
263 )?)
264 }
265 IndexValue::Int64(end) => IndexValue::Int64(end.checked_sub(1).ok_or({
266 BucketError::RangeEndUnderflow {
267 end: IndexValue::Int64(*end),
268 }
269 })?),
270 IndexValue::UInt64(end) => IndexValue::UInt64(end.checked_sub(1).ok_or({
271 BucketError::RangeEndUnderflow {
272 end: IndexValue::UInt64(*end),
273 }
274 })?),
275 })
276}
277
278#[cfg(test)]
279mod tests {
280 use std::num::NonZeroU64;
281
282 use chrono::TimeZone;
283
284 use super::*;
285
286 fn timestamp_kind(bucket: TimeBucket) -> IndexKind {
287 IndexKind::Timestamp {
288 bucket,
289 timezone: None,
290 }
291 }
292
293 #[test]
294 fn timestamp_mapping_is_ordered_across_epoch() {
295 let kind = timestamp_kind(TimeBucket::Seconds(1));
296 let before = Utc.timestamp_opt(-1, 0).single().unwrap().into();
297 let epoch = Utc.timestamp_opt(0, 0).single().unwrap().into();
298 let after = Utc.timestamp_opt(1, 0).single().unwrap().into();
299
300 assert_eq!(bucket_id(&kind, &before).unwrap(), SIGN_BIT - 1);
301 assert_eq!(bucket_id(&kind, &epoch).unwrap(), SIGN_BIT);
302 assert_eq!(bucket_id(&kind, &after).unwrap(), SIGN_BIT + 1);
303 }
304
305 #[test]
306 fn timestamp_mapping_uses_euclidean_buckets_before_epoch() {
307 let bucket = TimeBucket::Minutes(1);
308 assert_eq!(
309 bucket_id_from_epoch_secs(&bucket, -61).unwrap(),
310 SIGN_BIT - 2
311 );
312 assert_eq!(
313 bucket_id_from_epoch_secs(&bucket, -60).unwrap(),
314 SIGN_BIT - 1
315 );
316 assert_eq!(
317 bucket_id_from_epoch_secs(&bucket, -1).unwrap(),
318 SIGN_BIT - 1
319 );
320 assert_eq!(bucket_id_from_epoch_secs(&bucket, 0).unwrap(), SIGN_BIT);
321 }
322
323 #[test]
324 fn int64_mapping_handles_zero_and_extremes() {
325 for width in [1, 3, u64::MAX] {
326 let kind = IndexKind::Int64 {
327 bucket_width: NonZeroU64::new(width).unwrap(),
328 };
329 let values = [i64::MIN, -1, 0, 1, i64::MAX];
330 let buckets: Vec<_> = values
331 .into_iter()
332 .map(|value| bucket_id(&kind, &value.into()).unwrap())
333 .collect();
334 assert!(buckets.windows(2).all(|pair| pair[0] <= pair[1]));
335 }
336
337 let unit = IndexKind::Int64 {
338 bucket_width: NonZeroU64::new(1).unwrap(),
339 };
340 assert_eq!(bucket_id(&unit, &i64::MIN.into()).unwrap(), 0);
341 assert_eq!(bucket_id(&unit, &0i64.into()).unwrap(), SIGN_BIT);
342 assert_eq!(bucket_id(&unit, &i64::MAX.into()).unwrap(), u64::MAX);
343 }
344
345 #[test]
346 fn uint64_mapping_is_exact_through_max() {
347 let unit = IndexKind::UInt64 {
348 bucket_width: NonZeroU64::new(1).unwrap(),
349 };
350 for value in [0, i64::MAX as u64 + 1, u64::MAX] {
351 assert_eq!(bucket_id(&unit, &value.into()).unwrap(), value);
352 }
353
354 let width = IndexKind::UInt64 {
355 bucket_width: NonZeroU64::new(10).unwrap(),
356 };
357 assert_eq!(bucket_id(&width, &u64::MAX.into()).unwrap(), u64::MAX / 10);
358 }
359
360 #[test]
361 fn logical_bucket_ranges_use_configured_index_units() {
362 let signed_unit = IndexKind::Int64 {
363 bucket_width: NonZeroU64::new(1).unwrap(),
364 };
365 let signed_unit_bucket = bucket_id(&signed_unit, &50_464i64.into()).unwrap();
366 assert_eq!(
367 logical_bucket_range(&signed_unit, signed_unit_bucket)
368 .unwrap()
369 .to_string(),
370 "[50464, 50465)"
371 );
372
373 let signed = IndexKind::Int64 {
374 bucket_width: NonZeroU64::new(10).unwrap(),
375 };
376 let signed_bucket = bucket_id(&signed, &(-11i64).into()).unwrap();
377 assert_eq!(
378 logical_bucket_range(&signed, signed_bucket)
379 .unwrap()
380 .to_string(),
381 "[-20, -10)"
382 );
383
384 let unsigned = IndexKind::UInt64 {
385 bucket_width: NonZeroU64::new(10).unwrap(),
386 };
387 let unsigned_bucket = bucket_id(&unsigned, &50_464u64.into()).unwrap();
388 assert_eq!(
389 logical_bucket_range(&unsigned, unsigned_bucket)
390 .unwrap()
391 .to_string(),
392 "[50460, 50470)"
393 );
394
395 let timestamp = timestamp_kind(TimeBucket::Hours(1));
396 let epoch = Utc.timestamp_opt(0, 0).single().unwrap();
397 let timestamp_bucket = bucket_id(×tamp, &epoch.into()).unwrap();
398 assert_eq!(
399 logical_bucket_range(×tamp, timestamp_bucket)
400 .unwrap()
401 .to_string(),
402 "[1970-01-01T00:00:00Z, 1970-01-01T01:00:00Z)"
403 );
404
405 let before_epoch = Utc.timestamp_opt(-1, 0).single().unwrap();
406 let before_epoch_bucket = bucket_id(×tamp, &before_epoch.into()).unwrap();
407 assert_eq!(
408 logical_bucket_range(×tamp, before_epoch_bucket)
409 .unwrap()
410 .to_string(),
411 "[1969-12-31T23:00:00Z, 1970-01-01T00:00:00Z)"
412 );
413 }
414
415 #[test]
416 fn logical_bucket_ranges_clip_at_domain_maximum() -> Result<(), BucketError> {
417 let signed = IndexKind::Int64 {
418 bucket_width: NonZeroU64::new(10).unwrap(),
419 };
420 let signed_range =
421 logical_bucket_range(&signed, bucket_id(&signed, &i64::MAX.into()).unwrap())?;
422 assert_eq!(signed_range.end(), &IndexValue::Int64(i64::MAX));
423 assert!(signed_range.end_inclusive());
424 let signed_min_range =
425 logical_bucket_range(&signed, bucket_id(&signed, &i64::MIN.into()).unwrap())?;
426 assert_eq!(signed_min_range.start(), &IndexValue::Int64(i64::MIN));
427 assert!(!signed_min_range.end_inclusive());
428
429 let unsigned = IndexKind::UInt64 {
430 bucket_width: NonZeroU64::new(10).unwrap(),
431 };
432 let unsigned_range =
433 logical_bucket_range(&unsigned, bucket_id(&unsigned, &u64::MAX.into()).unwrap())?;
434 assert_eq!(unsigned_range.end(), &IndexValue::UInt64(u64::MAX));
435 assert!(unsigned_range.end_inclusive());
436
437 let timestamp = timestamp_kind(TimeBucket::Days(u32::MAX));
438 let timestamp_range = logical_bucket_range(
439 ×tamp,
440 bucket_id(×tamp, &IndexValue::Timestamp(DateTime::<Utc>::MAX_UTC))?,
441 )?;
442 assert_eq!(
443 timestamp_range.end(),
444 &IndexValue::Timestamp(DateTime::<Utc>::MAX_UTC)
445 );
446 assert!(timestamp_range.end_inclusive());
447 let timestamp_min_range = logical_bucket_range(
448 ×tamp,
449 bucket_id(×tamp, &IndexValue::Timestamp(DateTime::<Utc>::MIN_UTC))?,
450 )?;
451 assert_eq!(
452 timestamp_min_range.start(),
453 &IndexValue::Timestamp(DateTime::<Utc>::MIN_UTC)
454 );
455
456 Ok(())
457 }
458
459 #[test]
460 fn logical_bucket_range_rejects_unreachable_bucket() {
461 let kind = IndexKind::UInt64 {
462 bucket_width: NonZeroU64::new(2).unwrap(),
463 };
464 assert!(matches!(
465 logical_bucket_range(&kind, u64::MAX),
466 Err(BucketError::BucketOutsideDomain { .. })
467 ));
468 }
469
470 #[test]
471 fn half_open_integer_ranges_do_not_cross_end_boundary() {
472 let signed = IndexKind::Int64 {
473 bucket_width: NonZeroU64::new(10).unwrap(),
474 };
475 let range = bucket_range(&signed, &0i64.into(), &20i64.into()).unwrap();
476 assert_eq!(range, SIGN_BIT..=SIGN_BIT + 1);
477 assert_eq!(
478 bucket_for_exclusive_end(&signed, &20i64.into()).unwrap(),
479 SIGN_BIT + 1
480 );
481
482 let unsigned = IndexKind::UInt64 {
483 bucket_width: NonZeroU64::new(10).unwrap(),
484 };
485 assert_eq!(
486 bucket_range(&unsigned, &0u64.into(), &20u64.into()).unwrap(),
487 0..=1
488 );
489 assert_eq!(
490 bucket_range(&unsigned, &0u64.into(), &1u64.into()).unwrap(),
491 0..=0
492 );
493 }
494
495 #[test]
496 fn half_open_timestamp_range_preserves_nanoseconds() {
497 let kind = timestamp_kind(TimeBucket::Seconds(1));
498 let start = Utc.timestamp_opt(0, 0).single().unwrap();
499 let boundary = Utc.timestamp_opt(2, 0).single().unwrap();
500
501 assert_eq!(
502 bucket_range(&kind, &start.into(), &boundary.into()).unwrap(),
503 SIGN_BIT..=SIGN_BIT + 1
504 );
505 assert_eq!(
506 bucket_range(
507 &kind,
508 &start.into(),
509 &(boundary + Duration::nanoseconds(1)).into(),
510 )
511 .unwrap(),
512 SIGN_BIT..=SIGN_BIT + 2
513 );
514 }
515
516 #[test]
517 fn invalid_domains_ranges_and_zero_time_buckets_are_errors() {
518 let kind = timestamp_kind(TimeBucket::Seconds(0));
519 let epoch = Utc.timestamp_opt(0, 0).single().unwrap();
520 assert_eq!(
521 bucket_id(&kind, &epoch.into()),
522 Err(BucketError::ZeroTimeBucket)
523 );
524
525 let unsigned = IndexKind::UInt64 {
526 bucket_width: NonZeroU64::new(1).unwrap(),
527 };
528 assert!(matches!(
529 bucket_range(&unsigned, &0i64.into(), &1i64.into()),
530 Err(BucketError::IndexValue { .. })
531 ));
532 assert!(matches!(
533 bucket_range(&unsigned, &1u64.into(), &1u64.into()),
534 Err(BucketError::IndexValue { .. })
535 ));
536 assert!(matches!(
537 bucket_for_exclusive_end(&unsigned, &0u64.into()),
538 Err(BucketError::RangeEndUnderflow { .. })
539 ));
540 }
541}