Skip to main content

wasm_dbms_memory/table_registry/
table_reader.rs

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