Skip to content

Commit cf8d7ef

Browse files
add validations
1 parent b75e332 commit cf8d7ef

2 files changed

Lines changed: 103 additions & 10 deletions

File tree

src/query/mod.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ impl CountsRequest {
523523
let time_range = TimeRange::parse_human_time(&self.start_time, &self.end_time)?;
524524
let all_manifest_files = get_manifest_list(&self.stream, &time_range, tenant_id).await?;
525525
// get bounds
526-
let counts = self.get_bounds(&time_range);
526+
let counts = self.get_bounds(&time_range)?;
527527

528528
// we have start and end times for each bin
529529
// we also have all the manifest files for the given time range
@@ -566,7 +566,7 @@ impl CountsRequest {
566566
}
567567

568568
/// Calculate the end time for each bin based on the number of bins
569-
fn get_bounds(&self, time_range: &TimeRange) -> Vec<TimeBounds> {
569+
fn get_bounds(&self, time_range: &TimeRange) -> Result<Vec<TimeBounds>, QueryError> {
570570
let total_minutes = time_range
571571
.end
572572
.signed_duration_since(time_range.start)
@@ -591,6 +591,12 @@ impl CountsRequest {
591591
}
592592
};
593593

594+
if num_bins == 0 {
595+
return Err(QueryError::CustomError(
596+
"numBins must be greater than 0".to_string(),
597+
));
598+
}
599+
594600
// divide minutes by num bins to get minutes per bin
595601
let quotient = total_minutes / num_bins;
596602
let remainder = total_minutes % num_bins;
@@ -628,7 +634,7 @@ impl CountsRequest {
628634
});
629635
}
630636

631-
bounds
637+
Ok(bounds)
632638
}
633639

634640
/// This function will get executed only if self.conditions is some

src/utils/time.rs

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ pub enum TimeParseError {
3232
StartTimeAfterEndTime,
3333
}
3434

35+
#[derive(Debug, thiserror::Error)]
36+
pub enum TimeBinError {
37+
#[error("num_bins must be greater than 0")]
38+
InvalidNumBins,
39+
#[error("time bin interval is out of range")]
40+
IntervalOutOfRange,
41+
}
42+
3543
type Prefix = String;
3644

3745
#[derive(Clone, Copy)]
@@ -71,35 +79,71 @@ pub fn count_api_bin_interval(start: &DateTime<Utc>, end: &DateTime<Utc>) -> &'s
7179
}
7280
}
7381

