sqlrite/sql/pager/allocator.rs
1//! Page allocator for `save_database` (SQLR-6).
2//!
3//! Replaces the bare `next_free_page: u32` counter that the staging code
4//! used to thread through every `stage_*_btree` function. The allocator
5//! draws from three pools, in order of preference:
6//!
7//! 1. **Per-table preferred pool** — pages the table previously occupied,
8//! seeded by [`set_preferred`]. An unchanged table's stage produces
9//! byte-identical pages at the same numbers, so the diff pager skips
10//! every write for it.
11//! 2. **Global freelist** — pages dropped tables/indexes used to occupy
12//! plus the trunk pages of the previously-persisted freelist.
13//! 3. **Extend** — `next_extend++`, monotonic past the current high water.
14//!
15//! After staging finishes, [`high_water`] is the new `page_count` and
16//! [`used`] enumerates every page actually written this save (so the
17//! caller can compute the new freelist as `old_live − used`).
18
19use std::collections::{HashSet, VecDeque};
20
21/// Hands out page numbers during a save.
22///
23/// Lifetime: one allocator per `save_database` call. Not thread-safe; not
24/// shared across saves.
25pub struct PageAllocator {
26 /// Pages available globally. Drained after the per-table pool is empty.
27 /// Stored as a VecDeque so callers can append (push_back) and we always
28 /// hand them out front-first for ascending-order determinism.
29 freelist: VecDeque<u32>,
30 /// The current table's preferred pool (its previous-rootpage pages).
31 /// Drained before [`freelist`]. Cleared between tables by
32 /// [`finish_preferred`].
33 preferred: VecDeque<u32>,
34 /// Next page number for fresh extension. Page 0 is the header, so
35 /// the first alloc always returns ≥ 1.
36 next_extend: u32,
37 /// Every page handed out this save. Used to compute the newly-freed
38 /// set after staging completes.
39 used: HashSet<u32>,
40}
41
42impl PageAllocator {
43 /// `freelist` carries the pages from the previously-persisted
44 /// freelist (sorted ascending by the caller). `next_extend` is
45 /// typically `1` for a brand-new save.
46 pub fn new(freelist: VecDeque<u32>, next_extend: u32) -> Self {
47 let mut alloc = Self {
48 freelist,
49 preferred: VecDeque::new(),
50 next_extend,
51 used: HashSet::new(),
52 };
53 // Defensive: a corrupt freelist could push the high-water mark
54 // higher than `next_extend` claims. Bump so we never hand out a
55 // duplicate page on extend.
56 let max_free = alloc.freelist.iter().copied().max().unwrap_or(0);
57 if max_free + 1 > alloc.next_extend {
58 alloc.next_extend = max_free + 1;
59 }
60 alloc
61 }
62
63 /// Seeds the per-table preferred pool. Drained on subsequent
64 /// [`allocate`] calls before any other source.
65 pub fn set_preferred(&mut self, mut pool: Vec<u32>) {
66 // Sort ascending so the order matches the linear staging order
67 // and unchanged tables get byte-identical leaves.
68 pool.sort_unstable();
69 pool.dedup();
70 // Filter out anything the allocator has already handed out
71 // (defensive — shouldn't happen but keeps the invariant tidy).
72 pool.retain(|p| !self.used.contains(p));
73 self.preferred = VecDeque::from(pool);
74 }
75
76 /// Empties the per-table preferred pool, returning any leftover
77 /// pages to the global freelist (they're now free again).
78 pub fn finish_preferred(&mut self) {
79 while let Some(p) = self.preferred.pop_front() {
80 if !self.used.contains(&p) {
81 self.freelist.push_back(p);
82 }
83 }
84 }
85
86 /// Returns the next page to write. Picks from preferred → freelist →
87 /// extend. Records the result in `used` and bumps `next_extend` if
88 /// the page came from one of the pools and was past the current
89 /// high water.
90 pub fn allocate(&mut self) -> u32 {
91 let page = if let Some(p) = self.preferred.pop_front() {
92 p
93 } else if let Some(p) = self.freelist.pop_front() {
94 p
95 } else {
96 let p = self.next_extend;
97 self.next_extend += 1;
98 p
99 };
100 if page >= self.next_extend {
101 self.next_extend = page + 1;
102 }
103 // A double-allocation is an internal bug; assert in debug.
104 debug_assert!(
105 !self.used.contains(&page),
106 "PageAllocator handed out page {page} twice"
107 );
108 self.used.insert(page);
109 page
110 }
111
112 /// Adds pages to the global freelist. Used to drop pages that the
113 /// caller traversed but didn't end up restaging (e.g., a dropped
114 /// table's leaves; the previous freelist's trunk pages).
115 ///
116 /// Bumps `next_extend` past any added page so the final page_count
117 /// covers freelist trunks even if they live past the highest used
118 /// payload page.
119 pub fn add_to_freelist(&mut self, pages: impl IntoIterator<Item = u32>) {
120 for p in pages {
121 // Skip pages already used (we already restaged them) or
122 // already on the list.
123 if !self.used.contains(&p) && !self.freelist.contains(&p) {
124 self.freelist.push_back(p);
125 if p + 1 > self.next_extend {
126 self.next_extend = p + 1;
127 }
128 }
129 }
130 }
131
132 /// Page-count to publish in the new header. Equal to
133 /// `1 + max page handed out` after staging.
134 pub fn high_water(&self) -> u32 {
135 self.next_extend
136 }
137
138 /// Every page handed out this save.
139 pub fn used(&self) -> &HashSet<u32> {
140 &self.used
141 }
142
143 /// Snapshot of pages still on the global freelist (i.e., free pages
144 /// that need to be persisted into trunk pages). Sorted ascending so
145 /// the encoded freelist trunks are deterministic.
146 pub fn drain_freelist(&mut self) -> Vec<u32> {
147 let mut v: Vec<u32> = self.freelist.drain(..).collect();
148 v.sort_unstable();
149 v.dedup();
150 v
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn allocate_extends_when_pools_empty() {
160 let mut a = PageAllocator::new(VecDeque::new(), 1);
161 assert_eq!(a.allocate(), 1);
162 assert_eq!(a.allocate(), 2);
163 assert_eq!(a.allocate(), 3);
164 assert_eq!(a.high_water(), 4);
165 }
166
167 #[test]
168 fn preferred_pool_drains_first() {
169 let mut a = PageAllocator::new(VecDeque::from([8, 9]), 1);
170 a.set_preferred(vec![3, 4]);
171 assert_eq!(a.allocate(), 3);
172 assert_eq!(a.allocate(), 4);
173 // After preferred drains, freelist takes over.
174 assert_eq!(a.allocate(), 8);
175 assert_eq!(a.allocate(), 9);
176 // Then extend.
177 assert_eq!(a.allocate(), 10);
178 }
179
180 #[test]
181 fn freelist_drains_after_preferred() {
182 let mut a = PageAllocator::new(VecDeque::from([5, 7]), 1);
183 assert_eq!(a.allocate(), 5);
184 assert_eq!(a.allocate(), 7);
185 assert_eq!(a.allocate(), 8); // extend bumped because max free was 7
186 }
187
188 #[test]
189 fn finish_preferred_returns_leftovers_to_freelist() {
190 let mut a = PageAllocator::new(VecDeque::new(), 1);
191 a.set_preferred(vec![3, 4, 5]);
192 assert_eq!(a.allocate(), 3); // used 3
193 a.finish_preferred();
194 // Now 4 and 5 should be on the freelist.
195 assert_eq!(a.allocate(), 4);
196 assert_eq!(a.allocate(), 5);
197 }
198
199 #[test]
200 fn add_to_freelist_skips_already_used() {
201 let mut a = PageAllocator::new(VecDeque::new(), 1);
202 let p = a.allocate(); // 1
203 a.add_to_freelist([p, 5, 6]);
204 let drained = a.drain_freelist();
205 assert!(
206 !drained.contains(&p),
207 "used page should not land on freelist"
208 );
209 assert_eq!(drained, vec![5, 6]);
210 }
211
212 #[test]
213 fn next_extend_respects_max_free() {
214 // High pages on the freelist should bump next_extend so the
215 // allocator never collides with them on extend.
216 let mut a = PageAllocator::new(VecDeque::from([100]), 1);
217 // First alloc draws from freelist.
218 assert_eq!(a.allocate(), 100);
219 // Subsequent extend lands at 101, not 1.
220 assert_eq!(a.allocate(), 101);
221 }
222}