From 029866a1340fc757069adee8bf8e6fb12262ebdc Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 8 Aug 2026 21:26:45 +0700 Subject: [PATCH 1/2] fix: preserve TOC insert API compatibility --- src/persistence/space/index/mod.rs | 8 ++--- .../space/index/table_of_contents.rs | 34 +++++++++++++------ src/persistence/space/index/unsized_.rs | 8 ++--- src/persistence/space/index/util.rs | 6 ++-- tests/persistence/toc/write.rs | 2 +- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/persistence/space/index/mod.rs b/src/persistence/space/index/mod.rs index f6c613a..28b214e 100644 --- a/src/persistence/space/index/mod.rs +++ b/src/persistence/space/index/mod.rs @@ -246,7 +246,7 @@ where self.next_page_id.fetch_add(1, Ordering::Relaxed).into() }; self.table_of_contents - .insert((node_id.key.clone(), node_id.value), page_id)?; + .try_insert((node_id.key.clone(), node_id.value), page_id)?; self.table_of_contents.persist(&mut self.index_file).await?; self.add_new_index_page(node_id, page_id).await?; @@ -276,7 +276,7 @@ where &(node_id.key.clone(), node_id.value), (page.inner.node_id.key.clone(), page.inner.node_id.link), ); - self.table_of_contents.insert( + self.table_of_contents.try_insert( (splitted_page.node_id.key.clone(), splitted_page.node_id.link), new_page_id, )?; @@ -437,7 +437,7 @@ where self.next_page_id.fetch_add(1, Ordering::Relaxed).into() }; self.table_of_contents - .insert((max_value.key.clone(), max_value.value), page_id)?; + .try_insert((max_value.key.clone(), max_value.value), page_id)?; let size = get_index_page_size_from_data_length::(INNER_PAGE_SIZE as usize); let mut page = IndexPage::new(max_value.clone().into(), size); @@ -489,7 +489,7 @@ where page_to_update.inner.node_id.link, ), ); - self.table_of_contents.insert( + self.table_of_contents.try_insert( (splitted_page.node_id.key.clone(), splitted_page.node_id.link), new_page_id, )?; diff --git a/src/persistence/space/index/table_of_contents.rs b/src/persistence/space/index/table_of_contents.rs index a9f88d8..ee5c77f 100644 --- a/src/persistence/space/index/table_of_contents.rs +++ b/src/persistence/space/index/table_of_contents.rs @@ -54,7 +54,21 @@ where &mut self.pages[self.current_page] } - pub fn insert(&mut self, node_id: T, page_id: PageId) -> eyre::Result<()> + /// Inserts a fresh page identity into the table of contents. + /// + /// This compatibility entry point preserves the historical infallible API. + /// Persistence paths that can report a truncated loaded chain should use + /// [`Self::try_insert`] instead. + pub fn insert(&mut self, node_id: T, page_id: PageId) + where + T: Clone + SizeMeasurable, + { + self.try_insert(node_id, page_id) + .expect("table-of-contents chain should be fully loaded"); + } + + /// Fallible variant of [`Self::insert`] for persistence paths. + pub fn try_insert(&mut self, node_id: T, page_id: PageId) -> eyre::Result<()> where T: Clone + SizeMeasurable, { @@ -241,7 +255,7 @@ mod tests { fn insert_to_empty() { let mut toc = IndexTableOfContents::::new(0.into(), Arc::new(AtomicU32::new(0))); let key = 1; - toc.insert(key, 1.into()).unwrap(); + toc.insert(key, 1.into()); let page = toc.pages[toc.current_page].clone(); assert!( @@ -259,7 +273,7 @@ mod tests { #[test] fn checked_update_reports_a_missing_identity_without_mutating_the_toc() { let mut toc = IndexTableOfContents::::new(0.into(), Arc::new(AtomicU32::new(1))); - toc.insert(7, 2.into()).unwrap(); + toc.insert(7, 2.into()); assert!(!toc.try_update_key(&8, 9)); assert_eq!(toc.get(&7), Some(2.into())); @@ -271,7 +285,7 @@ mod tests { let mut toc = IndexTableOfContents::::new(0.into(), Arc::new(AtomicU32::new(0))); let mut keys = vec![]; for key in 0..10 { - toc.insert(key, 1.into()).unwrap(); + toc.insert(key, 1.into()); keys.push(key); } @@ -296,7 +310,7 @@ mod tests { fn insert_reaches_existing_tail_after_reload_resets_cursor() { let mut toc = IndexTableOfContents::::new(0.into(), Arc::new(AtomicU32::new(0))); for key in 0..10 { - toc.insert(key, u32::from(key).into()).unwrap(); + toc.insert(key, u32::from(key).into()); } assert!(toc.pages.len() > 1, "fixture must span TOC pages"); @@ -305,7 +319,7 @@ mod tests { // forward until an existing or newly-created tail can accept it. toc.current_page = 0; let before_sizes: Vec<_> = toc.pages.iter().map(|page| page.inner.estimated_size()).collect(); - toc.insert(200, PageId::from(200)).unwrap(); + toc.insert(200, PageId::from(200)); assert_eq!(toc.get(&200), Some(PageId::from(200))); for (page, before_size) in toc.pages.iter().zip(before_sizes) { @@ -324,13 +338,13 @@ mod tests { fn insert_reports_a_truncated_segment_chain() { let mut toc = IndexTableOfContents::::new(0.into(), Arc::new(AtomicU32::new(0))); for key in 0..10 { - toc.insert(key, u32::from(key).into()).unwrap(); + toc.insert(key, u32::from(key).into()); } assert!(!toc.pages[0].header.next_id.is_empty()); toc.pages.truncate(1); toc.current_page = 0; - let error = toc.insert(200, PageId::from(200)).unwrap_err(); + let error = toc.try_insert(200, PageId::from(200)).unwrap_err(); assert!(error.to_string().contains("links past the loaded chain")); } @@ -340,7 +354,7 @@ mod tests { let mut toc = IndexTableOfContents::::new(0.into(), Arc::new(AtomicU32::new(0))); let mut keys = vec![]; for key in 0..10 { - toc.insert(key, 1.into()).unwrap(); + toc.insert(key, 1.into()); keys.push(key); } @@ -368,7 +382,7 @@ mod tests { let new_key = keys.last().unwrap() + 1; let id = toc.pop_empty_page_id().unwrap(); let before_insert_segments = toc.pages.len(); - toc.insert(new_key, id).unwrap(); + toc.insert(new_key, id); assert_eq!(toc.get(&new_key), Some(id), "reused page id was not recorded"); assert_eq!( toc.pages.len(), diff --git a/src/persistence/space/index/unsized_.rs b/src/persistence/space/index/unsized_.rs index 704e1b0..76c70ad 100644 --- a/src/persistence/space/index/unsized_.rs +++ b/src/persistence/space/index/unsized_.rs @@ -135,7 +135,7 @@ where self.next_page_id.fetch_add(1, Ordering::Relaxed).into() }; self.table_of_contents - .insert((node_id.key.clone(), node_id.value), page_id)?; + .try_insert((node_id.key.clone(), node_id.value), page_id)?; self.table_of_contents.persist(&mut self.index_file).await?; self.add_new_index_page(node_id, page_id).await?; @@ -304,7 +304,7 @@ where &(node_id.key, node_id.value), (page.inner.node_id.key.clone(), page.inner.node_id.link), ); - self.table_of_contents.insert( + self.table_of_contents.try_insert( (splitted_page.node_id.key.clone(), splitted_page.node_id.link), new_page_id, )?; @@ -489,7 +489,7 @@ where self.next_page_id.fetch_add(1, Ordering::Relaxed).into() }; self.table_of_contents - .insert((max_value.key.clone(), max_value.value), page_id)?; + .try_insert((max_value.key.clone(), max_value.value), page_id)?; let page = UnsizedIndexPage::::new(max_value.clone().into())?; let header = GeneralHeader::new(page_id, PageType::IndexUnsized, self.space_id); @@ -559,7 +559,7 @@ where )); } let right_page_key = (splitted_page.node_id.key.clone(), splitted_page.node_id.link); - self.table_of_contents.insert(right_page_key.clone(), new_page_id)?; + self.table_of_contents.try_insert(right_page_key.clone(), new_page_id)?; if self.table_of_contents.get(&right_page_key) != Some(new_page_id) { return Err(eyre!( "unsized index split identity did not become canonical (page={new_page_id:?})" diff --git a/src/persistence/space/index/util.rs b/src/persistence/space/index/util.rs index eec7cf2..cde9801 100644 --- a/src/persistence/space/index/util.rs +++ b/src/persistence/space/index/util.rs @@ -21,8 +21,7 @@ where let mut toc = IndexTableOfContents::new(0.into(), next_page_id.clone()); for page in pages { let page_id = next_page_id.fetch_add(1, Ordering::Relaxed); - toc.insert((page.node_id.key.clone(), page.node_id.link), page_id.into()) - .expect("new in-memory TOC chain should remain internally consistent"); + toc.insert((page.node_id.key.clone(), page.node_id.link), page_id.into()); let header = GeneralHeader::new(page_id.into(), PageType::Index, 0.into()); let index_page = GeneralPage { inner: page, header }; general_index_pages.push(index_page) @@ -46,8 +45,7 @@ where let mut toc = IndexTableOfContents::new(0.into(), next_page_id.clone()); for page in pages { let page_id = next_page_id.fetch_add(1, Ordering::Relaxed); - toc.insert((page.node_id.key.clone(), page.node_id.link), page_id.into()) - .expect("new in-memory TOC chain should remain internally consistent"); + toc.insert((page.node_id.key.clone(), page.node_id.link), page_id.into()); let header = GeneralHeader::new(page_id.into(), PageType::IndexUnsized, 0.into()); let index_page = GeneralPage { inner: page, header }; general_index_pages.push(index_page) diff --git a/tests/persistence/toc/write.rs b/tests/persistence/toc/write.rs index 27d5fcd..554a559 100644 --- a/tests/persistence/toc/write.rs +++ b/tests/persistence/toc/write.rs @@ -11,7 +11,7 @@ async fn test_persist_index_table_of_contents() { remove_file_if_exists("tests/data/persist_index_table_of_contents.wt.idx".to_string()).await; let mut toc = IndexTableOfContents::::new(0.into(), Arc::new(AtomicU32::new(1))); - toc.insert(13, 1.into()).unwrap(); + let _: () = toc.insert(13, 1.into()); let mut file = File::create("tests/data/persist_index_table_of_contents.wt.idx") .await .unwrap(); From ebd4aa88e40acbc48cd0a7057cba03d47c0dd814 Mon Sep 17 00:00:00 2001 From: meh Date: Sun, 9 Aug 2026 00:37:30 +0700 Subject: [PATCH 2/2] docs: pin TOC compatibility contract --- src/persistence/space/index/table_of_contents.rs | 7 +++++-- tests/persistence/toc/write.rs | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/persistence/space/index/table_of_contents.rs b/src/persistence/space/index/table_of_contents.rs index ee5c77f..ff3995f 100644 --- a/src/persistence/space/index/table_of_contents.rs +++ b/src/persistence/space/index/table_of_contents.rs @@ -56,8 +56,11 @@ where /// Inserts a fresh page identity into the table of contents. /// - /// This compatibility entry point preserves the historical infallible API. - /// Persistence paths that can report a truncated loaded chain should use + /// This restores source compatibility with the public infallible API that + /// preceded PR #63. A fully constructed table of contents owns its complete + /// segment chain, so failure here is an internal invariant violation and + /// retains the historical panic-level contract. Loaded persistence paths, + /// where a truncated chain can be reported to the caller, must use /// [`Self::try_insert`] instead. pub fn insert(&mut self, node_id: T, page_id: PageId) where diff --git a/tests/persistence/toc/write.rs b/tests/persistence/toc/write.rs index 554a559..0bb0470 100644 --- a/tests/persistence/toc/write.rs +++ b/tests/persistence/toc/write.rs @@ -11,6 +11,8 @@ async fn test_persist_index_table_of_contents() { remove_file_if_exists("tests/data/persist_index_table_of_contents.wt.idx".to_string()).await; let mut toc = IndexTableOfContents::::new(0.into(), Arc::new(AtomicU32::new(1))); + // Compile-time compatibility regression: the public API before PR #63 + // returned unit, including for callers that bind the expression's type. let _: () = toc.insert(13, 1.into()); let mut file = File::create("tests/data/persist_index_table_of_contents.wt.idx") .await