diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 349b4e2be..0c2baaffd 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -5518,6 +5518,13 @@ pub enum IntegrityCheckError { references: Vec, page_category: PageCategory, }, + #[error( + "Freelist count mismatch. actual_count={actual_count}, expected_count={expected_count}" + )] + FreelistCountMismatch { + actual_count: usize, + expected_count: usize, + }, } #[derive(Debug, Clone, Copy, PartialEq)] @@ -5528,6 +5535,12 @@ pub(crate) enum PageCategory { FreePage, } +#[derive(Clone)] +pub struct CheckFreelist { + pub expected_count: usize, + pub actual_count: usize, +} + #[derive(Clone)] struct IntegrityCheckPageEntry { page_idx: usize, @@ -5540,6 +5553,7 @@ pub struct IntegrityCheckState { first_leaf_level: Option, page_reference: HashMap, page: Option, + pub freelist_count: CheckFreelist, } impl IntegrityCheckState { @@ -5549,9 +5563,17 @@ impl IntegrityCheckState { page_reference: HashMap::new(), first_leaf_level: None, page: None, + freelist_count: CheckFreelist { + expected_count: 0, + actual_count: 0, + }, } } + pub fn set_expected_freelist_count(&mut self, count: usize) { + self.freelist_count.expected_count = count; + } + pub fn start( &mut self, page_idx: usize, @@ -5585,10 +5607,7 @@ impl IntegrityCheckState { ) { let page_id = entry.page_idx as u64; let Some(previous) = self.page_reference.insert(page_id, referenced_by) else { - // do not traverse free pages as they have no meaingful structured content - if entry.page_category != PageCategory::FreePage { - self.page_stack.push(entry); - } + self.page_stack.push(entry); return; }; errors.push(IntegrityCheckError::PageReferencedMultipleTimes { @@ -5647,6 +5666,7 @@ pub fn integrity_check( let contents = page.get_contents(); if page_category == PageCategory::FreeListTrunk { + state.freelist_count.actual_count += 1; let next_freelist_trunk_page = contents.read_u32_no_offset(0); if next_freelist_trunk_page != 0 { state.push_page( @@ -5676,6 +5696,10 @@ pub fn integrity_check( } continue; } + if page_category == PageCategory::FreePage { + state.freelist_count.actual_count += 1; + continue; + } if page_category == PageCategory::Overflow { let next_overflow_page = contents.read_u32_no_offset(0); if next_overflow_page != 0 { diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index b58ddcb41..83e164adc 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -7363,6 +7363,9 @@ pub fn op_integrity_check( let mut current_root_idx = 0; // check freelist pages first, if there are any for database if freelist_trunk_page > 0 { + let expected_freelist_count = + return_if_io!(pager.with_header(|header| header.freelist_pages.get())); + integrity_check_state.set_expected_freelist_count(expected_freelist_count as usize); integrity_check_state.start( freelist_trunk_page as usize, PageCategory::FreeListTrunk, @@ -7389,6 +7392,14 @@ pub fn op_integrity_check( *current_root_idx += 1; return Ok(InsnFunctionStepResult::Step); } else { + if integrity_check_state.freelist_count.actual_count + != integrity_check_state.freelist_count.expected_count + { + errors.push(IntegrityCheckError::FreelistCountMismatch { + actual_count: integrity_check_state.freelist_count.actual_count, + expected_count: integrity_check_state.freelist_count.expected_count, + }); + } let message = if errors.is_empty() { "ok".to_string() } else {