1use crate::{db::ReadSetRevisionProof, value::OutputValue};
7use candid::CandidType;
8use serde::Deserialize;
9
10#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
12pub struct ScalarPageWork {
13 pub envelope_identity: u64,
15 pub entries_visited: u64,
17 pub result_rows: u32,
19}
20
21#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
26pub struct LiveQueryPageOutput {
27 pub entity: String,
29 pub columns: Vec<String>,
31 pub rows: Vec<Vec<OutputValue>>,
33 pub row_count: u32,
35 pub continuation: Option<String>,
37 pub work: ScalarPageWork,
39}
40
41#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
46pub struct ExhaustiveQueryPageOutput {
47 pub entity: String,
49 pub columns: Vec<String>,
51 pub rows: Vec<Vec<OutputValue>>,
53 pub row_count: u32,
55 pub continuation: Option<String>,
57 pub work: ScalarPageWork,
59 pub proof: ReadSetRevisionProof,
61}
62
63impl LiveQueryPageOutput {
64 #[must_use]
66 pub const fn len(&self) -> usize {
67 self.rows.len()
68 }
69
70 #[must_use]
72 pub const fn is_empty(&self) -> bool {
73 self.rows.is_empty()
74 }
75}
76
77impl ExhaustiveQueryPageOutput {
78 #[must_use]
80 pub const fn len(&self) -> usize {
81 self.rows.len()
82 }
83
84 #[must_use]
86 pub const fn is_empty(&self) -> bool {
87 self.rows.is_empty()
88 }
89
90 pub(in crate::db) fn from_live_page(
91 page: LiveQueryPageOutput,
92 proof: ReadSetRevisionProof,
93 ) -> Self {
94 Self {
95 entity: page.entity,
96 columns: page.columns,
97 rows: page.rows,
98 row_count: page.row_count,
99 continuation: page.continuation,
100 work: page.work,
101 proof,
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[derive(CandidType)]
111 struct FrozenScalarPageWorkWire {
112 envelope_identity: u64,
113 entries_visited: u64,
114 result_rows: u32,
115 }
116
117 #[derive(CandidType)]
118 struct FrozenLiveQueryPageOutputWire {
119 entity: String,
120 columns: Vec<String>,
121 rows: Vec<Vec<OutputValue>>,
122 row_count: u32,
123 continuation: Option<String>,
124 work: FrozenScalarPageWorkWire,
125 }
126
127 #[derive(CandidType)]
128 struct FrozenReadSetStoreIdentityWire([u8; 32]);
129
130 #[derive(CandidType)]
131 struct FrozenReadSetStoreRevisionWire {
132 store: FrozenReadSetStoreIdentityWire,
133 data_revision: u64,
134 access_state_revision: u64,
135 }
136
137 #[derive(CandidType)]
138 struct FrozenReadSetRevisionProofWire {
139 database_incarnation: [u8; 16],
140 accepted_root_revision: u64,
141 accepted_root_fingerprint_method: u8,
142 accepted_root_fingerprint: [u8; 32],
143 stores: Vec<FrozenReadSetStoreRevisionWire>,
144 }
145
146 #[derive(CandidType)]
147 struct FrozenExhaustiveQueryPageOutputWire {
148 entity: String,
149 columns: Vec<String>,
150 rows: Vec<Vec<OutputValue>>,
151 row_count: u32,
152 continuation: Option<String>,
153 work: FrozenScalarPageWorkWire,
154 proof: FrozenReadSetRevisionProofWire,
155 }
156
157 #[test]
158 fn live_query_page_output_preserves_its_initial_candid_record_shape() {
159 let current = LiveQueryPageOutput {
160 entity: "example".to_string(),
161 columns: vec!["id".to_string()],
162 rows: vec![vec![OutputValue::nat64(7)]],
163 row_count: 1,
164 continuation: Some("opaque".to_string()),
165 work: ScalarPageWork {
166 envelope_identity: 11,
167 entries_visited: 2,
168 result_rows: 1,
169 },
170 };
171 let frozen = FrozenLiveQueryPageOutputWire {
172 entity: current.entity.clone(),
173 columns: current.columns.clone(),
174 rows: current.rows.clone(),
175 row_count: current.row_count,
176 continuation: current.continuation.clone(),
177 work: FrozenScalarPageWorkWire {
178 envelope_identity: current.work.envelope_identity,
179 entries_visited: current.work.entries_visited,
180 result_rows: current.work.result_rows,
181 },
182 };
183
184 assert_eq!(current.len(), 1);
185 assert!(!current.is_empty());
186
187 assert_eq!(
188 candid::encode_one(¤t).expect("current live page should encode"),
189 candid::encode_one(&frozen).expect("frozen live page should encode"),
190 );
191 }
192
193 #[test]
194 fn exhaustive_query_page_output_freezes_its_initial_candid_record_shape() {
195 let proof = ReadSetRevisionProof::from_parts(
196 [1; 16],
197 7,
198 1,
199 [2; 32],
200 vec![crate::db::ReadSetStoreRevision::new(
201 crate::db::ReadSetStoreIdentity::from_bytes([3; 32]),
202 11,
203 13,
204 )],
205 )
206 .expect("bounded canonical proof should admit");
207 let current = ExhaustiveQueryPageOutput {
208 entity: "example".to_string(),
209 columns: vec!["id".to_string()],
210 rows: vec![vec![OutputValue::nat64(7)]],
211 row_count: 1,
212 continuation: Some("opaque".to_string()),
213 work: ScalarPageWork {
214 envelope_identity: 11,
215 entries_visited: 2,
216 result_rows: 1,
217 },
218 proof,
219 };
220 let frozen = FrozenExhaustiveQueryPageOutputWire {
221 entity: current.entity.clone(),
222 columns: current.columns.clone(),
223 rows: current.rows.clone(),
224 row_count: current.row_count,
225 continuation: current.continuation.clone(),
226 work: FrozenScalarPageWorkWire {
227 envelope_identity: current.work.envelope_identity,
228 entries_visited: current.work.entries_visited,
229 result_rows: current.work.result_rows,
230 },
231 proof: FrozenReadSetRevisionProofWire {
232 database_incarnation: current.proof.database_incarnation(),
233 accepted_root_revision: current.proof.accepted_root_revision(),
234 accepted_root_fingerprint_method: current.proof.accepted_root_fingerprint_method(),
235 accepted_root_fingerprint: current.proof.accepted_root_fingerprint(),
236 stores: current
237 .proof
238 .stores()
239 .iter()
240 .map(|store| FrozenReadSetStoreRevisionWire {
241 store: FrozenReadSetStoreIdentityWire(store.store().to_bytes()),
242 data_revision: store.data_revision(),
243 access_state_revision: store.access_state_revision(),
244 })
245 .collect(),
246 },
247 };
248
249 assert_eq!(current.len(), 1);
250 assert!(!current.is_empty());
251
252 assert_eq!(
253 candid::encode_one(¤t).expect("current exhaustive page should encode"),
254 candid::encode_one(&frozen).expect("frozen exhaustive page should encode"),
255 );
256 }
257}