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 ExhaustiveQueryPageOutput {
64 pub(in crate::db) fn from_live_page(
65 page: LiveQueryPageOutput,
66 proof: ReadSetRevisionProof,
67 ) -> Self {
68 Self {
69 entity: page.entity,
70 columns: page.columns,
71 rows: page.rows,
72 row_count: page.row_count,
73 continuation: page.continuation,
74 work: page.work,
75 proof,
76 }
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[derive(CandidType)]
85 struct FrozenScalarPageWorkWire {
86 envelope_identity: u64,
87 entries_visited: u64,
88 result_rows: u32,
89 }
90
91 #[derive(CandidType)]
92 struct FrozenLiveQueryPageOutputWire {
93 entity: String,
94 columns: Vec<String>,
95 rows: Vec<Vec<OutputValue>>,
96 row_count: u32,
97 continuation: Option<String>,
98 work: FrozenScalarPageWorkWire,
99 }
100
101 #[derive(CandidType)]
102 struct FrozenReadSetStoreIdentityWire([u8; 32]);
103
104 #[derive(CandidType)]
105 struct FrozenReadSetStoreRevisionWire {
106 store: FrozenReadSetStoreIdentityWire,
107 data_revision: u64,
108 access_state_revision: u64,
109 }
110
111 #[derive(CandidType)]
112 struct FrozenReadSetRevisionProofWire {
113 database_incarnation: [u8; 16],
114 accepted_root_revision: u64,
115 accepted_root_fingerprint_method: u8,
116 accepted_root_fingerprint: [u8; 32],
117 stores: Vec<FrozenReadSetStoreRevisionWire>,
118 }
119
120 #[derive(CandidType)]
121 struct FrozenExhaustiveQueryPageOutputWire {
122 entity: String,
123 columns: Vec<String>,
124 rows: Vec<Vec<OutputValue>>,
125 row_count: u32,
126 continuation: Option<String>,
127 work: FrozenScalarPageWorkWire,
128 proof: FrozenReadSetRevisionProofWire,
129 }
130
131 #[test]
132 fn live_query_page_output_preserves_its_initial_candid_record_shape() {
133 let current = LiveQueryPageOutput {
134 entity: "example".to_string(),
135 columns: vec!["id".to_string()],
136 rows: vec![vec![OutputValue::Nat64(7)]],
137 row_count: 1,
138 continuation: Some("opaque".to_string()),
139 work: ScalarPageWork {
140 envelope_identity: 11,
141 entries_visited: 2,
142 result_rows: 1,
143 },
144 };
145 let frozen = FrozenLiveQueryPageOutputWire {
146 entity: current.entity.clone(),
147 columns: current.columns.clone(),
148 rows: current.rows.clone(),
149 row_count: current.row_count,
150 continuation: current.continuation.clone(),
151 work: FrozenScalarPageWorkWire {
152 envelope_identity: current.work.envelope_identity,
153 entries_visited: current.work.entries_visited,
154 result_rows: current.work.result_rows,
155 },
156 };
157
158 assert_eq!(
159 candid::encode_one(¤t).expect("current live page should encode"),
160 candid::encode_one(&frozen).expect("frozen live page should encode"),
161 );
162 }
163
164 #[test]
165 fn exhaustive_query_page_output_freezes_its_initial_candid_record_shape() {
166 let proof = ReadSetRevisionProof::from_parts(
167 [1; 16],
168 7,
169 1,
170 [2; 32],
171 vec![crate::db::ReadSetStoreRevision::new(
172 crate::db::ReadSetStoreIdentity::from_bytes([3; 32]),
173 11,
174 13,
175 )],
176 )
177 .expect("bounded canonical proof should admit");
178 let current = ExhaustiveQueryPageOutput {
179 entity: "example".to_string(),
180 columns: vec!["id".to_string()],
181 rows: vec![vec![OutputValue::Nat64(7)]],
182 row_count: 1,
183 continuation: Some("opaque".to_string()),
184 work: ScalarPageWork {
185 envelope_identity: 11,
186 entries_visited: 2,
187 result_rows: 1,
188 },
189 proof,
190 };
191 let frozen = FrozenExhaustiveQueryPageOutputWire {
192 entity: current.entity.clone(),
193 columns: current.columns.clone(),
194 rows: current.rows.clone(),
195 row_count: current.row_count,
196 continuation: current.continuation.clone(),
197 work: FrozenScalarPageWorkWire {
198 envelope_identity: current.work.envelope_identity,
199 entries_visited: current.work.entries_visited,
200 result_rows: current.work.result_rows,
201 },
202 proof: FrozenReadSetRevisionProofWire {
203 database_incarnation: current.proof.database_incarnation(),
204 accepted_root_revision: current.proof.accepted_root_revision(),
205 accepted_root_fingerprint_method: current.proof.accepted_root_fingerprint_method(),
206 accepted_root_fingerprint: current.proof.accepted_root_fingerprint(),
207 stores: current
208 .proof
209 .stores()
210 .iter()
211 .map(|store| FrozenReadSetStoreRevisionWire {
212 store: FrozenReadSetStoreIdentityWire(store.store().to_bytes()),
213 data_revision: store.data_revision(),
214 access_state_revision: store.access_state_revision(),
215 })
216 .collect(),
217 },
218 };
219
220 assert_eq!(
221 candid::encode_one(¤t).expect("current exhaustive page should encode"),
222 candid::encode_one(&frozen).expect("frozen exhaustive page should encode"),
223 );
224 }
225}