ic_query/cache/
completeness.rs1use serde::{Deserialize as SerdeDeserialize, Serialize};
9
10const API_EXHAUSTED_STATUS: &str = "api_exhausted";
11
12#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
19pub struct CacheCollectionCompleteness {
20 pub status: String,
22 pub page_size: u32,
24 pub page_count: u32,
26 pub row_count: usize,
28 pub point_in_time_guaranteed: bool,
30}
31
32impl CacheCollectionCompleteness {
33 #[must_use]
35 pub fn api_exhausted(
36 page_size: u32,
37 page_count: u32,
38 row_count: usize,
39 point_in_time_guaranteed: bool,
40 ) -> Self {
41 Self {
42 status: API_EXHAUSTED_STATUS.to_string(),
43 page_size,
44 page_count,
45 row_count,
46 point_in_time_guaranteed,
47 }
48 }
49
50 #[must_use]
52 pub fn is_api_exhausted(&self) -> bool {
53 self.status == API_EXHAUSTED_STATUS
54 }
55}
56
57pub fn validate_cache_collection_completeness(
59 completeness: &CacheCollectionCompleteness,
60 actual_row_count: usize,
61) -> Result<(), String> {
62 if !completeness.is_api_exhausted() {
63 return Err(format!(
64 "completeness status is {}, expected {API_EXHAUSTED_STATUS}",
65 completeness.status
66 ));
67 }
68 if completeness.page_size == 0 {
69 return Err("completeness page_size must be greater than zero".to_string());
70 }
71 if completeness.page_count == 0 {
72 return Err("completeness page_count must be greater than zero".to_string());
73 }
74 if completeness.row_count != actual_row_count {
75 return Err(format!(
76 "completeness row_count is {}, actual row count is {actual_row_count}",
77 completeness.row_count
78 ));
79 }
80 let maximum_row_count = u64::from(completeness.page_size) * u64::from(completeness.page_count);
81 if actual_row_count as u128 > u128::from(maximum_row_count) {
82 return Err(format!(
83 "completeness row_count is {actual_row_count}, maximum for {} pages of size {} is {maximum_row_count}",
84 completeness.page_count, completeness.page_size
85 ));
86 }
87 Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn completeness_requires_api_exhaustion_and_consistent_page_evidence() {
96 for page_size in 0..=2 {
97 for page_count in 0..=2 {
98 for declared_row_count in 0..=2 {
99 for actual_row_count in 0..=2 {
100 let completeness = CacheCollectionCompleteness::api_exhausted(
101 page_size,
102 page_count,
103 declared_row_count,
104 false,
105 );
106 let expected_valid = page_size > 0
107 && page_count > 0
108 && declared_row_count == actual_row_count
109 && actual_row_count <= (page_size * page_count) as usize;
110
111 assert_eq!(
112 validate_cache_collection_completeness(&completeness, actual_row_count)
113 .is_ok(),
114 expected_valid,
115 "page_size={page_size}, page_count={page_count}, declared_row_count={declared_row_count}, actual_row_count={actual_row_count}"
116 );
117 }
118 }
119 }
120 }
121
122 let mut unknown = CacheCollectionCompleteness::api_exhausted(1, 1, 0, false);
123 unknown.status = "unknown".to_string();
124 assert_eq!(
125 validate_cache_collection_completeness(&unknown, 0),
126 Err("completeness status is unknown, expected api_exhausted".to_string())
127 );
128 assert_eq!(
129 serde_json::to_value(CacheCollectionCompleteness::api_exhausted(1, 1, 0, false))
130 .expect("serialize completeness")["status"],
131 "api_exhausted"
132 );
133 }
134
135 #[test]
136 fn completeness_enforces_page_capacity_without_narrow_multiplication() {
137 for (page_size, page_count, row_count, valid) in [
138 (2, 2, 4, true),
139 (2, 2, 5, false),
140 (1, 1, 0, true),
141 (u32::MAX, 2, 100, true),
142 (u32::MAX, u32::MAX, 0, true),
143 ] {
144 let completeness =
145 CacheCollectionCompleteness::api_exhausted(page_size, page_count, row_count, false);
146 assert_eq!(
147 validate_cache_collection_completeness(&completeness, row_count).is_ok(),
148 valid
149 );
150 }
151 }
152}