krabka_restore/bound/
compile.rs1use {
11 super::{ExcludedOffsets, Predicates},
12 crate::{
13 args::{PartitionRef, RestoreArgs},
14 error::RestoreError,
15 },
16 krabka_ids::Offset,
17 std::collections::HashMap,
18};
19
20impl Predicates {
21 pub fn from_args(args: &RestoreArgs) -> Result<Self, RestoreError> {
42 let mut to_offset = HashMap::with_capacity(args.to_offset.len());
43 for bound in &args.to_offset {
44 to_offset.insert(bound.partition.clone(), bound.last_offset);
45 }
46
47 let mut exclude_offset: HashMap<PartitionRef, Vec<ExcludedOffsets>> = HashMap::new();
48 for range in &args.exclude_offset {
49 exclude_offset
50 .entry(range.partition.clone())
51 .or_default()
52 .push(ExcludedOffsets {
53 start: range.start,
54 end_exclusive: range.end_exclusive,
55 });
56 }
57
58 for (partition, &last_offset) in &to_offset {
59 let fully_excluded = exclude_offset
60 .get(partition)
61 .is_some_and(|ranges| fully_covers(ranges, last_offset));
62 if fully_excluded {
63 return Err(RestoreError::InvalidArgument(format!(
64 "--exclude-offset excludes every offset that --to-offset keeps in {partition}"
65 )));
66 }
67 }
68
69 Ok(Self {
70 to_offset,
71 to_timestamp: args.to_timestamp,
72 exclude_key: args.exclude_key.clone(),
73 exclude_header: args.exclude_header.clone(),
74 exclude_producer_id: args.exclude_producer_id.iter().copied().collect(),
75 exclude_offset,
76 })
77 }
78}
79
80fn fully_covers(ranges: &[ExcludedOffsets], last_offset: Offset) -> bool {
87 let mut sorted = ranges.to_vec();
88 sorted.sort_unstable_by_key(|range| range.start);
89
90 let mut covered_through = Offset::ZERO;
91 for ExcludedOffsets {
92 start,
93 end_exclusive,
94 } in sorted
95 {
96 if start > covered_through {
97 return false;
98 }
99 if end_exclusive > covered_through {
100 covered_through = end_exclusive;
101 }
102 if covered_through > last_offset {
103 return true;
104 }
105 }
106 covered_through > last_offset
107}
108
109#[cfg(test)]
110mod tests {
111 use {super::*, crate::bound::test_support::args_from, assert2::check};
112
113 #[test]
114 fn exclude_offset_fully_covering_the_to_offset_window_is_rejected() {
115 let result = Predicates::from_args(&args_from(&[
116 "--to-offset",
117 "orders:0=5",
118 "--exclude-offset",
119 "orders:0=0..6",
120 ]));
121
122 check!(matches!(result, Err(RestoreError::InvalidArgument(_))));
123 }
124
125 #[test]
126 fn exclude_offset_covering_the_window_through_several_ranges_is_rejected() {
127 let result = Predicates::from_args(&args_from(&[
128 "--to-offset",
129 "orders:0=5",
130 "--exclude-offset",
131 "orders:0=0..3",
132 "--exclude-offset",
133 "orders:0=3..6",
134 ]));
135
136 check!(matches!(result, Err(RestoreError::InvalidArgument(_))));
137 }
138
139 #[test]
140 fn exclude_offset_leaving_any_gap_in_the_window_is_accepted() {
141 let result = Predicates::from_args(&args_from(&[
142 "--to-offset",
143 "orders:0=5",
144 "--exclude-offset",
145 "orders:0=0..3",
146 ]));
147
148 check!(result.is_ok());
149 }
150
151 #[test]
152 fn exclude_offset_covering_an_unrelated_partition_does_not_reject() {
153 let result = Predicates::from_args(&args_from(&[
154 "--to-offset",
155 "orders:0=5",
156 "--exclude-offset",
157 "orders:1=0..100",
158 ]));
159
160 check!(result.is_ok());
161 }
162
163 #[test]
164 fn no_to_offset_bound_means_no_coverage_check_at_all() {
165 let result =
166 Predicates::from_args(&args_from(&["--exclude-offset", "orders:0=0..1000000"]));
167
168 check!(result.is_ok());
169 }
170}