74-
pub fn interval_for_num_bins(start: &DateTime<Utc>, end: &DateTime<Utc>, num_bins: u64) -> String {
82+
pub fn interval_for_num_bins(
83+
start: &DateTime<Utc>,
84+
end: &DateTime<Utc>,
85+
num_bins: u64,
86+
) -> Result<String, TimeBinError> {
87+
if num_bins == 0 {
88+
return Err(TimeBinError::InvalidNumBins);
89+
}
90+
7591
let total_seconds = end.signed_duration_since(*start).num_seconds().max(1) as u64;
7692
let bin_seconds = (total_seconds / num_bins).max(1);
77-
format!("{bin_seconds} seconds")
93+
Ok(format!("{bin_seconds} seconds"))
7894
}
7995

80-
pub fn expected_time_bins(time_range: &TimeRange, num_bins: u64) -> Vec<(String, String)> {
96+
pub fn expected_time_bins(
97+
time_range: &TimeRange,
98+
num_bins: u64,
99+
) -> Result<Vec<(String, String)>, TimeBinError> {
100+
if num_bins == 0 {
101+
return Err(TimeBinError::InvalidNumBins);
102+
}
103+
81104
let total_seconds = time_range
82105
.end
83106
.signed_duration_since(time_range.start)
84107
.num_seconds()
85108
.max(1) as u64;
86109
let bin_seconds = (total_seconds / num_bins).max(1);
110+
let bin_seconds_i64 =
111+
i64::try_from(bin_seconds).map_err(|_| TimeBinError::IntervalOutOfRange)?;
112+
let first_bin_start =
113+
time_range.start.timestamp().div_euclid(bin_seconds_i64) * bin_seconds_i64;
114+
let first_bin_start =
115+
DateTime::from_timestamp(first_bin_start, 0).ok_or(TimeBinError::IntervalOutOfRange)?;
87116

88117
(0..num_bins)
89118
.map(|i| {
90-
let bin_start = time_range.start + chrono::Duration::seconds((i * bin_seconds) as i64);
119+
let offset = i64::try_from(i)
120+
.ok()
121+
.and_then(|i| i.checked_mul(bin_seconds_i64))
122+
.ok_or(TimeBinError::IntervalOutOfRange)?;
123+
let bin_start = first_bin_start
124+
.checked_add_signed(chrono::Duration::seconds(offset))
125+
.ok_or(TimeBinError::IntervalOutOfRange)?;
91126
let bin_end = if i == num_bins - 1 {
92127
time_range.end
93128
} else {
94-
time_range.start + chrono::Duration::seconds(((i + 1) * bin_seconds) as i64)
129+
bin_start
130+
.checked_add_signed(chrono::Duration::seconds(bin_seconds_i64))
131+
.ok_or(TimeBinError::IntervalOutOfRange)?
95132
};
96-
(bin_start.to_rfc3339(), bin_end.to_rfc3339())
133+
Ok((bin_start.to_rfc3339(), bin_end.to_rfc3339()))
97134
})
98135
.collect()
99136
}
100137

101138
pub fn match_time_bin_key(sql_bin_start: &str, expected_bins: &[(String, String)]) -> String {
102-
let normalized = if !sql_bin_start.contains('+') && !sql_bin_start.ends_with('Z') {
139+
let has_timezone = sql_bin_start.ends_with('Z')
140+
|| DateTime::parse_from_rfc3339(sql_bin_start).is_ok()
141+
|| sql_bin_start
142+
.rsplit_once(['+', '-'])
143+
.is_some_and(|(_, suffix)| {
144+
suffix.len() == 5 && suffix.as_bytes().get(2) == Some(&b':')
145+
});
146+
let normalized = if !has_timezone {
103147
format!("{sql_bin_start}+00:00")
104148
} else {
105149
sql_bin_start.to_string()
@@ -507,6 +551,49 @@ mod tests {
507551
assert!(matches!(result, Err(TimeParseError::HumanTime(_))));
508552
}
509553

554+
#[test]
555+
fn interval_for_num_bins_rejects_zero_bins() {
556+
let start = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
557+
let end = Utc.with_ymd_and_hms(2023, 1, 1, 12, 10, 0).unwrap();
558+
559+
let result = interval_for_num_bins(&start, &end, 0);
560+
561+
assert!(matches!(result, Err(TimeBinError::InvalidNumBins)));
562+
}
563+
564+
#[test]
565+
fn expected_time_bins_rejects_zero_bins() {
566+
let time_range = time_period_from_str("2023-01-01T12:00:00Z", "2023-01-01T12:10:00Z");
567+
568+
let result = expected_time_bins(&time_range, 0);
569+
570+
assert!(matches!(result, Err(TimeBinError::InvalidNumBins)));
571+
}
572+
573+
#[test]
574+
fn expected_time_bins_aligns_to_epoch_anchor() {
575+
let time_range = time_period_from_str("2023-01-01T12:02:00Z", "2023-01-01T12:12:00Z");
576+
577+
let bins = expected_time_bins(&time_range, 2).unwrap();
578+
579+
assert_eq!(bins[0].0, "2023-01-01T12:00:00+00:00");
580+
assert_eq!(bins[0].1, "2023-01-01T12:05:00+00:00");
581+
assert_eq!(bins[1].0, "2023-01-01T12:05:00+00:00");
582+
assert_eq!(bins[1].1, "2023-01-01T12:12:00+00:00");
583+
}
584+
585+
#[test]
586+
fn match_time_bin_key_handles_negative_timezone_offsets() {
587+
let expected_bins = vec![(
588+
"2023-01-01T05:00:00+00:00".to_string(),
589+
"2023-01-01T06:00:00+00:00".to_string(),
590+
)];
591+
592+
let matched = match_time_bin_key("2023-01-01T00:00:00-05:00", &expected_bins);
593+
594+
assert_eq!(matched, "2023-01-01T05:00:00+00:00");
595+
}
596+
510597
fn time_period_from_str(start: &str, end: &str) -> TimeRange {
511598
TimeRange {
512599
start: DateTime::parse_from_rfc3339(start).unwrap().into(),

0 commit comments

Comments
 (0)