Skip to main content

wasm_dbms_memory/table_registry/
table_reader.rs

1// Rust guideline compliant 2026-02-28
2
3use std::borrow::Cow;
4use std::marker::PhantomData;
5
6use wasm_dbms_api::prelude::{
7    DecodeError, Encode, MSize, MemoryError, MemoryResult, Page, PageOffset,
8};
9
10use super::page_ledger::PageLedger;
11use super::raw_record::{RAW_RECORD_HEADER_SIZE, RawRecord};
12use crate::{MemoryAccess, align_up};
13
14/// Stores the current position to read/write in memory.
15#[derive(Debug, Copy, Clone, PartialEq, Eq)]
16struct Position {
17    page: Page,
18    offset: PageOffset,
19}
20
21/// Represents the next record to read from memory.
22/// It also contains the new [`Position`] after reading the record.
23#[derive(Debug, Copy, Clone, PartialEq, Eq)]
24struct FoundRecord {
25    page: Page,
26    offset: PageOffset,
27    length: MSize,
28    new_position: Option<Position>,
29}
30
31/// Represents the next record read by the [`TableReader`].
32#[derive(Debug, Copy, Clone, PartialEq, Eq)]
33pub struct NextRecord<E>
34where
35    E: Encode,
36{
37    pub record: E,
38    pub page: Page,
39    pub offset: PageOffset,
40}
41
42/// A reader for the table registry that allows reading records from memory.
43///
44/// The table reader provides methods to read records from the table registry one by one,
45/// using the underlying `PageLedger` to locate the records in memory.
46pub struct TableReader<'a, E, MA>
47where
48    E: Encode,
49    MA: MemoryAccess,
50{
51    /// Buffer used to read records from memory.
52    buffer: Vec<u8>,
53    /// Reference to the memory access implementor.
54    mm: &'a mut MA,
55    page_ledger: &'a PageLedger,
56    page_size: usize,
57    phantom: PhantomData<E>,
58    /// Current position in the table registry.
59    /// If `None`, the reader has reached the end of the table.
60    position: Option<Position>,
61}
62
63impl<'a, E, MA> TableReader<'a, E, MA>
64where
65    E: Encode,
66    MA: MemoryAccess,
67{
68    /// Creates a new table reader starting from the beginning of the table registry.
69    pub fn new(page_ledger: &'a PageLedger, mm: &'a mut MA) -> Self {
70        // init position
71        let position = page_ledger.pages().first().map(|page_record| Position {
72            page: page_record.page,
73            offset: 0,
74        });
75        let page_size = mm.page_size() as usize;
76        Self {
77            buffer: vec![0u8; page_size],
78            mm,
79            page_ledger,
80            phantom: PhantomData,
81            position,
82            page_size,
83        }
84    }
85
86    /// Reads the next record from the table registry.
87    pub fn try_next(&mut self) -> MemoryResult<Option<NextRecord<E>>> {
88        let Some(Position { page, offset }) = self.position else {
89            return Ok(None);
90        };
91
92        // find next record segment
93        let Some(next_record) = self.find_next_record(page, offset)? else {
94            // no more records
95            self.position = None;
96            return Ok(None);
97        };
98
99        // decode the record from the page already held in the buffer
100        let start = next_record.offset as usize;
101        let end = start + next_record.length as usize;
102        let record = RawRecord::<E>::decode(Cow::Borrowed(&self.buffer[start..end]))?;
103
104        // update position
105        self.position = next_record.new_position;
106
107        Ok(Some(NextRecord {
108            record: record.data,
109            page: next_record.page,
110            offset: next_record.offset,
111        }))
112    }
113
114    /// Finds the next record starting from the given position.
115    ///
116    /// If a record is found, returns [`Some<NextRecord<E>>`], otherwise returns [`None`].
117    /// If [`None`] is returned, the reader has reached the end of the table.
118    fn find_next_record(
119        &mut self,
120        mut page: Page,
121        mut offset: PageOffset,
122    ) -> MemoryResult<Option<FoundRecord>> {
123        loop {
124            // if offset is zero, read page; otherwise, just reuse buffer
125            if offset == 0 {
126                self.mm.read_at_raw(page, 0, &mut self.buffer)?;
127            }
128
129            // find next record in buffer; if found, return it
130            if let Some((next_segment_offset, next_segment_size)) =
131                self.find_next_record_position(&self.buffer, offset as usize)?
132            {
133                // Found a record; compute the end in `usize` so a record
134                // ending exactly at the page boundary cannot wrap the
135                // `PageOffset` cursor back to the start of the page.
136                let new_offset = next_segment_offset as usize + next_segment_size as usize;
137                let new_position = if new_offset >= self.page_size {
138                    // move to next page
139                    self.next_page(page)
140                } else {
141                    Some(Position {
142                        page,
143                        offset: new_offset as PageOffset,
144                    })
145                };
146                return Ok(Some(FoundRecord {
147                    page,
148                    offset: next_segment_offset,
149                    length: next_segment_size,
150                    new_position,
151                }));
152            }
153
154            // read next page
155            match self.next_page(page) {
156                Some(pos) => {
157                    page = pos.page;
158                    offset = pos.offset;
159                }
160                None => break,
161            }
162        }
163
164        Ok(None)
165    }
166
167    /// Gets the next page after the given current page, in ledger order.
168    fn next_page(&self, current_page: Page) -> Option<Position> {
169        self.page_ledger
170            .next_page_after(current_page)
171            .map(|page| Position { page, offset: 0 })
172    }
173
174    /// Finds the next record segment position.
175    ///
176    /// This is done by starting from the current offset
177    /// and searching for each multiple of [`E::ALIGNMENT`] until we find a size different from `0x00`.
178    ///
179    /// Returns the offset and size of the next record segment if found.
180    fn find_next_record_position(
181        &self,
182        buf: &[u8],
183        mut offset: usize,
184    ) -> MemoryResult<Option<(PageOffset, MSize)>> {
185        // first round the offset to the next alignment
186        offset = align_up::<RawRecord<E>>(offset);
187        // search for the first non-zero record length
188        let mut data_len;
189        loop {
190            // check whether we are at the end of the page
191            if offset + 1 >= buf.len() {
192                return Ok(None);
193            }
194            // read next two bytes
195            data_len = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as MSize;
196            if data_len != 0 {
197                break;
198            }
199            // Move to the next slot; slots are laid out with the raw-record
200            // alignment, which includes the length header for fixed-size
201            // types.
202            offset += <RawRecord<E> as Encode>::ALIGNMENT as usize;
203        }
204
205        let data_offset = offset + RAW_RECORD_HEADER_SIZE as usize;
206        if buf.len() < data_offset + data_len as usize {
207            return Err(MemoryError::DecodeError(DecodeError::TooShort));
208        }
209
210        Ok(Some((
211            offset as PageOffset,
212            data_len + RAW_RECORD_HEADER_SIZE,
213        )))
214    }
215}
216
217#[cfg(test)]
218mod tests {
219
220    use super::*;
221    use crate::table_registry::test_utils::User;
222    use crate::{HeapMemoryProvider, MemoryManager, TableRegistry, TableRegistryPage};
223
224    #[test]
225    fn test_should_read_all_records() {
226        const COUNT: u32 = 4_000;
227        let mut mm = MemoryManager::init(HeapMemoryProvider::default());
228        let table_registry = mock_table_registry(COUNT, &mut mm);
229        let mut reader = mocked(&table_registry, &mut mm);
230
231        // should read all records
232        let mut id = 0;
233        while let Some(NextRecord { record: user, .. }) =
234            reader.try_next().expect("failed to read user")
235        {
236            assert_eq!(user.id, id);
237            assert_eq!(user.name, format!("User {}", id));
238
239            id += 1;
240        }
241        assert_eq!(id, COUNT);
242    }
243
244    #[test]
245    fn test_should_find_next_page() {
246        let mut mm = MemoryManager::init(HeapMemoryProvider::default());
247        let table_registry = mock_table_registry(4_000, &mut mm);
248        let reader = mocked(&table_registry, &mut mm);
249
250        let page = reader.position.expect("should have position").page;
251
252        let next_page = reader.next_page(page).expect("should have next page");
253        assert_eq!(next_page.page, page + 1);
254        let next_page = reader.next_page(next_page.page);
255        assert!(next_page.is_some());
256        let next_page = reader.next_page(next_page.unwrap().page);
257        assert!(next_page.is_some());
258        let next_page = reader.next_page(next_page.unwrap().page);
259        assert!(
260            next_page.is_none(),
261            "should not have next page, but got {:?}",
262            next_page
263        );
264    }
265
266    #[test]
267    fn test_should_scan_reclaimed_pages_in_ledger_order() {
268        let mut mm = MemoryManager::init(HeapMemoryProvider::default());
269        let mut table_registry = mock_table_registry(0, &mut mm);
270
271        // Reclaim two pages so they are handed back in descending order.
272        let low = mm.claim_page().expect("claim low");
273        let high = mm.claim_page().expect("claim high");
274        mm.unclaim_page(low).expect("unclaim low");
275        mm.unclaim_page(high).expect("unclaim high");
276
277        // Each record is larger than half a page, so the second one needs a
278        // new page: the ledger becomes [high, low].
279        for id in 0..2 {
280            let user = User {
281                id,
282                name: String::new(),
283                email: "x".repeat(40_000),
284                age: 20,
285            };
286            table_registry.insert(user, &mut mm).expect("insert");
287        }
288        let ledger_pages: Vec<Page> = table_registry
289            .page_ledger
290            .pages()
291            .iter()
292            .map(|record| record.page)
293            .collect();
294        assert_eq!(ledger_pages, vec![high, low]);
295
296        let mut reader = mocked(&table_registry, &mut mm);
297        let mut ids = Vec::new();
298        while let Some(next) = reader.try_next().expect("scan") {
299            ids.push(next.record.id);
300        }
301        assert_eq!(ids, vec![0, 1]);
302    }
303
304    #[test]
305    fn test_should_read_record_ending_at_page_boundary() {
306        let mut mm = MemoryManager::init(HeapMemoryProvider::default());
307        let mut table_registry = mock_table_registry(0, &mut mm);
308
309        // Raw footprint: 2 (header) + 12 (fixed fields) + 32_754 (email)
310        // = 32_768, so two records fill a 64 KiB page exactly.
311        for id in 0..2 {
312            let user = User {
313                id,
314                name: String::new(),
315                email: "x".repeat(32_754),
316                age: 20,
317            };
318            table_registry.insert(user, &mut mm).expect("insert");
319        }
320        assert_eq!(table_registry.page_ledger.pages().len(), 1);
321
322        let mut reader = mocked(&table_registry, &mut mm);
323        let first = reader.try_next().expect("first").expect("first row");
324        assert_eq!((first.record.id, first.offset), (0, 0));
325        let second = reader.try_next().expect("second").expect("second row");
326        assert_eq!((second.record.id, second.offset), (1, 32_768));
327        assert!(reader.try_next().expect("end").is_none());
328    }
329
330    #[test]
331    fn test_should_find_next_record_position() {
332        let mut mm = MemoryManager::init(HeapMemoryProvider::default());
333        let table_registry = mock_table_registry(1, &mut mm);
334        let reader = mocked(&table_registry, &mut mm);
335
336        let mut buf = vec![0u8; User::ALIGNMENT as usize];
337        buf.extend_from_slice(&[5u8, 0u8, 0u8, 0, 0, 0, 0, 0, 0]);
338
339        let (offset, size) = reader
340            .find_next_record_position(&buf, 0)
341            .expect("failed to get next record")
342            .expect("should have next record");
343
344        assert_eq!(offset, 32);
345        assert_eq!(size, 7);
346    }
347
348    #[test]
349    fn test_should_not_find_next_record_position_none() {
350        let mut mm = MemoryManager::init(HeapMemoryProvider::default());
351        let table_registry = mock_table_registry(1, &mut mm);
352        let reader = mocked(&table_registry, &mut mm);
353
354        let buf = vec![0u8; User::ALIGNMENT as usize * 2];
355        let result = reader
356            .find_next_record_position(&buf, 0)
357            .expect("failed to get next record");
358
359        assert!(result.is_none());
360    }
361
362    #[test]
363    fn test_should_not_find_next_record_position_too_short_for_length() {
364        let mut mm = MemoryManager::init(HeapMemoryProvider::default());
365        let table_registry = mock_table_registry(1, &mut mm);
366        let reader = mocked(&table_registry, &mut mm);
367
368        let buf = [5u8, 16u8];
369        let result = reader.find_next_record_position(&buf, 0);
370        assert!(result.is_err(), "expected error but got {:?}", result);
371        let err = result.unwrap_err();
372
373        assert!(matches!(
374            err,
375            MemoryError::DecodeError(DecodeError::TooShort)
376        ));
377    }
378
379    #[test]
380    fn test_should_not_find_next_record_position_too_short_for_data() {
381        let mut mm = MemoryManager::init(HeapMemoryProvider::default());
382        let table_registry = mock_table_registry(1, &mut mm);
383        let reader = mocked(&table_registry, &mut mm);
384
385        let buf = [5u8, 0u8, 0u8, 0, 0];
386        let result = reader.find_next_record_position(&buf, 0);
387
388        assert!(matches!(
389            result,
390            Err(MemoryError::DecodeError(DecodeError::TooShort))
391        ));
392    }
393
394    fn mock_table_registry(
395        entries: u32,
396        mm: &mut MemoryManager<HeapMemoryProvider>,
397    ) -> TableRegistry {
398        let schema_snapshot_page = mm.claim_page().expect("failed to get page");
399        let page_ledger_page = mm.claim_page().expect("failed to get page");
400        let free_segments_page = mm.claim_page().expect("failed to get page");
401        let index_registry_page = mm.claim_page().expect("failed to get page");
402        super::super::test_utils::write_dummy_schema_snapshot(schema_snapshot_page, mm);
403        let mut registry = TableRegistry::load(
404            TableRegistryPage {
405                schema_snapshot_page,
406                pages_list_page: page_ledger_page,
407                free_segments_page,
408                index_registry_page,
409                autoincrement_registry_page: None,
410            },
411            mm,
412        )
413        .expect("failed to load registry");
414
415        // insert `entries` records
416        for id in 0..entries {
417            let user = User {
418                id,
419                name: format!("User {}", id),
420                email: "new_user@example.com".to_string(),
421                age: 20 + id,
422            };
423            registry.insert(user, mm).expect("failed to insert user");
424        }
425
426        registry
427    }
428
429    fn mocked<'a>(
430        table_registry: &'a TableRegistry,
431        mm: &'a mut MemoryManager<HeapMemoryProvider>,
432    ) -> TableReader<'a, User, MemoryManager<HeapMemoryProvider>> {
433        TableReader::new(&table_registry.page_ledger, mm)
434    }
435}