use std::collections::VecDeque;
use crate::error::{Result, SQLRiteError};
use crate::sql::pager::page::{PAGE_HEADER_SIZE, PAGE_SIZE, PAYLOAD_PER_PAGE};
use crate::sql::pager::pager::Pager;
pub const PAGE_TYPE_FREELIST_TRUNK: u8 = 5;
pub const FREELIST_IDS_PER_TRUNK: usize = (PAYLOAD_PER_PAGE - 2) / 4;
pub fn encode_trunk(buf: &mut [u8; PAGE_SIZE], next_trunk: u32, page_ids: &[u32]) -> Result<()> {
if page_ids.len() > FREELIST_IDS_PER_TRUNK {
return Err(SQLRiteError::Internal(format!(
"freelist trunk overflow: {} ids exceeds capacity {}",
page_ids.len(),
FREELIST_IDS_PER_TRUNK
)));
}
buf.fill(0);
buf[0] = PAGE_TYPE_FREELIST_TRUNK;
buf[1..5].copy_from_slice(&next_trunk.to_le_bytes());
buf[5..7].copy_from_slice(&0u16.to_le_bytes());
let count = page_ids.len() as u16;
buf[PAGE_HEADER_SIZE..PAGE_HEADER_SIZE + 2].copy_from_slice(&count.to_le_bytes());
let mut off = PAGE_HEADER_SIZE + 2;
for id in page_ids {
buf[off..off + 4].copy_from_slice(&id.to_le_bytes());
off += 4;
}
Ok(())
}
fn decode_trunk(buf: &[u8; PAGE_SIZE]) -> Result<(u32, Vec<u32>)> {
if buf[0] != PAGE_TYPE_FREELIST_TRUNK {
return Err(SQLRiteError::General(format!(
"expected freelist trunk page (tag {PAGE_TYPE_FREELIST_TRUNK}), got tag {}",
buf[0]
)));
}
let next_trunk = u32::from_le_bytes(buf[1..5].try_into().unwrap());
let count = u16::from_le_bytes(
buf[PAGE_HEADER_SIZE..PAGE_HEADER_SIZE + 2]
.try_into()
.unwrap(),
) as usize;
if count > FREELIST_IDS_PER_TRUNK {
return Err(SQLRiteError::General(format!(
"freelist trunk count {count} exceeds capacity {FREELIST_IDS_PER_TRUNK}"
)));
}
let mut ids = Vec::with_capacity(count);
let mut off = PAGE_HEADER_SIZE + 2;
for _ in 0..count {
let id = u32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
ids.push(id);
off += 4;
}
Ok((next_trunk, ids))
}
pub fn read_freelist(pager: &Pager, head: u32) -> Result<(Vec<u32>, Vec<u32>)> {
let mut leaves: Vec<u32> = Vec::new();
let mut trunks: Vec<u32> = Vec::new();
let mut cursor = head;
let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::new();
while cursor != 0 {
if !visited.insert(cursor) {
return Err(SQLRiteError::General(format!(
"freelist cycle detected at trunk page {cursor}"
)));
}
let buf = pager.read_page(cursor).ok_or_else(|| {
SQLRiteError::General(format!(
"freelist trunk page {cursor} is past page_count or unreadable"
))
})?;
let (next, ids) = decode_trunk(buf)?;
leaves.extend(ids);
trunks.push(cursor);
cursor = next;
}
Ok((leaves, trunks))
}
pub fn stage_freelist(pager: &mut Pager, free_pages: Vec<u32>) -> Result<u32> {
if free_pages.is_empty() {
return Ok(0);
}
let mut ids = free_pages;
ids.sort_unstable();
ids.dedup();
let n = ids.len();
let t = n.div_ceil(FREELIST_IDS_PER_TRUNK + 1);
let leaves_count = n - t;
let trunk_pages: Vec<u32> = ids.split_off(leaves_count);
let leaves = ids;
let mut chunks: Vec<&[u32]> = leaves.chunks(FREELIST_IDS_PER_TRUNK).collect();
while chunks.len() < trunk_pages.len() {
chunks.push(&[]);
}
for (i, chunk) in chunks.iter().enumerate() {
let next = if i + 1 < trunk_pages.len() {
trunk_pages[i + 1]
} else {
0
};
let mut buf = [0u8; PAGE_SIZE];
encode_trunk(&mut buf, next, chunk)?;
pager.stage_page(trunk_pages[i], buf);
}
Ok(trunk_pages[0])
}
pub fn freelist_to_deque(leaves: Vec<u32>) -> VecDeque<u32> {
let mut sorted = leaves;
sorted.sort_unstable();
sorted.dedup();
VecDeque::from(sorted)
}
pub const MIN_PAGES_FOR_AUTO_VACUUM: u32 = 16;
pub fn should_auto_vacuum(pager: &Pager, threshold: f32) -> Result<bool> {
let header = pager.header();
if header.page_count < MIN_PAGES_FOR_AUTO_VACUUM {
return Ok(false);
}
if header.freelist_head == 0 {
return Ok(false);
}
let (leaves, trunks) = read_freelist(pager, header.freelist_head)?;
let free_pages = leaves.len() + trunks.len();
let ratio = free_pages as f32 / header.page_count as f32;
Ok(ratio > threshold)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_freelist_round_trip() {
let mut buf = [0u8; PAGE_SIZE];
encode_trunk(&mut buf, 0, &[]).unwrap();
let (next, ids) = decode_trunk(&buf).unwrap();
assert_eq!(next, 0);
assert!(ids.is_empty());
}
#[test]
fn single_chunk_round_trip() {
let mut buf = [0u8; PAGE_SIZE];
let pages = [3u32, 7, 12, 99];
encode_trunk(&mut buf, 42, &pages).unwrap();
let (next, ids) = decode_trunk(&buf).unwrap();
assert_eq!(next, 42);
assert_eq!(ids, pages);
}
#[test]
fn full_chunk_fits_capacity() {
let mut buf = [0u8; PAGE_SIZE];
let pages: Vec<u32> = (1..=FREELIST_IDS_PER_TRUNK as u32).collect();
encode_trunk(&mut buf, 0, &pages).unwrap();
let (next, ids) = decode_trunk(&buf).unwrap();
assert_eq!(next, 0);
assert_eq!(ids.len(), FREELIST_IDS_PER_TRUNK);
assert_eq!(ids[0], 1);
assert_eq!(
ids[FREELIST_IDS_PER_TRUNK - 1],
FREELIST_IDS_PER_TRUNK as u32
);
}
#[test]
fn over_capacity_errors() {
let mut buf = [0u8; PAGE_SIZE];
let pages: Vec<u32> = (1..=(FREELIST_IDS_PER_TRUNK as u32 + 1)).collect();
let err = encode_trunk(&mut buf, 0, &pages).unwrap_err();
assert!(format!("{err}").contains("freelist trunk overflow"));
}
#[test]
fn wrong_tag_errors_on_decode() {
let mut buf = [0u8; PAGE_SIZE];
buf[0] = 2; let err = decode_trunk(&buf).unwrap_err();
assert!(format!("{err}").contains("expected freelist trunk page"));
}
}