Skip to main content

icydb_core/db/response/
page.rs

1//! Module: response::page
2//! Responsibility: public bounded scalar-page response payloads.
3//! Does not own: cursor validation, planning, or source revision proofs.
4//! Boundary: executor progress -> Candid-safe live page DTO.
5
6use crate::{db::ReadSetRevisionProof, value::OutputValue};
7use candid::CandidType;
8use serde::Deserialize;
9
10/// Bounded work observed while producing one scalar page.
11#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
12pub struct ScalarPageWork {
13    /// Exact identity of the operational work envelope used for this page.
14    pub envelope_identity: u64,
15    /// Physical keys or index entries visited by this page execution.
16    pub entries_visited: u64,
17    /// Logical rows returned to the caller.
18    pub result_rows: u32,
19}
20
21/// One revision-tolerant scalar keyset page.
22///
23/// A non-null continuation means traversal has not been proven exhausted. The
24/// token is authenticated but not encrypted and must be treated as opaque.
25#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
26pub struct LiveQueryPageOutput {
27    /// Accepted entity name used for the read.
28    pub entity: String,
29    /// Selected output-column names in row order.
30    pub columns: Vec<String>,
31    /// Row-oriented output values.
32    pub rows: Vec<Vec<OutputValue>>,
33    /// Number of returned rows.
34    pub row_count: u32,
35    /// Authenticated continuation, or `None` after proven exhaustion.
36    pub continuation: Option<String>,
37    /// Bounded work observed while producing this page.
38    pub work: ScalarPageWork,
39}
40
41/// One revision-strict exhaustive scalar page.
42///
43/// The returned proof must be persisted beside the continuation and supplied
44/// unchanged on resume. Any participating source change invalidates traversal.
45#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
46pub struct ExhaustiveQueryPageOutput {
47    /// Accepted entity name used for the read.
48    pub entity: String,
49    /// Selected output-column names in row order.
50    pub columns: Vec<String>,
51    /// Row-oriented output values.
52    pub rows: Vec<Vec<OutputValue>>,
53    /// Number of returned rows.
54    pub row_count: u32,
55    /// Authenticated continuation, or `None` after proof-bound exhaustion.
56    pub continuation: Option<String>,
57    /// Bounded work observed while producing this page.
58    pub work: ScalarPageWork,
59    /// Complete source authority that must accompany a resume.
60    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(&current).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(&current).expect("current exhaustive page should encode"),
222            candid::encode_one(&frozen).expect("frozen exhaustive page should encode"),
223        );
224    }
225}