Skip to main content

rhiza_sql/
page_state.rs

1use rhiza_core::LogHash;
2use serde::{Deserialize, Serialize};
3
4use crate::{Error, Result};
5
6const MIN_SQLITE_PAGE_SIZE: u32 = 512;
7const MAX_SQLITE_PAGE_SIZE: u32 = 65_536;
8const LEAF_DOMAIN: &[u8] = b"rhiza:qwal-v3:page-state:leaf\0";
9const EMPTY_DOMAIN: &[u8] = b"rhiza:qwal-v3:page-state:empty\0";
10const INTERNAL_DOMAIN: &[u8] = b"rhiza:qwal-v3:page-state:internal\0";
11const STATE_DOMAIN: &[u8] = b"rhiza:qwal-v3:page-state:root\0";
12
13/// The content identity of a closed SQLite database in QWAL v3.
14///
15/// `state_root` binds the page size, page count, and canonical Merkle root.
16#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
17#[serde(deny_unknown_fields)]
18pub struct StateIdentityV3 {
19    pub page_size: u32,
20    pub page_count: u32,
21    pub state_root: LogHash,
22}
23
24/// A borrowed final page image used to calculate or install a state change.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub(crate) struct PageStatePatchV3<'a> {
27    page_no: u32,
28    after_image: &'a [u8],
29}
30
31impl<'a> PageStatePatchV3<'a> {
32    pub(crate) const fn new(page_no: u32, after_image: &'a [u8]) -> Self {
33        Self {
34            page_no,
35            after_image,
36        }
37    }
38}
39
40/// Dense, rebuildable Merkle state for closed SQLite page images.
41///
42/// This cache is not authoritative. Callers must rebuild it from the closed
43/// database whenever its provenance or consistency is uncertain.
44#[derive(Clone, Debug, Eq, PartialEq)]
45pub(crate) struct PageStateCacheV3 {
46    page_size: u32,
47    page_count: u32,
48    // `levels[0]` contains leaves. Each later level contains the parents of
49    // the preceding level, with canonical empty subtrees filling the right
50    // edge up to the next power of two.
51    levels: Vec<Vec<LogHash>>,
52}
53
54#[derive(Clone, Copy)]
55struct HashedPatch {
56    page_index: u64,
57    hash: LogHash,
58}
59
60impl PageStateCacheV3 {
61    /// Rebuilds the cache from every page in canonical one-based order.
62    pub(crate) fn from_pages<I, P>(page_size: u32, pages: I) -> Result<Self>
63    where
64        I: IntoIterator<Item = P>,
65        P: AsRef<[u8]>,
66    {
67        validate_page_size(page_size)?;
68
69        let mut leaves = Vec::new();
70        for page in pages {
71            let page = page.as_ref();
72            if page.len() != page_size as usize {
73                return invalid("page image length does not match page size");
74            }
75            let page_no = u32::try_from(leaves.len() + 1)
76                .map_err(|_| Error::ResourceExhausted("page count exceeds u32".into()))?;
77            leaves.push(hash_leaf(page_no, page));
78        }
79        let page_count = u32::try_from(leaves.len())
80            .map_err(|_| Error::ResourceExhausted("page count exceeds u32".into()))?;
81        validate_page_count(page_count)?;
82
83        let empty = empty_subtrees(tree_height(page_count));
84        let mut levels = vec![leaves];
85        for height in 1..=tree_height(page_count) as usize {
86            let children = &levels[height - 1];
87            let mut parents = Vec::with_capacity(children.len().div_ceil(2));
88            for pair in children.chunks(2) {
89                let right = pair.get(1).copied().unwrap_or(empty[height - 1]);
90                parents.push(hash_internal(height as u32, pair[0], right));
91            }
92            levels.push(parents);
93        }
94
95        Ok(Self {
96            page_size,
97            page_count,
98            levels,
99        })
100    }
101
102    pub(crate) fn identity(&self) -> StateIdentityV3 {
103        identity(self.page_size, self.page_count, self.tree_root())
104    }
105
106    /// Calculates the target identity without mutating the dense cache.
107    pub(crate) fn overlay(
108        &self,
109        target_page_count: u32,
110        patches: &[PageStatePatchV3<'_>],
111    ) -> Result<StateIdentityV3> {
112        let hashed = self.validate_and_hash(target_page_count, patches)?;
113        Ok(self.overlay_hashed(target_page_count, &hashed))
114    }
115
116    /// Atomically validates and installs a page patch into the cache.
117    ///
118    /// Validation and target-root calculation finish before the cache is
119    /// changed, so every returned error leaves the cache untouched.
120    pub(crate) fn apply_patch(
121        &mut self,
122        target_page_count: u32,
123        patches: &[PageStatePatchV3<'_>],
124    ) -> Result<StateIdentityV3> {
125        let hashed = self.validate_and_hash(target_page_count, patches)?;
126        let target = self.overlay_hashed(target_page_count, &hashed);
127        self.apply_hashed(target_page_count, &hashed);
128        debug_assert_eq!(self.identity(), target);
129        Ok(target)
130    }
131
132    fn tree_root(&self) -> LogHash {
133        self.levels
134            .last()
135            .and_then(|level| level.first())
136            .copied()
137            .expect("validated page-state caches are non-empty")
138    }
139
140    fn validate_and_hash(
141        &self,
142        target_page_count: u32,
143        patches: &[PageStatePatchV3<'_>],
144    ) -> Result<Vec<HashedPatch>> {
145        validate_page_count(target_page_count)?;
146        usize::try_from(target_page_count)
147            .map_err(|_| Error::ResourceExhausted("page count exceeds usize".into()))?;
148
149        let mut previous = 0;
150        let mut hashed = Vec::with_capacity(patches.len());
151        for patch in patches {
152            if patch.page_no == 0 {
153                return invalid("page numbers must be one-based");
154            }
155            if patch.page_no <= previous {
156                return invalid("page patches must be strictly ordered without duplicates");
157            }
158            if patch.page_no > target_page_count {
159                return invalid("page patch lies outside the target state");
160            }
161            if patch.after_image.len() != self.page_size as usize {
162                return invalid("page image length does not match page size");
163            }
164            hashed.push(HashedPatch {
165                page_index: u64::from(patch.page_no - 1),
166                hash: hash_leaf(patch.page_no, patch.after_image),
167            });
168            previous = patch.page_no;
169        }
170
171        if target_page_count > self.page_count {
172            let first_new = patches.partition_point(|patch| patch.page_no <= self.page_count);
173            let required = u64::from(target_page_count - self.page_count);
174            let supplied = u64::try_from(patches.len() - first_new)
175                .map_err(|_| Error::ResourceExhausted("page patch count exceeds u64".into()))?;
176            if supplied != required {
177                return invalid("growth must include every newly allocated page");
178            }
179            for (offset, patch) in patches[first_new..].iter().enumerate() {
180                let offset = u32::try_from(offset)
181                    .map_err(|_| Error::ResourceExhausted("page patch count exceeds u32".into()))?;
182                if patch.page_no != self.page_count + 1 + offset {
183                    return invalid("growth must include a complete new page suffix");
184                }
185            }
186        }
187
188        Ok(hashed)
189    }
190
191    fn overlay_hashed(&self, target_page_count: u32, patches: &[HashedPatch]) -> StateIdentityV3 {
192        let height = tree_height(target_page_count);
193        let empty = empty_subtrees(height);
194        let tree_root = self.overlay_node(0, height, u64::from(target_page_count), patches, &empty);
195        identity(self.page_size, target_page_count, tree_root)
196    }
197
198    fn overlay_node(
199        &self,
200        start: u64,
201        height: u32,
202        target_page_count: u64,
203        patches: &[HashedPatch],
204        empty: &[LogHash],
205    ) -> LogHash {
206        if start >= target_page_count {
207            return empty[height as usize];
208        }
209
210        let width = 1_u64 << height;
211        let end = start + width;
212        if patches.is_empty() && end <= u64::from(self.page_count) && end <= target_page_count {
213            return self.levels[height as usize][(start >> height) as usize];
214        }
215
216        if height == 0 {
217            if let Some(patch) = patches.first() {
218                debug_assert_eq!(patch.page_index, start);
219                return patch.hash;
220            }
221            return self.levels[0][start as usize];
222        }
223
224        let midpoint = start + (width >> 1);
225        let split = patches.partition_point(|patch| patch.page_index < midpoint);
226        let left = self.overlay_node(
227            start,
228            height - 1,
229            target_page_count,
230            &patches[..split],
231            empty,
232        );
233        let right = self.overlay_node(
234            midpoint,
235            height - 1,
236            target_page_count,
237            &patches[split..],
238            empty,
239        );
240        hash_internal(height, left, right)
241    }
242
243    fn apply_hashed(&mut self, target_page_count: u32, patches: &[HashedPatch]) {
244        let target_len = target_page_count as usize;
245        let old_page_count = self.page_count;
246        let height = tree_height(target_page_count) as usize;
247        let empty = empty_subtrees(height as u32);
248
249        self.levels[0].resize(target_len, empty[0]);
250        self.levels[0].truncate(target_len);
251        for patch in patches {
252            self.levels[0][patch.page_index as usize] = patch.hash;
253        }
254
255        let mut dirty: Vec<usize> = patches
256            .iter()
257            .map(|patch| patch.page_index as usize)
258            .collect();
259        if target_page_count < old_page_count {
260            dirty.push(target_len - 1);
261            dirty.sort_unstable();
262            dirty.dedup();
263        }
264
265        self.levels.resize_with(height + 1, Vec::new);
266        for current_height in 1..=height {
267            let desired_len = target_len.div_ceil(1_usize << current_height);
268            self.levels[current_height].resize(desired_len, empty[current_height]);
269            self.levels[current_height].truncate(desired_len);
270
271            let mut parents: Vec<usize> = dirty.iter().map(|index| index / 2).collect();
272            parents.dedup();
273            for parent in &parents {
274                let left_index = parent * 2;
275                let (left, right) = {
276                    let children = &self.levels[current_height - 1];
277                    (
278                        children[left_index],
279                        children
280                            .get(left_index + 1)
281                            .copied()
282                            .unwrap_or(empty[current_height - 1]),
283                    )
284                };
285                self.levels[current_height][*parent] =
286                    hash_internal(current_height as u32, left, right);
287            }
288            dirty = parents;
289        }
290        self.levels.truncate(height + 1);
291        self.page_count = target_page_count;
292    }
293}
294
295fn validate_page_size(page_size: u32) -> Result<()> {
296    if !(MIN_SQLITE_PAGE_SIZE..=MAX_SQLITE_PAGE_SIZE).contains(&page_size)
297        || !page_size.is_power_of_two()
298    {
299        return invalid("page size must be a power of two from 512 through 65536");
300    }
301    Ok(())
302}
303
304fn validate_page_count(page_count: u32) -> Result<()> {
305    if page_count == 0 {
306        return invalid("page count must be positive");
307    }
308    Ok(())
309}
310
311fn tree_height(page_count: u32) -> u32 {
312    u32::BITS - (page_count - 1).leading_zeros()
313}
314
315fn hash_leaf(page_no: u32, page: &[u8]) -> LogHash {
316    let page_no = page_no.to_be_bytes();
317    LogHash::digest(&[LEAF_DOMAIN, &page_no, page])
318}
319
320fn hash_internal(height: u32, left: LogHash, right: LogHash) -> LogHash {
321    let height = height.to_be_bytes();
322    LogHash::digest(&[INTERNAL_DOMAIN, &height, left.as_bytes(), right.as_bytes()])
323}
324
325fn empty_subtrees(height: u32) -> Vec<LogHash> {
326    let mut empty = Vec::with_capacity(height as usize + 1);
327    empty.push(LogHash::digest(&[EMPTY_DOMAIN]));
328    for current_height in 1..=height {
329        let child = empty[current_height as usize - 1];
330        empty.push(hash_internal(current_height, child, child));
331    }
332    empty
333}
334
335fn identity(page_size: u32, page_count: u32, tree_root: LogHash) -> StateIdentityV3 {
336    let page_size_bytes = page_size.to_be_bytes();
337    let page_count_bytes = page_count.to_be_bytes();
338    StateIdentityV3 {
339        page_size,
340        page_count,
341        state_root: LogHash::digest(&[
342            STATE_DOMAIN,
343            &page_size_bytes,
344            &page_count_bytes,
345            tree_root.as_bytes(),
346        ]),
347    }
348}
349
350fn invalid<T>(message: impl Into<String>) -> Result<T> {
351    Err(Error::InvalidEntry(format!(
352        "invalid QWAL v3 page state: {}",
353        message.into()
354    )))
355}
356
357#[cfg(test)]
358mod tests {
359    use proptest::prelude::*;
360
361    use super::*;
362
363    const PAGE_SIZE: usize = 512;
364
365    fn page(byte: u8) -> Vec<u8> {
366        vec![byte; PAGE_SIZE]
367    }
368
369    fn cache(pages: &[Vec<u8>]) -> PageStateCacheV3 {
370        PageStateCacheV3::from_pages(PAGE_SIZE as u32, pages).unwrap()
371    }
372
373    fn patches(pages: &[(u32, Vec<u8>)]) -> Vec<PageStatePatchV3<'_>> {
374        pages
375            .iter()
376            .map(|(page_no, image)| PageStatePatchV3::new(*page_no, image))
377            .collect()
378    }
379
380    #[test]
381    fn identity_binds_page_number_size_count_and_contents() {
382        let first = cache(&[page(1), page(2)]).identity();
383        let reordered = cache(&[page(2), page(1)]).identity();
384        let changed = cache(&[page(1), page(3)]).identity();
385        let shorter = cache(&[page(1)]).identity();
386        let larger_pages = PageStateCacheV3::from_pages(1024, [vec![1; 1024], vec![2; 1024]])
387            .unwrap()
388            .identity();
389
390        assert_ne!(first, reordered);
391        assert_ne!(first, changed);
392        assert_ne!(first.state_root, shorter.state_root);
393        assert_ne!(first.state_root, larger_pages.state_root);
394    }
395
396    #[test]
397    fn overlay_is_non_mutating_and_apply_matches_full_rebuild() {
398        let mut pages = vec![page(1), page(2), page(3), page(4)];
399        let mut state = cache(&pages);
400        let before = state.clone();
401        let changed = vec![(2, page(8)), (4, page(9))];
402        let patch = patches(&changed);
403
404        let overlaid = state.overlay(4, &patch).unwrap();
405        assert_eq!(state, before);
406        pages[1] = changed[0].1.clone();
407        pages[3] = changed[1].1.clone();
408        assert_eq!(overlaid, cache(&pages).identity());
409        assert_eq!(state.apply_patch(4, &patch).unwrap(), overlaid);
410        assert_eq!(state.identity(), cache(&pages).identity());
411    }
412
413    #[test]
414    fn growth_requires_the_complete_new_suffix_without_mutating_on_error() {
415        let mut state = cache(&[page(1), page(2)]);
416        let before = state.clone();
417        let incomplete = vec![(3, page(3)), (5, page(5))];
418
419        assert!(state.apply_patch(5, &patches(&incomplete)).is_err());
420        assert_eq!(state, before);
421
422        let complete = vec![(3, page(3)), (4, page(4)), (5, page(5))];
423        let identity = state.apply_patch(5, &patches(&complete)).unwrap();
424        assert_eq!(
425            identity,
426            cache(&[page(1), page(2), page(3), page(4), page(5)]).identity()
427        );
428    }
429
430    #[test]
431    fn shrink_prunes_removed_pages_before_later_growth() {
432        let mut state = cache(&[page(1), page(2), page(3), page(4), page(5)]);
433        let shrunk = state.apply_patch(2, &[]).unwrap();
434        assert_eq!(shrunk, cache(&[page(1), page(2)]).identity());
435
436        let replacement = vec![(3, page(9)), (4, page(8)), (5, page(7))];
437        let regrown = state.apply_patch(5, &patches(&replacement)).unwrap();
438        assert_eq!(
439            regrown,
440            cache(&[page(1), page(2), page(9), page(8), page(7)]).identity()
441        );
442    }
443
444    #[test]
445    fn invalid_patch_shapes_fail_closed() {
446        let cases = [
447            vec![(0, page(9))],
448            vec![(2, page(8)), (1, page(9))],
449            vec![(1, page(8)), (1, page(9))],
450            vec![(4, page(9))],
451            vec![(1, vec![0; PAGE_SIZE - 1])],
452        ];
453
454        for malformed in &cases {
455            let mut state = cache(&[page(1), page(2), page(3)]);
456            let before = state.clone();
457            assert!(state.apply_patch(3, &patches(malformed)).is_err());
458            assert_eq!(state, before);
459        }
460    }
461
462    #[test]
463    fn rebuild_rejects_invalid_page_geometry() {
464        assert!(PageStateCacheV3::from_pages(511, [vec![0; 511]]).is_err());
465        assert!(PageStateCacheV3::from_pages(512, Vec::<Vec<u8>>::new()).is_err());
466        assert!(PageStateCacheV3::from_pages(512, [vec![0; 511]]).is_err());
467    }
468
469    proptest! {
470        #![proptest_config(ProptestConfig::with_cases(64))]
471
472        #[test]
473        fn incremental_identity_matches_full_rebuild_across_mixed_changes(
474            initial_len in 1usize..8,
475            initial_seed in any::<u8>(),
476            operations in prop::collection::vec((0u8..3, any::<u8>(), 0usize..16), 1..40),
477        ) {
478            let mut pages: Vec<Vec<u8>> = (0..initial_len)
479                .map(|index| page(initial_seed.wrapping_add(index as u8)))
480                .collect();
481            let mut state = cache(&pages);
482
483            for (kind, byte, hint) in operations {
484                let changed = match kind {
485                    0 => {
486                        let index = hint % pages.len();
487                        pages[index] = page(byte);
488                        vec![(index as u32 + 1, pages[index].clone())]
489                    }
490                    1 if pages.len() < 16 => {
491                        let added = 1 + hint % (16 - pages.len()).min(3);
492                        let first = pages.len();
493                        pages.extend((0..added).map(|offset| page(byte.wrapping_add(offset as u8))));
494                        pages[first..]
495                            .iter()
496                            .enumerate()
497                            .map(|(offset, image)| ((first + offset) as u32 + 1, image.clone()))
498                            .collect()
499                    }
500                    _ if pages.len() > 1 => {
501                        let target = 1 + hint % (pages.len() - 1);
502                        pages.truncate(target);
503                        Vec::new()
504                    }
505                    _ => {
506                        pages[0] = page(byte);
507                        vec![(1, pages[0].clone())]
508                    }
509                };
510                let patch = patches(&changed);
511                let rebuilt = cache(&pages).identity();
512
513                prop_assert_eq!(state.overlay(pages.len() as u32, &patch).unwrap(), rebuilt);
514                prop_assert_eq!(state.apply_patch(pages.len() as u32, &patch).unwrap(), rebuilt);
515                prop_assert_eq!(state.identity(), rebuilt);
516            }
517        }
518    }
519}