Skip to main content

wasm_dbms_memory/
unclaimed_pages.rs

1// Rust guideline compliant 2026-04-27
2// X-WHERE-CLAUSE, M-CANONICAL-DOCS
3
4//! Unclaimed-pages ledger persisted on the reserved page
5//! [`crate::memory_manager::UNCLAIMED_PAGES_PAGE`].
6//!
7//! Tracks pages that have been released by destructive operations (e.g.
8//! `MigrationOp::DropTable`) so that they can be reused by future
9//! [`MemoryAccess::claim_page`](crate::MemoryAccess::claim_page) calls
10//! before bumping the high-water mark via the underlying provider.
11
12use std::borrow::Cow;
13
14use wasm_dbms_api::prelude::{
15    DEFAULT_ALIGNMENT, DataSize, Encode, MSize, MemoryError, MemoryResult, Page, PageOffset,
16};
17
18/// Bytes of the on-page header (`u32` length prefix).
19const HEADER_SIZE: u64 = 4;
20/// Bytes of one page entry (`u32`).
21const ENTRY_SIZE: u64 = 4;
22
23/// Maximum number of entries that fit in the reserved page (64 KiB).
24///
25/// Computed at build time so that the encoded ledger size stays within
26/// [`MSize`] (`u16`). Currently 16382 entries.
27pub const UNCLAIMED_PAGES_CAPACITY: u32 = {
28    let max_bytes = MSize::MAX as u64;
29    let entries = (max_bytes - HEADER_SIZE) / ENTRY_SIZE;
30    entries as u32
31};
32
33/// On-disk representation of the unclaimed-pages ledger.
34///
35/// The ledger is a LIFO stack of [`Page`] numbers. Push appends, pop
36/// removes from the tail.
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
38pub struct UnclaimedPages {
39    pages: Vec<Page>,
40}
41
42impl UnclaimedPages {
43    /// Returns an empty ledger.
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Returns the number of pages currently in the ledger.
49    pub fn len(&self) -> usize {
50        self.pages.len()
51    }
52
53    /// Returns how many additional pages can still be tracked.
54    pub fn remaining_capacity(&self) -> u32 {
55        UNCLAIMED_PAGES_CAPACITY - (self.pages.len() as u32)
56    }
57
58    /// Returns `true` when no pages are currently tracked.
59    pub fn is_empty(&self) -> bool {
60        self.pages.is_empty()
61    }
62
63    /// Removes and returns the last unclaimed page, if any.
64    pub fn pop(&mut self) -> Option<Page> {
65        self.pages.pop()
66    }
67
68    /// Appends `page` to the ledger.
69    ///
70    /// # Errors
71    ///
72    /// [`MemoryError::UnclaimedPagesFull`] when the ledger is at capacity.
73    pub fn push(&mut self, page: Page) -> MemoryResult<()> {
74        if self.pages.len() as u32 >= UNCLAIMED_PAGES_CAPACITY {
75            return Err(MemoryError::UnclaimedPagesFull {
76                capacity: UNCLAIMED_PAGES_CAPACITY,
77            });
78        }
79        self.pages.push(page);
80        Ok(())
81    }
82
83    /// Returns a slice over the tracked pages (oldest first).
84    pub fn as_slice(&self) -> &[Page] {
85        &self.pages
86    }
87}
88
89impl Encode for UnclaimedPages {
90    const SIZE: DataSize = DataSize::Dynamic;
91    const ALIGNMENT: PageOffset = DEFAULT_ALIGNMENT;
92
93    fn encode(&'_ self) -> Cow<'_, [u8]> {
94        let mut buf = Vec::with_capacity(self.size() as usize);
95        buf.extend_from_slice(&(self.pages.len() as u32).to_le_bytes());
96        for &page in &self.pages {
97            buf.extend_from_slice(&page.to_le_bytes());
98        }
99        Cow::Owned(buf)
100    }
101
102    fn decode(data: Cow<[u8]>) -> MemoryResult<Self>
103    where
104        Self: Sized,
105    {
106        if data.len() < HEADER_SIZE as usize {
107            return Ok(Self::default());
108        }
109        let count = u32::from_le_bytes(data[0..4].try_into()?) as usize;
110        if count > UNCLAIMED_PAGES_CAPACITY as usize {
111            return Err(MemoryError::UnclaimedPagesFull {
112                capacity: UNCLAIMED_PAGES_CAPACITY,
113            });
114        }
115        let mut pages = Vec::with_capacity(count);
116        let mut cursor = HEADER_SIZE as usize;
117        for _ in 0..count {
118            let page = Page::from_le_bytes(data[cursor..cursor + 4].try_into()?);
119            pages.push(page);
120            cursor += 4;
121        }
122        Ok(Self { pages })
123    }
124
125    fn size(&self) -> MSize {
126        (HEADER_SIZE as MSize) + (self.pages.len() as MSize) * (ENTRY_SIZE as MSize)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_should_be_empty_by_default() {
136        let ledger = UnclaimedPages::new();
137        assert!(ledger.is_empty());
138        assert_eq!(ledger.len(), 0);
139    }
140
141    #[test]
142    fn test_should_push_and_pop() {
143        let mut ledger = UnclaimedPages::new();
144        ledger.push(10).expect("push");
145        ledger.push(20).expect("push");
146        ledger.push(30).expect("push");
147
148        assert_eq!(ledger.len(), 3);
149        assert_eq!(ledger.pop(), Some(30));
150        assert_eq!(ledger.pop(), Some(20));
151        assert_eq!(ledger.pop(), Some(10));
152        assert_eq!(ledger.pop(), None);
153    }
154
155    #[test]
156    fn test_should_round_trip_encode_decode() {
157        let mut ledger = UnclaimedPages::new();
158        for page in [3u32, 5, 7, 11] {
159            ledger.push(page).expect("push");
160        }
161
162        let encoded = ledger.encode();
163        let decoded = UnclaimedPages::decode(encoded).expect("decode");
164        assert_eq!(ledger, decoded);
165    }
166
167    #[test]
168    fn test_should_decode_empty_buffer_as_empty_ledger() {
169        let buf = vec![0u8; 65536];
170        let ledger = UnclaimedPages::decode(Cow::Owned(buf)).expect("decode");
171        assert!(ledger.is_empty());
172    }
173
174    #[test]
175    fn test_should_reject_push_when_full() {
176        let mut ledger = UnclaimedPages {
177            pages: vec![0; UNCLAIMED_PAGES_CAPACITY as usize],
178        };
179        let err = ledger.push(42).expect_err("push at capacity");
180        assert!(matches!(err, MemoryError::UnclaimedPagesFull { .. }));
181    }
182
183    #[test]
184    fn test_size_matches_encoded_length() {
185        let mut ledger = UnclaimedPages::new();
186        ledger.push(1).expect("push");
187        ledger.push(2).expect("push");
188        let encoded = ledger.encode();
189        assert_eq!(ledger.size() as usize, encoded.len());
190    }
191}