From 6b0adb8716d21603b04ef74b3c8de421b34bf6df Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:42:50 +0300 Subject: [PATCH 01/32] WIP --- src/in_memory/empty_link_registry.rs | 290 ++++++++++++++++++++++--- src/in_memory/mod.rs | 1 + src/in_memory/pages.rs | 4 - src/table/mod.rs | 1 + src/table/vacuum/fragmentation_info.rs | 45 ++++ src/table/vacuum/mod.rs | 16 ++ 6 files changed, 328 insertions(+), 29 deletions(-) create mode 100644 src/table/vacuum/fragmentation_info.rs create mode 100644 src/table/vacuum/mod.rs diff --git a/src/in_memory/empty_link_registry.rs b/src/in_memory/empty_link_registry.rs index 35d5e06..b8c6d32 100644 --- a/src/in_memory/empty_link_registry.rs +++ b/src/in_memory/empty_link_registry.rs @@ -1,11 +1,14 @@ use crate::in_memory::DATA_INNER_LENGTH; use data_bucket::Link; +use data_bucket::page::PageId; +use derive_more::Into; use indexset::concurrent::multimap::BTreeMultiMap; use indexset::concurrent::set::BTreeSet; use parking_lot::FairMutex; +use std::sync::atomic::{AtomicU32, Ordering}; /// A link wrapper that implements `Ord` based on absolute index calculation. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Into)] pub struct IndexOrdLink(pub Link); impl IndexOrdLink { @@ -73,6 +76,11 @@ impl Ord for IndexOrdLink { pub struct EmptyLinkRegistry { index_ord_links: BTreeSet>, length_ord_links: BTreeMultiMap, + + pub page_links_map: BTreeMultiMap, + + sum_links_len: AtomicU32, + op_lock: FairMutex<()>, } @@ -81,12 +89,32 @@ impl Default for EmptyLinkRegistry { Self { index_ord_links: BTreeSet::new(), length_ord_links: BTreeMultiMap::new(), + page_links_map: BTreeMultiMap::new(), + sum_links_len: Default::default(), op_lock: Default::default(), } } } impl EmptyLinkRegistry { + fn remove_link>(&self, link: L) { + let link = link.into(); + self.index_ord_links.remove(&IndexOrdLink(link)); + self.length_ord_links.remove(&link.length, &link); + self.page_links_map.remove(&link.page_id, &link); + + self.sum_links_len.fetch_sub(link.length, Ordering::AcqRel); + } + + fn insert_link>(&self, link: L) { + let link = link.into(); + self.index_ord_links.insert(IndexOrdLink(link)); + self.length_ord_links.insert(link.length, link); + self.page_links_map.insert(link.page_id, link); + + self.sum_links_len.fetch_add(link.length, Ordering::AcqRel); + } + pub fn push(&self, link: Link) { let mut index_ord_link = IndexOrdLink(link); let _g = self.op_lock.lock(); @@ -101,9 +129,7 @@ impl EmptyLinkRegistry { drop(iter); // Remove left neighbor - self.index_ord_links.remove(&possible_left_neighbor); - self.length_ord_links - .remove(&possible_left_neighbor.0.length, &possible_left_neighbor.0); + self.remove_link(possible_left_neighbor); index_ord_link = united_link; } @@ -120,39 +146,35 @@ impl EmptyLinkRegistry { drop(iter); // Remove right neighbor - self.index_ord_links.remove(&possible_right_neighbor); - self.length_ord_links.remove( - &possible_right_neighbor.0.length, - &possible_right_neighbor.0, - ); + self.remove_link(possible_right_neighbor); index_ord_link = united_link; } } } - self.index_ord_links.insert(index_ord_link); - self.length_ord_links - .insert(index_ord_link.0.length, index_ord_link.0); + self.insert_link(index_ord_link); } pub fn pop_max(&self) -> Option { let _g = self.op_lock.lock(); let mut iter = self.length_ord_links.iter().rev(); - let (len, max_length_link) = iter.next()?; - let index_ord_link = IndexOrdLink(*max_length_link); + let (_, max_length_link) = iter.next()?; drop(iter); - self.length_ord_links.remove(len, max_length_link); - self.index_ord_links.remove(&index_ord_link); + self.remove_link(*max_length_link); - Some(index_ord_link.0) + Some(*max_length_link) } pub fn iter(&self) -> impl Iterator + '_ { self.index_ord_links.iter().map(|l| l.0) } + + pub fn get_empty_links_size_bytes(&self) -> u32 { + self.sum_links_len.load(Ordering::Acquire) + } } #[cfg(test)] @@ -160,7 +182,142 @@ mod tests { use super::*; #[test] - fn test_empty_link_registry_insert_and_pop() { + fn test_unite_with_right_neighbor() { + let left = IndexOrdLink::(Link { + page_id: 1.into(), + offset: 0, + length: 100, + }); + + let right = IndexOrdLink::(Link { + page_id: 1.into(), + offset: 100, + length: 50, + }); + + let united = left.unite_with_right_neighbor(&right).unwrap(); + assert_eq!(united.0.page_id, 1.into()); + assert_eq!(united.0.offset, 0); + assert_eq!(united.0.length, 150); + } + + #[test] + fn test_unite_with_left_neighbor() { + let left = IndexOrdLink::(Link { + page_id: 1.into(), + offset: 0, + length: 100, + }); + + let right = IndexOrdLink::(Link { + page_id: 1.into(), + offset: 100, + length: 50, + }); + + let united = right.unite_with_left_neighbor(&left).unwrap(); + assert_eq!(united.0.page_id, 1.into()); + assert_eq!(united.0.offset, 0); + assert_eq!(united.0.length, 150); + } + + #[test] + fn test_unite_fails_on_gap() { + let link1 = IndexOrdLink::(Link { + page_id: 1.into(), + offset: 0, + length: 100, + }); + + let link2 = IndexOrdLink::(Link { + page_id: 1.into(), + offset: 200, + length: 50, + }); + + assert!(link1.unite_with_right_neighbor(&link2).is_none()); + assert!(link2.unite_with_left_neighbor(&link1).is_none()); + } + + #[test] + fn test_unite_fails_on_different_pages() { + let link1 = IndexOrdLink::(Link { + page_id: 1.into(), + offset: 0, + length: 100, + }); + + let link2 = IndexOrdLink::(Link { + page_id: 2.into(), + offset: 100, + length: 50, + }); + + assert!(link1.unite_with_right_neighbor(&link2).is_none()); + assert!(link2.unite_with_left_neighbor(&link1).is_none()); + } + + #[test] + fn test_index_ord_link_ordering() { + const TEST_DATA_LENGTH: usize = 1000; + + let link1 = IndexOrdLink::(Link { + page_id: 1.into(), + offset: 0, + length: 100, + }); + + let link2 = IndexOrdLink::(Link { + page_id: 1.into(), + offset: 100, + length: 50, + }); + + let link3 = IndexOrdLink::(Link { + page_id: 2.into(), + offset: 0, + length: 200, + }); + + assert!(link1 < link2); + assert!(link2 < link3); + assert!(link1 < link3); + } + + #[test] + fn test_push_merges_both_sides() { + let registry = EmptyLinkRegistry::::default(); + + let left = Link { + page_id: 1.into(), + offset: 0, + length: 100, + }; + + let middle = Link { + page_id: 1.into(), + offset: 100, + length: 50, + }; + + let right = Link { + page_id: 1.into(), + offset: 150, + length: 75, + }; + + registry.push(left); + registry.push(right); + registry.push(middle); + + let result = registry.pop_max().unwrap(); + assert_eq!(result.page_id, 1.into()); + assert_eq!(result.offset, 0); + assert_eq!(result.length, 225); + } + + #[test] + fn test_push_non_adjacent_no_merge() { let registry = EmptyLinkRegistry::::default(); let link1 = Link { @@ -170,13 +327,70 @@ mod tests { }; let link2 = Link { + page_id: 1.into(), + offset: 200, + length: 50, + }; + + registry.push(link1); + registry.push(link2); + + let pop1 = registry.pop_max().unwrap(); + let pop2 = registry.pop_max().unwrap(); + + assert_eq!(pop1.length, 100); + assert_eq!(pop2.length, 50); + } + + #[test] + fn test_pop_max_returns_largest() { + let registry = EmptyLinkRegistry::::default(); + + let small = Link { + page_id: 1.into(), + offset: 0, + length: 50, + }; + + let large = Link { page_id: 1.into(), offset: 100, + length: 200, + }; + + let medium = Link { + page_id: 1.into(), + offset: 300, + length: 100, + }; + + registry.push(small); + registry.push(large); + registry.push(medium); + + assert_eq!(registry.pop_max().unwrap().length, 200); + assert_eq!(registry.pop_max().unwrap().length, 100); + assert_eq!(registry.pop_max().unwrap().length, 50); + } + + #[test] + fn test_iter_returns_all_links() { + let registry = EmptyLinkRegistry::::default(); + + let link1 = Link { + page_id: 1.into(), + offset: 0, + length: 100, + }; + + let link2 = Link { + page_id: 2.into(), + offset: 0, length: 150, }; let link3 = Link { - page_id: 2.into(), + page_id: 3.into(), offset: 0, length: 200, }; @@ -185,15 +399,41 @@ mod tests { registry.push(link2); registry.push(link3); - // After inserting link1 and link2, they should be united - let united_link = Link { + let links: Vec = registry.iter().collect(); + assert_eq!(links.len(), 3); + } + + #[test] + fn test_empty_registry() { + let registry = EmptyLinkRegistry::::default(); + + assert_eq!(registry.pop_max(), None); + assert_eq!(registry.iter().count(), 0); + } + + #[test] + fn test_sum_links_counter() { + let registry = EmptyLinkRegistry::::default(); + + let link1 = Link { page_id: 1.into(), offset: 0, - length: 250, + length: 100, }; - assert_eq!(registry.pop_max(), Some(united_link)); - assert_eq!(registry.pop_max(), Some(link3)); - assert_eq!(registry.pop_max(), None); + let link2 = Link { + page_id: 1.into(), + offset: 100, + length: 150, + }; + + registry.push(link1); + assert_eq!(registry.sum_links_len.load(Ordering::Acquire), 100); + + registry.push(link2); + assert_eq!(registry.sum_links_len.load(Ordering::Acquire), 250); + + registry.pop_max(); + assert_eq!(registry.sum_links_len.load(Ordering::Acquire), 0); } } diff --git a/src/in_memory/mod.rs b/src/in_memory/mod.rs index addfaae..c032ea1 100644 --- a/src/in_memory/mod.rs +++ b/src/in_memory/mod.rs @@ -4,5 +4,6 @@ mod pages; mod row; pub use data::{DATA_INNER_LENGTH, Data, ExecutionError as DataExecutionError}; +pub use empty_link_registry::EmptyLinkRegistry; pub use pages::{DataPages, ExecutionError as PagesExecutionError}; pub use row::{GhostWrapper, Query, RowWrapper, StorableRow}; diff --git a/src/in_memory/pages.rs b/src/in_memory/pages.rs index 9432979..6941025 100644 --- a/src/in_memory/pages.rs +++ b/src/in_memory/pages.rs @@ -102,9 +102,7 @@ where { let general_row = ::WrappedRow::from_inner(row); - //println!("Popping empty link"); if let Some(link) = self.empty_links.pop_max() { - //println!("Empty link len {}", self.empty_links.len()); let pages = self.pages.read().unwrap(); let current_page: usize = page_id_mapper(link.page_id.into()); let page = &pages[current_page]; @@ -112,9 +110,7 @@ where match unsafe { page.try_save_row_by_link(&general_row, link) } { Ok((link, left_link)) => { if let Some(l) = left_link { - //println!("Pushing empty link"); self.empty_links.push(l); - //println!("Pushed empty link"); } return Ok(link); } diff --git a/src/table/mod.rs b/src/table/mod.rs index 19f4668..766cd03 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -1,5 +1,6 @@ pub mod select; pub mod system_info; +pub mod vacuum; use std::fmt::Debug; use std::marker::PhantomData; diff --git a/src/table/vacuum/fragmentation_info.rs b/src/table/vacuum/fragmentation_info.rs new file mode 100644 index 0000000..2f03b7a --- /dev/null +++ b/src/table/vacuum/fragmentation_info.rs @@ -0,0 +1,45 @@ +use std::collections::HashMap; + +use data_bucket::page::PageId; + +use crate::in_memory::EmptyLinkRegistry; + +#[derive(Debug, Copy, Clone)] +pub struct PageFragmentationInfo { + pub page_id: PageId, + pub empty_bytes: u32, + pub filled_empty_ratio: f64, +} + +impl EmptyLinkRegistry { + pub fn get_per_page_info(&self) -> Vec> { + let mut page_empty_bytes: HashMap = HashMap::new(); + + for (page_id, link) in self.page_links_map.iter() { + let entry = page_empty_bytes.entry(*page_id).or_insert(0); + *entry += link.length; + } + + let mut per_page_data: Vec> = page_empty_bytes + .into_iter() + .map(|(page_id, empty_bytes)| { + let filled_empty_ratio = if empty_bytes > 0 { + let filled_bytes = DATA_LENGTH.saturating_sub(empty_bytes as usize); + filled_bytes as f64 / empty_bytes as f64 + } else { + 0.0 + }; + + PageFragmentationInfo { + page_id, + empty_bytes, + filled_empty_ratio, + } + }) + .collect(); + + per_page_data.sort_by_key(|info| info.page_id); + + per_page_data + } +} diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs new file mode 100644 index 0000000..8e33dc2 --- /dev/null +++ b/src/table/vacuum/mod.rs @@ -0,0 +1,16 @@ +mod fragmentation_info; + +use crate::in_memory::{DataPages, StorableRow}; + +#[derive(Debug)] +pub struct EmptyDataVacuum { + data_pages: DataPages, +} + +impl EmptyDataVacuum { + pub fn new(data_pages: DataPages) -> Self { + Self { data_pages } + } + + pub fn vacuum_pages() -> eyre::Result<()> {} +} From c87f2535ecacedb7e8c81f20a8722b78e0ad2a95 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:51:44 +0300 Subject: [PATCH 02/32] WIP --- src/in_memory/pages.rs | 4 + src/lock/mod.rs | 2 +- src/lock/row_lock.rs | 40 +++++ src/table/vacuum/fragmentation_info.rs | 9 ++ src/table/vacuum/lock.rs | 196 +++++++++++++++++++++++++ src/table/vacuum/mod.rs | 96 +++++++++++- src/table/vacuum/page.rs | 1 + 7 files changed, 340 insertions(+), 8 deletions(-) create mode 100644 src/table/vacuum/lock.rs create mode 100644 src/table/vacuum/page.rs diff --git a/src/in_memory/pages.rs b/src/in_memory/pages.rs index 6941025..b710af8 100644 --- a/src/in_memory/pages.rs +++ b/src/in_memory/pages.rs @@ -347,6 +347,10 @@ where self.empty_links.iter().collect() } + pub fn empty_links_registry(&self) -> &EmptyLinkRegistry { + &self.empty_links + } + pub fn with_empty_links(mut self, links: Vec) -> Self { let registry = EmptyLinkRegistry::default(); for l in links { diff --git a/src/lock/mod.rs b/src/lock/mod.rs index 4c5a230..959b7a6 100644 --- a/src/lock/mod.rs +++ b/src/lock/mod.rs @@ -12,7 +12,7 @@ use futures::task::AtomicWaker; use parking_lot::Mutex; pub use map::LockMap; -pub use row_lock::RowLock; +pub use row_lock::{FullRowLock, RowLock}; #[derive(Debug)] pub struct Lock { diff --git a/src/lock/row_lock.rs b/src/lock/row_lock.rs index 5720c3b..e8fe2dd 100644 --- a/src/lock/row_lock.rs +++ b/src/lock/row_lock.rs @@ -18,3 +18,43 @@ pub trait RowLock { where Self: Sized; } + +/// Full row lock represented by a single lock. +/// Unlike generated per-column lock types, this uses one lock for the entire +/// row. +#[derive(Debug)] +pub struct FullRowLock { + l: Arc, +} + +impl RowLock for FullRowLock { + fn is_locked(&self) -> bool { + self.l.is_locked() + } + + fn with_lock(id: u16) -> (Self, Arc) + where + Self: Sized, + { + let l = Arc::new(Lock::new(id)); + (FullRowLock { l: l.clone() }, l) + } + + fn lock(&mut self, id: u16) -> (HashSet>, Arc) { + let mut set = HashSet::new(); + let l = Arc::new(Lock::new(id)); + set.insert(self.l.clone()); + self.l = l.clone(); + + (set, l) + } + + fn merge(&mut self, other: &mut Self) -> HashSet> + where + Self: Sized, + { + let set = HashSet::from_iter([self.l.clone()]); + self.l = other.l.clone(); + set + } +} diff --git a/src/table/vacuum/fragmentation_info.rs b/src/table/vacuum/fragmentation_info.rs index 2f03b7a..e70e895 100644 --- a/src/table/vacuum/fragmentation_info.rs +++ b/src/table/vacuum/fragmentation_info.rs @@ -1,17 +1,26 @@ use std::collections::HashMap; +use data_bucket::Link; use data_bucket::page::PageId; use crate::in_memory::EmptyLinkRegistry; +/// Fragmentation info for a single data [`Page`]. +/// +/// [`Page`]: crate::in_memory::Data #[derive(Debug, Copy, Clone)] pub struct PageFragmentationInfo { pub page_id: PageId, pub empty_bytes: u32, + /// Ratio of filled bytes to empty bytes. Higher means more utilized. pub filled_empty_ratio: f64, } impl EmptyLinkRegistry { + pub fn get_page_empty_links(&self, page_id: PageId) -> Vec { + self.page_links_map.get(&page_id).map(|(_, link)| *link).collect() + } + pub fn get_per_page_info(&self) -> Vec> { let mut page_empty_bytes: HashMap = HashMap::new(); diff --git a/src/table/vacuum/lock.rs b/src/table/vacuum/lock.rs new file mode 100644 index 0000000..cdd1d52 --- /dev/null +++ b/src/table/vacuum/lock.rs @@ -0,0 +1,196 @@ +use std::sync::Arc; + +use data_bucket::Link; +use data_bucket::page::PageId; + +use crate::lock::{FullRowLock, LockMap, RowLock}; + +/// Lock manager for vacuum operations. +/// Supports locking at both page and link granularity. +#[derive(Debug, Default)] +pub struct VacuumLock { + per_link_lock: Arc>, + per_page_lock: Arc>, +} + +impl VacuumLock { + /// Locks a page, returning the [`FullRowLock`]. + pub async fn lock_page(&self, page_id: PageId) -> Arc> { + if let Some(lock) = self.per_page_lock.get(&page_id) { + return lock; + } + + let (row_lock, _) = FullRowLock::with_lock(self.per_page_lock.next_id()); + let lock = Arc::new(tokio::sync::RwLock::new(row_lock)); + self.per_page_lock.insert(page_id, lock.clone()); + lock + } + + /// Locks a [`Link`], returning the [`FullRowLock`]. + pub async fn lock_link(&self, link: Link) -> Arc> { + if let Some(lock) = self.per_link_lock.get(&link) { + return lock; + } + + let (row_lock, _lock) = FullRowLock::with_lock(self.per_link_lock.next_id()); + let lock = Arc::new(tokio::sync::RwLock::new(row_lock)); + self.per_link_lock.insert(link, lock.clone()); + lock + } + + /// Checks if a [`Link`] is locked. + /// [`Link`] is locked if it was locked OR its page is locked. + pub fn is_link_locked(&self, link: &Link) -> bool { + if let Some(page_lock) = self.per_page_lock.get(&link.page_id) { + match page_lock.try_read() { + Ok(guard) => { + if guard.is_locked() { + return true; + } + } + Err(_) => return true, // write lock held + } + } + + if let Some(link_lock) = self.per_link_lock.get(link) { + match link_lock.try_read() { + Ok(guard) => { + if guard.is_locked() { + return true; + } + } + Err(_) => return true, // write lock held + } + } + + false + } + + /// Checks if a page is locked. + pub fn is_page_locked(&self, page_id: &PageId) -> bool { + if let Some(page_lock) = self.per_page_lock.get(page_id) { + match page_lock.try_read() { + Ok(guard) => guard.is_locked(), + Err(_) => true, // write lock held + } + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_page_locked_not_locked() { + let vacuum_lock = VacuumLock::default(); + let page_id = PageId::from(1); + + assert!(!vacuum_lock.is_page_locked(&page_id)); + } + + #[tokio::test] + async fn test_is_page_locked_after_lock() { + let vacuum_lock = VacuumLock::default(); + let page_id = PageId::from(1); + + let _lock = vacuum_lock.lock_page(page_id).await; + + assert!(vacuum_lock.is_page_locked(&page_id)); + } + + #[tokio::test] + async fn test_is_page_locked_with_write_lock() { + let vacuum_lock = VacuumLock::default(); + let page_id = PageId::from(1); + + let lock = vacuum_lock.lock_page(page_id).await; + let _write_guard = lock.write().await; + + assert!(vacuum_lock.is_page_locked(&page_id)); + } + + #[test] + fn test_is_link_locked_not_locked() { + let vacuum_lock = VacuumLock::default(); + let link = Link { + page_id: PageId::from(1), + offset: 0, + length: 100, + }; + + assert!(!vacuum_lock.is_link_locked(&link)); + } + + #[tokio::test] + async fn test_is_link_locked_by_link() { + let vacuum_lock = VacuumLock::default(); + let link = Link { + page_id: PageId::from(1), + offset: 0, + length: 100, + }; + + let _lock = vacuum_lock.lock_link(link).await; + + assert!(vacuum_lock.is_link_locked(&link)); + } + + #[tokio::test] + async fn test_is_link_locked_by_page() { + let vacuum_lock = VacuumLock::default(); + let link = Link { + page_id: PageId::from(1), + offset: 0, + length: 100, + }; + + let _lock = vacuum_lock.lock_page(link.page_id).await; + + assert!(vacuum_lock.is_link_locked(&link)); + } + + #[tokio::test] + async fn test_is_link_locked_with_link_write_lock() { + let vacuum_lock = VacuumLock::default(); + let link = Link { + page_id: PageId::from(1), + offset: 0, + length: 100, + }; + + let lock = vacuum_lock.lock_link(link).await; + let _write_guard = lock.write().await; + + assert!(vacuum_lock.is_link_locked(&link)); + } + + #[tokio::test] + async fn test_lock_page_returns_same_lock() { + let vacuum_lock = VacuumLock::default(); + let page_id = PageId::from(1); + + let lock1 = vacuum_lock.lock_page(page_id).await; + let lock2 = vacuum_lock.lock_page(page_id).await; + + // Same pointer = same lock instance + assert!(Arc::ptr_eq(&lock1, &lock2)); + } + + #[tokio::test] + async fn test_lock_link_returns_same_lock() { + let vacuum_lock = VacuumLock::default(); + let link = Link { + page_id: PageId::from(1), + offset: 0, + length: 100, + }; + + let lock1 = vacuum_lock.lock_link(link).await; + let lock2 = vacuum_lock.lock_link(link).await; + + assert!(Arc::ptr_eq(&lock1, &lock2)); + } +} diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 8e33dc2..3e74038 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -1,16 +1,98 @@ mod fragmentation_info; +mod lock; +mod page; -use crate::in_memory::{DataPages, StorableRow}; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::sync::Arc; + +use data_bucket::Link; +use indexset::core::node::NodeLike; +use indexset::core::pair::Pair; +use rkyv::rancor::Strategy; +use rkyv::ser::Serializer; +use rkyv::ser::allocator::ArenaHandle; +use rkyv::ser::sharing::Share; +use rkyv::util::AlignedVec; +use rkyv::{Archive, Serialize}; + +use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; +use crate::lock::{FullRowLock, LockMap}; +use crate::prelude::TablePrimaryKey; +use crate::vacuum::fragmentation_info::PageFragmentationInfo; +use crate::{AvailableIndex, IndexMap, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; #[derive(Debug)] -pub struct EmptyDataVacuum { +pub struct EmptyDataVacuum< + Row, + PrimaryKey, + PkNodeType, + SecondaryIndexes, + SecondaryEvents, + AvailableTypes, + AvailableIndexes, + const DATA_LENGTH: usize, +> where + PrimaryKey: Clone + Ord + Send + 'static + std::hash::Hash, + Row: StorableRow + Send + Clone + 'static, + PkNodeType: NodeLike> + Send + 'static, +{ data_pages: DataPages, + vacuum_lock: Arc>, + + primary_index: Arc>, + secondary_indexes: Arc, + + phantom_data: PhantomData<(SecondaryEvents, AvailableTypes, AvailableIndexes)>, } -impl EmptyDataVacuum { - pub fn new(data_pages: DataPages) -> Self { - Self { data_pages } - } +impl< + Row, + PrimaryKey, + PkNodeType, + SecondaryIndexes, + SecondaryEvents, + AvailableTypes, + AvailableIndexes, + const DATA_LENGTH: usize, +> + EmptyDataVacuum< + Row, + PrimaryKey, + PkNodeType, + SecondaryIndexes, + SecondaryEvents, + AvailableTypes, + AvailableIndexes, + DATA_LENGTH, + > +where + Row: TableRow + StorableRow + Send + Clone + 'static, + PrimaryKey: Debug + Clone + Ord + Send + TablePrimaryKey + std::hash::Hash, + PkNodeType: NodeLike> + Send + 'static, + ::WrappedRow: RowWrapper, + Row: Archive + + Clone + + for<'a> Serialize< + Strategy, Share>, rkyv::rancor::Error>, + >, + ::WrappedRow: Archive + + for<'a> Serialize< + Strategy, Share>, rkyv::rancor::Error>, + >, + <::WrappedRow as Archive>::Archived: GhostWrapper, + SecondaryIndexes: TableSecondaryIndex + + TableSecondaryIndexCdc, + AvailableIndexes: Debug + AvailableIndex, +{ + // fn defragment_page(&self, info: PageFragmentationInfo) - pub fn vacuum_pages() -> eyre::Result<()> {} + // pub fn new(data_pages: DataPages) -> Self { + // Self { + // data_pages, + // vacuum_lock: Arc::new(Default::default()), + // } + // } + // + // pub fn vacuum_pages() -> eyre::Result<()> {} } diff --git a/src/table/vacuum/page.rs b/src/table/vacuum/page.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/table/vacuum/page.rs @@ -0,0 +1 @@ + From 5d96cffde54bc7a3ce203d765eeac106a0cdb249 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:05:53 +0300 Subject: [PATCH 03/32] add OffsetEqLink --- src/table/vacuum/fragmentation_info.rs | 5 +- src/table/vacuum/lock.rs | 22 +-- src/table/vacuum/mod.rs | 16 ++- src/util/mod.rs | 1 + src/util/offset_eq_link.rs | 186 +++++++++++++++++++++++++ 5 files changed, 216 insertions(+), 14 deletions(-) create mode 100644 src/util/offset_eq_link.rs diff --git a/src/table/vacuum/fragmentation_info.rs b/src/table/vacuum/fragmentation_info.rs index e70e895..f52106a 100644 --- a/src/table/vacuum/fragmentation_info.rs +++ b/src/table/vacuum/fragmentation_info.rs @@ -18,7 +18,10 @@ pub struct PageFragmentationInfo { impl EmptyLinkRegistry { pub fn get_page_empty_links(&self, page_id: PageId) -> Vec { - self.page_links_map.get(&page_id).map(|(_, link)| *link).collect() + self.page_links_map + .get(&page_id) + .map(|(_, link)| *link) + .collect() } pub fn get_per_page_info(&self) -> Vec> { diff --git a/src/table/vacuum/lock.rs b/src/table/vacuum/lock.rs index cdd1d52..0f8dd74 100644 --- a/src/table/vacuum/lock.rs +++ b/src/table/vacuum/lock.rs @@ -15,7 +15,7 @@ pub struct VacuumLock { impl VacuumLock { /// Locks a page, returning the [`FullRowLock`]. - pub async fn lock_page(&self, page_id: PageId) -> Arc> { + pub fn lock_page(&self, page_id: PageId) -> Arc> { if let Some(lock) = self.per_page_lock.get(&page_id) { return lock; } @@ -27,7 +27,7 @@ impl VacuumLock { } /// Locks a [`Link`], returning the [`FullRowLock`]. - pub async fn lock_link(&self, link: Link) -> Arc> { + pub fn lock_link(&self, link: Link) -> Arc> { if let Some(lock) = self.per_link_lock.get(&link) { return lock; } @@ -96,7 +96,7 @@ mod tests { let vacuum_lock = VacuumLock::default(); let page_id = PageId::from(1); - let _lock = vacuum_lock.lock_page(page_id).await; + let _lock = vacuum_lock.lock_page(page_id); assert!(vacuum_lock.is_page_locked(&page_id)); } @@ -106,7 +106,7 @@ mod tests { let vacuum_lock = VacuumLock::default(); let page_id = PageId::from(1); - let lock = vacuum_lock.lock_page(page_id).await; + let lock = vacuum_lock.lock_page(page_id); let _write_guard = lock.write().await; assert!(vacuum_lock.is_page_locked(&page_id)); @@ -133,7 +133,7 @@ mod tests { length: 100, }; - let _lock = vacuum_lock.lock_link(link).await; + let _lock = vacuum_lock.lock_link(link); assert!(vacuum_lock.is_link_locked(&link)); } @@ -147,7 +147,7 @@ mod tests { length: 100, }; - let _lock = vacuum_lock.lock_page(link.page_id).await; + let _lock = vacuum_lock.lock_page(link.page_id); assert!(vacuum_lock.is_link_locked(&link)); } @@ -161,7 +161,7 @@ mod tests { length: 100, }; - let lock = vacuum_lock.lock_link(link).await; + let lock = vacuum_lock.lock_link(link); let _write_guard = lock.write().await; assert!(vacuum_lock.is_link_locked(&link)); @@ -172,8 +172,8 @@ mod tests { let vacuum_lock = VacuumLock::default(); let page_id = PageId::from(1); - let lock1 = vacuum_lock.lock_page(page_id).await; - let lock2 = vacuum_lock.lock_page(page_id).await; + let lock1 = vacuum_lock.lock_page(page_id); + let lock2 = vacuum_lock.lock_page(page_id); // Same pointer = same lock instance assert!(Arc::ptr_eq(&lock1, &lock2)); @@ -188,8 +188,8 @@ mod tests { length: 100, }; - let lock1 = vacuum_lock.lock_link(link).await; - let lock2 = vacuum_lock.lock_link(link).await; + let lock1 = vacuum_lock.lock_link(link); + let lock2 = vacuum_lock.lock_link(link); assert!(Arc::ptr_eq(&lock1, &lock2)); } diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 3e74038..cd2ae5f 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -20,6 +20,7 @@ use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; use crate::lock::{FullRowLock, LockMap}; use crate::prelude::TablePrimaryKey; use crate::vacuum::fragmentation_info::PageFragmentationInfo; +use crate::vacuum::lock::VacuumLock; use crate::{AvailableIndex, IndexMap, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; #[derive(Debug)] @@ -38,7 +39,7 @@ pub struct EmptyDataVacuum< PkNodeType: NodeLike> + Send + 'static, { data_pages: DataPages, - vacuum_lock: Arc>, + vacuum_lock: Arc, primary_index: Arc>, secondary_indexes: Arc, @@ -85,7 +86,18 @@ where + TableSecondaryIndexCdc, AvailableIndexes: Debug + AvailableIndex, { - // fn defragment_page(&self, info: PageFragmentationInfo) + async fn defragment_page(&self, info: PageFragmentationInfo) { + let lock = self.vacuum_lock.lock_page(info.page_id); + + let mut page_empty_links = self + .data_pages + .empty_links_registry() + .page_links_map + .get(&info.page_id) + .map(|(_, l)| *l) + .collect::>(); + page_empty_links.sort_by(|l1, l2| l1.offset.cmp(&l2.offset)); + } // pub fn new(data_pages: DataPages) -> Self { // Self { diff --git a/src/util/mod.rs b/src/util/mod.rs index 7e68e97..8260445 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,4 @@ +mod offset_eq_link; mod optimized_vec; mod ordered_float; diff --git a/src/util/offset_eq_link.rs b/src/util/offset_eq_link.rs new file mode 100644 index 0000000..06fbfff --- /dev/null +++ b/src/util/offset_eq_link.rs @@ -0,0 +1,186 @@ +//! A link wrapper with equality based on absolute position. +//! +//! [`OffsetEqLink`] wraps a [`Link`] and implements `Eq`, `Ord`, and `Hash` +//! based on its absolute index within the data pages, rather than on the +//! raw `Link` fields. + +use data_bucket::Link; + +use crate::in_memory::DATA_INNER_LENGTH; +use crate::prelude::Into; + +/// A link wrapper that implements `Eq` based on absolute index. +#[derive(Copy, Clone, Debug, Into)] +pub struct OffsetEqLink(pub Link); + +impl OffsetEqLink { + /// Calculates the absolute index of the link. + fn absolute_index(&self) -> u64 { + let page_id: u32 = self.0.page_id.into(); + (page_id as u64 * DATA_LENGTH as u64) + self.0.offset as u64 + } +} + +impl std::hash::Hash for OffsetEqLink { + fn hash(&self, state: &mut H) { + self.absolute_index().hash(state); + } +} + +impl PartialOrd for OffsetEqLink { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for OffsetEqLink { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.absolute_index().cmp(&other.absolute_index()) + } +} + +impl PartialEq for OffsetEqLink { + fn eq(&self, other: &Self) -> bool { + self.absolute_index().eq(&other.absolute_index()) + } +} + +impl Eq for OffsetEqLink {} + +#[cfg(test)] +mod tests { + use super::*; + use data_bucket::page::PageId; + use std::collections::HashSet; + + const TEST_DATA_LENGTH: usize = 4096; + + #[test] + fn test_same_position_different_length_are_equal() { + let link1 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }); + let link2 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 100, + length: 100, + }); + + assert_eq!(link1, link2); + } + + #[test] + fn test_different_page_not_equal() { + let link1 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }); + let link2 = OffsetEqLink::(Link { + page_id: PageId::from(2), + offset: 100, + length: 50, + }); + + assert_ne!(link1, link2); + } + + #[test] + fn test_different_offset_not_equal() { + let link1 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }); + let link2 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 200, + length: 50, + }); + + assert_ne!(link1, link2); + } + + #[test] + fn test_ordering_same_page() { + let link1 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }); + let link2 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 200, + length: 50, + }); + + assert!(link1 < link2); + } + + #[test] + fn test_ordering_different_pages() { + let link1 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 4000, + length: 50, + }); + let link2 = OffsetEqLink::(Link { + page_id: PageId::from(2), + offset: 100, + length: 50, + }); + + assert!(link1 < link2); // page 1 end < page 2 start + } + + #[test] + fn test_ordering_within_page_boundaries() { + let link1 = OffsetEqLink::(Link { + page_id: PageId::from(0), + offset: 0, + length: 10, + }); + let link2 = OffsetEqLink::(Link { + page_id: PageId::from(0), + offset: TEST_DATA_LENGTH as u32 - 1, + length: 10, + }); + let link3 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 0, + length: 10, + }); + + assert!(link1 < link2); + assert!(link2 < link3); + } + + #[test] + fn test_hash_consistent_with_equality() { + let link1 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }); + let link2 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 100, + length: 100, + }); + let link3 = OffsetEqLink::(Link { + page_id: PageId::from(1), + offset: 200, + length: 50, + }); + + let mut set = HashSet::new(); + set.insert(link1); + set.insert(link2); + set.insert(link3); + + // link1 and link2 are equal, so only 2 elements in set + assert_eq!(set.len(), 2); + } +} From 8b77e19230596773adb42ac284506b34f1ce70ce Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Sun, 18 Jan 2026 18:18:03 +0300 Subject: [PATCH 04/32] WIP --- Cargo.toml | 4 +- .../persist_table/generator/space_file/mod.rs | 24 +++++- codegen/src/worktable/generator/index/mod.rs | 8 +- .../src/worktable/generator/index/usual.rs | 31 ++----- .../src/worktable/generator/queries/delete.rs | 8 +- .../worktable/generator/queries/in_place.rs | 2 +- .../src/worktable/generator/queries/select.rs | 2 +- .../src/worktable/generator/queries/update.rs | 8 +- .../src/worktable/generator/table/impls.rs | 6 +- .../worktable/generator/table/index_fns.rs | 4 +- codegen/src/worktable/generator/table/mod.rs | 4 +- src/index/mod.rs | 2 +- src/index/table_index/cdc.rs | 39 ++++++--- src/index/table_index/mod.rs | 30 ++++--- src/index/table_index/util.rs | 85 +++++++++++++++++++ src/lib.rs | 6 +- src/mem_stat/mod.rs | 12 +++ src/table/mod.rs | 49 ++++++----- src/table/system_info.rs | 4 +- src/table/vacuum/mod.rs | 1 - src/util/mod.rs | 1 + src/util/offset_eq_link.rs | 35 +++++++- 22 files changed, 258 insertions(+), 107 deletions(-) create mode 100644 src/index/table_index/util.rs diff --git a/Cargo.toml b/Cargo.toml index 739079f..afc8875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,9 +27,9 @@ lockfree = { version = "0.5.1" } fastrand = "2.3.0" futures = "0.3.30" uuid = { version = "1.10.0", features = ["v4", "v7"] } -data_bucket = "=0.3.9" +# data_bucket = "=0.3.9" # data_bucket = { git = "https://github.com/pathscale/DataBucket", branch = "page_cdc_correction", version = "0.2.7" } -# data_bucket = { path = "../DataBucket", version = "0.3.8" } +data_bucket = { path = "../DataBucket", version = "0.3.10" } performance_measurement_codegen = { path = "performance_measurement/codegen", version = "0.1.0", optional = true } performance_measurement = { path = "performance_measurement", version = "0.1.0", optional = true } indexset = { version = "=0.14.0", features = ["concurrent", "cdc", "multimap"] } diff --git a/codegen/src/persist_table/generator/space_file/mod.rs b/codegen/src/persist_table/generator/space_file/mod.rs index 07a8619..5b30b66 100644 --- a/codegen/src/persist_table/generator/space_file/mod.rs +++ b/codegen/src/persist_table/generator/space_file/mod.rs @@ -159,18 +159,34 @@ impl Generator { let pk_map = if self.attributes.pk_unsized { let pk_ident = &self.pk_ident; quote! { - let pk_map = IndexMap::<#pk_ident, Link, UnsizedNode<_>>::with_maximum_node_size(#const_name); + let pk_map = IndexMap::<#pk_ident, OffsetEqLink, UnsizedNode<_>>::with_maximum_node_size(#const_name); for page in self.primary_index.1 { - let node = page.inner.get_node(); + let node = page + .inner + .get_node() + .into_iter() + .map(|p| IndexPair { + key: p.key, + value: p.value.into(), + }) + .collect(); pk_map.attach_node(UnsizedNode::from_inner(node, #const_name)); } } } else { quote! { let size = get_index_page_size_from_data_length::<#pk_type>(#const_name); - let pk_map = IndexMap::with_maximum_node_size(size); + let pk_map = IndexMap::<_, OffsetEqLink>::with_maximum_node_size(size); for page in self.primary_index.1 { - let node = page.inner.get_node(); + let node = page + .inner + .get_node() + .into_iter() + .map(|p| IndexPair { + key: p.key, + value: p.value.into(), + }) + .collect(); pk_map.attach_node(node); } } diff --git a/codegen/src/worktable/generator/index/mod.rs b/codegen/src/worktable/generator/index/mod.rs index a258df2..af30d1a 100644 --- a/codegen/src/worktable/generator/index/mod.rs +++ b/codegen/src/worktable/generator/index/mod.rs @@ -60,16 +60,16 @@ impl Generator { let res = if idx.is_unique { if is_unsized(&t.to_string()) { quote! { - #i: IndexMap<#t, Link, UnsizedNode>> + #i: IndexMap<#t, OffsetEqLink, UnsizedNode>> } } else { - quote! {#i: IndexMap<#t, Link>} + quote! {#i: IndexMap<#t, OffsetEqLink>} } } else { if is_unsized(&t.to_string()) { - quote! {#i: IndexMultiMap<#t, Link, UnsizedNode>>} + quote! {#i: IndexMultiMap<#t, OffsetEqLink, UnsizedNode>>} } else { - quote! {#i: IndexMultiMap<#t, Link>} + quote! {#i: IndexMultiMap<#t, OffsetEqLink>} } }; Ok::<_, syn::Error>(res) diff --git a/codegen/src/worktable/generator/index/usual.rs b/codegen/src/worktable/generator/index/usual.rs index 1bc508c..6989b31 100644 --- a/codegen/src/worktable/generator/index/usual.rs +++ b/codegen/src/worktable/generator/index/usual.rs @@ -125,15 +125,15 @@ impl Generator { let remove = if idx.is_unique { quote! { if val_new == val_old { - self.#index_field_name.insert(val_new.clone(), link_new); + TableIndex::insert(&self.#index_field_name, val_new.clone(), link_new); } else { - TableIndex::remove(&self.#index_field_name, val_old, link_old); + TableIndex::remove(&self.#index_field_name, &val_old, link_old); } } } else { quote! { - self.#index_field_name.insert(val_new.clone(), link_new); - TableIndex::remove(&self.#index_field_name, val_old, link_old); + TableIndex::insert(&self.#index_field_name, val_new.clone(), link_new); + TableIndex::remove(&self.#index_field_name, &val_old, link_old); } }; let insert = if idx.is_unique { @@ -212,14 +212,8 @@ impl Generator { row.#i } }; - if idx.is_unique { - quote! { - self.#index_field_name.remove(&#row); - } - } else { - quote! { - self.#index_field_name.remove(&#row, &link); - } + quote! { + TableIndex::remove(&self.#index_field_name, &#row, link); } }) .collect::>(); @@ -259,7 +253,7 @@ impl Generator { if let Some(diff) = difference.get(#diff_key) { if let #avt_type_ident::#variant_ident(old) = &diff.old { let key_old = #old_value_expr; - TableIndex::remove(&self.#index_field_name, key_old, link); + TableIndex::remove(&self.#index_field_name, &key_old, link); } } } @@ -372,19 +366,10 @@ impl Generator { row.#i } }; - let delete = if idx.is_unique { - quote! { - self.#index_field_name.remove(&#row); - } - } else { - quote! { - self.#index_field_name.remove(&#row, &link); - } - }; quote! { #avt_index_ident::#index_variant => { - #delete + TableIndex::remove(&self.#index_field_name, &#row, link); }, } }) diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index 5b9b13d..38b1d21 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -102,7 +102,7 @@ impl Generator { let link = match self.0 .pk_map .get(&pk) - .map(|v| v.get().value) + .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound) { Ok(l) => l, Err(e) => { @@ -119,7 +119,7 @@ impl Generator { let link = self.0 .pk_map .get(&pk) - .map(|v| v.get().value) + .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; let row = self.select(pk.clone()).unwrap(); #process @@ -197,7 +197,7 @@ impl Generator { pub async fn #name(&self, by: #type_) -> core::result::Result<(), WorkTableError> { let rows_to_update = self.0.indexes.#index.get(#by).map(|kv| kv.1).collect::>(); for link in rows_to_update { - let row = self.0.data.select_non_ghosted(*link).map_err(WorkTableError::PagesError)?; + let row = self.0.data.select_non_ghosted((*link).into()).map_err(WorkTableError::PagesError)?; self.delete(row.get_primary_key()).await?; } core::result::Result::Ok(()) @@ -217,7 +217,7 @@ impl Generator { }; quote! { pub async fn #name(&self, by: #type_) -> core::result::Result<(), WorkTableError> { - let row_to_update = self.0.indexes.#index.get(#by).map(|v| v.get().value); + let row_to_update = self.0.indexes.#index.get(#by).map(|v| v.get().value.into()); if let Some(link) = row_to_update { let row = self.0.data.select_non_ghosted(link).map_err(WorkTableError::PagesError)?; self.delete(row.get_primary_key()).await?; diff --git a/codegen/src/worktable/generator/queries/in_place.rs b/codegen/src/worktable/generator/queries/in_place.rs index 922eb37..5338f7e 100644 --- a/codegen/src/worktable/generator/queries/in_place.rs +++ b/codegen/src/worktable/generator/queries/in_place.rs @@ -116,7 +116,7 @@ impl Generator { .0 .pk_map .get(&pk) - .map(|v| v.get().value) + .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; unsafe { self.0 diff --git a/codegen/src/worktable/generator/queries/select.rs b/codegen/src/worktable/generator/queries/select.rs index 1676746..ebb9257 100644 --- a/codegen/src/worktable/generator/queries/select.rs +++ b/codegen/src/worktable/generator/queries/select.rs @@ -31,7 +31,7 @@ impl Generator { { let iter = self.0.pk_map .iter() - .filter_map(|(_, link)| self.0.data.select_non_ghosted(*link).ok()); + .filter_map(|(_, link)| self.0.data.select_non_ghosted(link.0).ok()); SelectQueryBuilder::new(iter) } diff --git a/codegen/src/worktable/generator/queries/update.rs b/codegen/src/worktable/generator/queries/update.rs index dae5fb4..e7ae775 100644 --- a/codegen/src/worktable/generator/queries/update.rs +++ b/codegen/src/worktable/generator/queries/update.rs @@ -95,7 +95,7 @@ impl Generator { let link = match self.0 .pk_map .get(&pk) - .map(|v| v.get().value) + .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound) { Ok(l) => l, Err(e) => { @@ -480,7 +480,7 @@ impl Generator { let link = match self.0 .pk_map .get(&pk) - .map(|v| v.get().value) + .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound) { Ok(l) => l, Err(e) => { @@ -710,7 +710,7 @@ impl Generator { let link = self.0.indexes.#index .get(#by) - .map(|kv| kv.get().value) + .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; let pk = self.0.data.select_non_ghosted(link)?.get_primary_key().clone(); @@ -720,7 +720,7 @@ impl Generator { let link = match self.0.indexes.#index .get(#by) - .map(|kv| kv.get().value) + .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound) { Ok(l) => l, Err(e) => { diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index 34b5974..3b82513 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -238,7 +238,7 @@ impl Generator { fn gen_table_iter_inner(&self, func: TokenStream) -> TokenStream { quote! { - let first = self.0.pk_map.iter().next().map(|(k, v)| (k.clone(), *v)); + let first = self.0.pk_map.iter().next().map(|(k, v)| (k.clone(), v.0)); let Some((mut k, link)) = first else { return Ok(()) }; @@ -250,11 +250,11 @@ impl Generator { while !ind { let next = { let mut iter = self.0.pk_map.range(k.clone()..); - let next = iter.next().map(|(k, v)| (k.clone(), *v)).filter(|(key, _)| key != &k); + let next = iter.next().map(|(k, v)| (k.clone(), v.0)).filter(|(key, _)| key != &k); if next.is_some() { next } else { - iter.next().map(|(k, v)| (k.clone(), *v)) + iter.next().map(|(k, v)| (k.clone(), v.0)) } }; if let Some((key, link)) = next { diff --git a/codegen/src/worktable/generator/table/index_fns.rs b/codegen/src/worktable/generator/table/index_fns.rs index 8471879..98782d5 100644 --- a/codegen/src/worktable/generator/table/index_fns.rs +++ b/codegen/src/worktable/generator/table/index_fns.rs @@ -65,7 +65,7 @@ impl Generator { Ok(quote! { pub fn #fn_name(&self, by: #type_) -> Option<#row_ident> { - let link = self.0.indexes.#field_ident.get(#by).map(|kv| kv.get().value)?; + let link = self.0.indexes.#field_ident.get(#by).map(|kv| kv.get().value.into())?; self.0.data.select_non_ghosted(link).ok() } }) @@ -104,7 +104,7 @@ impl Generator { let rows = self.0.indexes.#field_ident .get(#by) .into_iter() - .filter_map(|(_, link)| self.0.data.select_non_ghosted(*link).ok()) + .filter_map(|(_, link)| self.0.data.select_non_ghosted((*link).into()).ok()) .filter(move |r| &r.#row_field_ident == &by); SelectQueryBuilder::new(rows) diff --git a/codegen/src/worktable/generator/table/mod.rs b/codegen/src/worktable/generator/table/mod.rs index fb09607..7f380f4 100644 --- a/codegen/src/worktable/generator/table/mod.rs +++ b/codegen/src/worktable/generator/table/mod.rs @@ -101,11 +101,11 @@ impl Generator { }; let node_type = if pk_types_unsized { quote! { - UnsizedNode> + UnsizedNode> } } else { quote! { - Vec> + Vec> } }; diff --git a/src/index/mod.rs b/src/index/mod.rs index a164ecb..745614d 100644 --- a/src/index/mod.rs +++ b/src/index/mod.rs @@ -8,7 +8,7 @@ pub use available_index::AvailableIndex; pub use indexset::concurrent::map::BTreeMap as IndexMap; pub use indexset::concurrent::multimap::BTreeMultiMap as IndexMultiMap; pub use multipair::MultiPairRecreate; -pub use table_index::{TableIndex, TableIndexCdc}; +pub use table_index::{TableIndex, TableIndexCdc, convert_change_events}; pub use table_secondary_index::{ IndexError, TableSecondaryIndex, TableSecondaryIndexCdc, TableSecondaryIndexEventsOps, TableSecondaryIndexInfo, diff --git a/src/index/table_index/cdc.rs b/src/index/table_index/cdc.rs index eef930a..be7241f 100644 --- a/src/index/table_index/cdc.rs +++ b/src/index/table_index/cdc.rs @@ -7,6 +7,8 @@ use indexset::core::multipair::MultiPair; use indexset::core::node::NodeLike; use indexset::core::pair::Pair; +use crate::index::table_index::util::convert_change_events; +use crate::util::OffsetEqLink; use crate::{IndexMap, IndexMultiMap}; pub trait TableIndexCdc { @@ -20,23 +22,25 @@ pub trait TableIndexCdc { ) -> (Option<(T, Link)>, Vec>>); } -impl TableIndexCdc for IndexMultiMap +impl TableIndexCdc for IndexMultiMap, Node> where T: Debug + Eq + Hash + Clone + Send + Ord, - Node: NodeLike> + Send + 'static, + Node: NodeLike>> + Send + 'static, { fn insert_cdc(&self, value: T, link: Link) -> (Option, Vec>>) { - let (res, evs) = self.insert_cdc(value, link); - (res, evs.into_iter().map(Into::into).collect()) + let (res, evs) = self.insert_cdc(value, OffsetEqLink(link)); + let pair_evs = evs.into_iter().map(Into::into).collect(); + let res_link = res.map(|l| l.0); + (res_link, convert_change_events(pair_evs)) } - // TODO: refactor this to be more straightforward fn insert_checked_cdc(&self, value: T, link: Link) -> Option>>> { - let (res, evs) = self.insert_cdc(value, link); + let (res, evs) = self.insert_cdc(value, OffsetEqLink(link)); + let pair_evs = evs.into_iter().map(Into::into).collect(); if res.is_some() { None } else { - Some(evs.into_iter().map(Into::into).collect()) + Some(convert_change_events(pair_evs)) } } @@ -45,22 +49,27 @@ where value: T, link: Link, ) -> (Option<(T, Link)>, Vec>>) { - let (res, evs) = self.remove_cdc(&value, &link); - (res, evs.into_iter().map(Into::into).collect()) + let (res, evs) = self.remove_cdc(&value, &OffsetEqLink(link)); + let pair_evs = evs.into_iter().map(Into::into).collect(); + let res_pair = res.map(|(k, v)| (k, v.into())); + (res_pair, convert_change_events(pair_evs)) } } -impl TableIndexCdc for IndexMap +impl TableIndexCdc for IndexMap, Node> where T: Debug + Eq + Hash + Clone + Send + Ord, - Node: NodeLike> + Send + 'static, + Node: NodeLike>> + Send + 'static, { fn insert_cdc(&self, value: T, link: Link) -> (Option, Vec>>) { - self.insert_cdc(value, link) + let (res, evs) = self.insert_cdc(value, OffsetEqLink(link)); + let res_link = res.map(|l| l.0); + (res_link, convert_change_events(evs)) } fn insert_checked_cdc(&self, value: T, link: Link) -> Option>>> { - self.checked_insert_cdc(value, link) + let res = self.checked_insert_cdc(value, OffsetEqLink(link)); + res.map(|evs| convert_change_events(evs)) } fn remove_cdc( @@ -68,6 +77,8 @@ where value: T, _: Link, ) -> (Option<(T, Link)>, Vec>>) { - self.remove_cdc(&value) + let (res, evs) = self.remove_cdc(&value); + let res_pair = res.map(|(k, v)| (k, v.0)); + (res_pair, convert_change_events(evs)) } } diff --git a/src/index/table_index/mod.rs b/src/index/table_index/mod.rs index 6272cbb..88cf043 100644 --- a/src/index/table_index/mod.rs +++ b/src/index/table_index/mod.rs @@ -6,54 +6,58 @@ use indexset::core::multipair::MultiPair; use indexset::core::node::NodeLike; use indexset::core::pair::Pair; +use crate::util::OffsetEqLink; use crate::{IndexMap, IndexMultiMap}; mod cdc; +pub mod util; pub use cdc::TableIndexCdc; +pub use util::convert_change_events; pub trait TableIndex { fn insert(&self, value: T, link: Link) -> Option; fn insert_checked(&self, value: T, link: Link) -> Option<()>; - fn remove(&self, value: T, link: Link) -> Option<(T, Link)>; + fn remove(&self, value: &T, link: Link) -> Option<(T, Link)>; } -impl TableIndex for IndexMultiMap +impl TableIndex for IndexMultiMap where T: Debug + Eq + Hash + Clone + Send + Ord, - Node: NodeLike> + Send + 'static, + Node: NodeLike> + Send + 'static, { fn insert(&self, value: T, link: Link) -> Option { - self.insert(value, link) + self.insert(value, OffsetEqLink(link)).map(|l| l.0) } fn insert_checked(&self, value: T, link: Link) -> Option<()> { - if self.insert(value, link).is_some() { + if self.insert(value, OffsetEqLink(link)).is_some() { None } else { Some(()) } } - fn remove(&self, value: T, link: Link) -> Option<(T, Link)> { - self.remove(&value, &link) + fn remove(&self, value: &T, link: Link) -> Option<(T, Link)> { + self.remove(value, &OffsetEqLink(link)) + .map(|(v, l)| (v, l.0)) } } -impl TableIndex for IndexMap +impl TableIndex for IndexMap where T: Debug + Eq + Hash + Clone + Send + Ord, - Node: NodeLike> + Send + 'static, + Node: NodeLike> + Send + 'static, { fn insert(&self, value: T, link: Link) -> Option { - self.insert(value, link) + self.insert(value, OffsetEqLink(link)).map(|l| l.0) } fn insert_checked(&self, value: T, link: Link) -> Option<()> { - self.checked_insert(value, link) + self.checked_insert(value, OffsetEqLink(link)) } - fn remove(&self, value: T, _: Link) -> Option<(T, Link)> { - self.remove(&value) + fn remove(&self, value: &T, _: Link) -> Option<(T, Link)> { + self.remove(value).map(|(v, l)| (v, l.0)) } } diff --git a/src/index/table_index/util.rs b/src/index/table_index/util.rs new file mode 100644 index 0000000..5f308a1 --- /dev/null +++ b/src/index/table_index/util.rs @@ -0,0 +1,85 @@ +use indexset::cdc::change::ChangeEvent; +use indexset::core::pair::Pair; + +pub fn convert_change_event(ev: ChangeEvent>) -> ChangeEvent> +where + L1: Into, +{ + match ev { + ChangeEvent::InsertAt { + event_id, + max_value, + value, + index, + } => ChangeEvent::InsertAt { + event_id, + max_value: Pair { + key: max_value.key, + value: max_value.value.into(), + }, + value: Pair { + key: value.key, + value: value.value.into(), + }, + index, + }, + ChangeEvent::RemoveAt { + event_id, + max_value, + value, + index, + } => ChangeEvent::RemoveAt { + event_id, + max_value: Pair { + key: max_value.key, + value: max_value.value.into(), + }, + value: Pair { + key: value.key, + value: value.value.into(), + }, + index, + }, + ChangeEvent::CreateNode { + event_id, + max_value, + } => ChangeEvent::CreateNode { + event_id, + max_value: Pair { + key: max_value.key, + value: max_value.value.into(), + }, + }, + ChangeEvent::RemoveNode { + event_id, + max_value, + } => ChangeEvent::RemoveNode { + event_id, + max_value: Pair { + key: max_value.key, + value: max_value.value.into(), + }, + }, + ChangeEvent::SplitNode { + event_id, + max_value, + split_index, + } => ChangeEvent::SplitNode { + event_id, + max_value: Pair { + key: max_value.key, + value: max_value.value.into(), + }, + split_index, + }, + } +} + +pub fn convert_change_events( + evs: Vec>>, +) -> Vec>> +where + L1: Into, +{ + evs.into_iter().map(convert_change_event).collect() +} diff --git a/src/lib.rs b/src/lib.rs index d6699cb..ff84ad2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,7 +35,7 @@ pub mod prelude { pub use crate::primary_key::{PrimaryKeyGenerator, PrimaryKeyGeneratorState, TablePrimaryKey}; pub use crate::table::select::{Order, QueryParams, SelectQueryBuilder, SelectQueryExecutor}; pub use crate::table::system_info::{IndexInfo, IndexKind, SystemInfo}; - pub use crate::util::{OrderedF32Def, OrderedF64Def}; + pub use crate::util::{OffsetEqLink, OrderedF32Def, OrderedF64Def}; pub use crate::{ AvailableIndex, Difference, IndexError, IndexMap, IndexMultiMap, MultiPairRecreate, TableIndex, TableIndexCdc, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc, @@ -62,7 +62,3 @@ pub mod prelude { pub const WT_INDEX_EXTENSION: &str = ".wt.idx"; pub const WT_DATA_EXTENSION: &str = ".wt.data"; } - -// TODO: -// 1. add checked inserts to indexset to not insert/remove but just insert with violation error -// 2. Add pre-update state storage to avoid ghost reads of updated data if it will be rolled back diff --git a/src/mem_stat/mod.rs b/src/mem_stat/mod.rs index 6a4c757..d6d1bba 100644 --- a/src/mem_stat/mod.rs +++ b/src/mem_stat/mod.rs @@ -16,6 +16,7 @@ use uuid::Uuid; use crate::IndexMultiMap; use crate::persistence::OperationType; use crate::prelude::OperationId; +use crate::util::OffsetEqLink; use crate::{IndexMap, impl_memstat_zero}; pub trait MemStat { @@ -179,3 +180,14 @@ where } impl_memstat_zero!(Link, PageId, Uuid, OperationId, OperationType); + +// OffsetEqLink has zero heap size (just wraps Link) +impl MemStat for OffsetEqLink { + fn heap_size(&self) -> usize { + 0 + } + + fn used_size(&self) -> usize { + 0 + } +} diff --git a/src/table/mod.rs b/src/table/mod.rs index 766cd03..d077817 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -10,11 +10,12 @@ use crate::lock::LockMap; use crate::persistence::{InsertOperation, Operation}; use crate::prelude::{OperationId, PrimaryKeyGeneratorState}; use crate::primary_key::{PrimaryKeyGenerator, TablePrimaryKey}; +use crate::util::OffsetEqLink; use crate::{ AvailableIndex, IndexError, IndexMap, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc, - in_memory, + convert_change_events, in_memory, }; -use data_bucket::{INNER_PAGE_SIZE, Link}; +use data_bucket::INNER_PAGE_SIZE; use derive_more::{Display, Error, From}; use indexset::core::node::NodeLike; use indexset::core::pair::Pair; @@ -38,16 +39,16 @@ pub struct WorkTable< SecondaryIndexes = (), LockType = (), PkGen = ::Generator, - PkNodeType = Vec>, + PkNodeType = Vec>, const DATA_LENGTH: usize = INNER_PAGE_SIZE, > where PrimaryKey: Clone + Ord + Send + 'static + std::hash::Hash, Row: StorableRow + Send + Clone + 'static, - PkNodeType: NodeLike> + Send + 'static, + PkNodeType: NodeLike> + Send + 'static, { pub data: DataPages, - pub pk_map: IndexMap, + pub pk_map: IndexMap, pub indexes: SecondaryIndexes, @@ -89,7 +90,7 @@ where PrimaryKey: Debug + Clone + Ord + Send + TablePrimaryKey + std::hash::Hash, SecondaryIndexes: Default, PkGen: Default, - PkNodeType: NodeLike> + Send + 'static, + PkNodeType: NodeLike> + Send + 'static, Row: StorableRow + Send + Clone + 'static, ::WrappedRow: RowWrapper, { @@ -132,7 +133,7 @@ impl< where Row: TableRow, PrimaryKey: Debug + Clone + Ord + Send + TablePrimaryKey + std::hash::Hash, - PkNodeType: NodeLike> + Send + 'static, + PkNodeType: NodeLike> + Send + 'static, Row: StorableRow + Send + Clone + 'static, ::WrappedRow: RowWrapper, { @@ -158,7 +159,7 @@ where <::WrappedRow as Archive>::Archived: Deserialize<::WrappedRow, HighDeserializer>, { - let link = self.pk_map.get(&pk).map(|v| v.get().value); + let link = self.pk_map.get(&pk).map(|v| v.get().value.into()); if let Some(link) = link { self.data.select(link).ok() } else { @@ -193,7 +194,11 @@ where .data .insert(row.clone()) .map_err(WorkTableError::PagesError)?; - if self.pk_map.checked_insert(pk.clone(), link).is_none() { + if self + .pk_map + .checked_insert(pk.clone(), OffsetEqLink(link)) + .is_none() + { self.data.delete(link).map_err(WorkTableError::PagesError)?; return Err(WorkTableError::AlreadyExists("Primary".to_string())); }; @@ -255,11 +260,14 @@ where .data .insert_cdc(row.clone()) .map_err(WorkTableError::PagesError)?; - let primary_key_events = self.pk_map.checked_insert_cdc(pk.clone(), link); - if primary_key_events.is_none() { + let primary_key_events = self + .pk_map + .checked_insert_cdc(pk.clone(), OffsetEqLink(link)); + let Some(primary_key_events) = primary_key_events else { self.data.delete(link).map_err(WorkTableError::PagesError)?; return Err(WorkTableError::AlreadyExists("Primary".to_string())); - } + }; + let primary_key_events = convert_change_events(primary_key_events); let indexes_res = self.indexes.save_row_cdc(row.clone(), link); if let Err(e) = indexes_res { return match e { @@ -290,7 +298,7 @@ where let op = Operation::Insert(InsertOperation { id: OperationId::Single(Uuid::now_v7()), pk_gen_state: self.pk_gen.get_state(), - primary_key_events: primary_key_events.expect("should be checked before for existence"), + primary_key_events, secondary_keys_events: indexes_res.expect("was checked before"), bytes, link, @@ -307,6 +315,8 @@ where /// part is for new row. Goal is to make `PrimaryKey` of the row always /// acceptable. As for reinsert `PrimaryKey` will be same for both old and /// new [`Link`]'s, goal will be achieved. + /// + /// [`Link`]: data_bucket::Link pub fn reinsert(&self, row_old: Row, row_new: Row) -> Result where Row: Archive @@ -332,7 +342,7 @@ where let old_link = self .pk_map .get(&pk) - .map(|v| v.get().value) + .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; let new_link = self .data @@ -343,7 +353,7 @@ where .with_mut_ref(new_link, |r| r.unghost()) .map_err(WorkTableError::PagesError)? } - self.pk_map.insert(pk.clone(), new_link); + self.pk_map.insert(pk.clone(), OffsetEqLink(new_link)); let indexes_res = self .indexes @@ -354,7 +364,7 @@ where at, inserted_already, } => { - self.pk_map.insert(pk.clone(), old_link); + self.pk_map.insert(pk.clone(), OffsetEqLink(old_link)); self.indexes .delete_from_indexes(row_new, new_link, inserted_already)?; self.data @@ -408,7 +418,7 @@ where let old_link = self .pk_map .get(&pk) - .map(|v| v.get().value) + .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; let (new_link, _) = self .data @@ -419,7 +429,8 @@ where .with_mut_ref(new_link, |r| r.unghost()) .map_err(WorkTableError::PagesError)? } - let (_, primary_key_events) = self.pk_map.insert_cdc(pk.clone(), new_link); + let (_, primary_key_events) = self.pk_map.insert_cdc(pk.clone(), OffsetEqLink(new_link)); + let primary_key_events = convert_change_events(primary_key_events); let indexes_res = self.indexes .reinsert_row_cdc(row_old, old_link, row_new.clone(), new_link); @@ -429,7 +440,7 @@ where at, inserted_already, } => { - self.pk_map.insert(pk.clone(), old_link); + self.pk_map.insert(pk.clone(), OffsetEqLink(old_link)); self.indexes .delete_from_indexes(row_new, new_link, inserted_already)?; self.data diff --git a/src/table/system_info.rs b/src/table/system_info.rs index 6575195..b0e4aca 100644 --- a/src/table/system_info.rs +++ b/src/table/system_info.rs @@ -1,4 +1,3 @@ -use data_bucket::Link; use indexset::core::node::NodeLike; use indexset::core::pair::Pair; use prettytable::{Table, format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR, row}; @@ -6,6 +5,7 @@ use std::fmt::{self, Debug, Display, Formatter}; use crate::in_memory::{RowWrapper, StorableRow}; use crate::mem_stat::MemStat; +use crate::util::OffsetEqLink; use crate::{TableSecondaryIndexInfo, WorkTable}; #[derive(Debug)] @@ -71,7 +71,7 @@ where PrimaryKey: Debug + Clone + Ord + Send + 'static + std::hash::Hash, Row: StorableRow + Send + Clone + 'static, ::WrappedRow: RowWrapper, - NodeType: NodeLike> + Send + 'static, + NodeType: NodeLike> + Send + 'static, SecondaryIndexes: MemStat + TableSecondaryIndexInfo, { pub fn system_info(&self) -> SystemInfo { diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index cd2ae5f..14e2c04 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -17,7 +17,6 @@ use rkyv::util::AlignedVec; use rkyv::{Archive, Serialize}; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; -use crate::lock::{FullRowLock, LockMap}; use crate::prelude::TablePrimaryKey; use crate::vacuum::fragmentation_info::PageFragmentationInfo; use crate::vacuum::lock::VacuumLock; diff --git a/src/util/mod.rs b/src/util/mod.rs index 8260445..cfb87fa 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -2,5 +2,6 @@ mod offset_eq_link; mod optimized_vec; mod ordered_float; +pub use offset_eq_link::OffsetEqLink; pub use optimized_vec::OptimizedVec; pub use ordered_float::{OrderedF32Def, OrderedF64Def}; diff --git a/src/util/offset_eq_link.rs b/src/util/offset_eq_link.rs index 06fbfff..51dd224 100644 --- a/src/util/offset_eq_link.rs +++ b/src/util/offset_eq_link.rs @@ -4,13 +4,14 @@ //! based on its absolute index within the data pages, rather than on the //! raw `Link` fields. -use data_bucket::Link; +use data_bucket::{Link, SizeMeasurable}; +use derive_more::From; use crate::in_memory::DATA_INNER_LENGTH; use crate::prelude::Into; /// A link wrapper that implements `Eq` based on absolute index. -#[derive(Copy, Clone, Debug, Into)] +#[derive(Copy, Clone, Debug, Default, Into, From)] pub struct OffsetEqLink(pub Link); impl OffsetEqLink { @@ -47,6 +48,36 @@ impl PartialEq for OffsetEqLink { impl Eq for OffsetEqLink {} +impl std::ops::Deref for OffsetEqLink { + type Target = Link; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for OffsetEqLink { + fn as_ref(&self) -> &Link { + &self.0 + } +} + +impl PartialEq for OffsetEqLink { + fn eq(&self, other: &Link) -> bool { + self.0.eq(other) + } +} + +impl SizeMeasurable for OffsetEqLink { + fn aligned_size(&self) -> usize { + self.0.aligned_size() + } + + fn align() -> Option { + Link::align() + } +} + #[cfg(test)] mod tests { use super::*; From 7c5be2858b14e69487a9dba58cae037a04f12159 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:11:07 +0300 Subject: [PATCH 05/32] finalize link rework --- codegen/src/persist_index/generator.rs | 45 ++++++++++++++++--- codegen/src/worktable/generator/index/cdc.rs | 4 +- .../src/worktable/generator/queries/delete.rs | 2 +- .../src/worktable/generator/queries/update.rs | 4 +- .../worktable/generator/table/index_fns.rs | 2 +- src/in_memory/empty_link_registry.rs | 3 +- src/index/multipair.rs | 9 ++-- src/util/offset_eq_link.rs | 2 +- tests/persistence/read.rs | 2 +- tests/worktable/base.rs | 2 +- 10 files changed, 53 insertions(+), 22 deletions(-) diff --git a/codegen/src/persist_index/generator.rs b/codegen/src/persist_index/generator.rs index 940cd43..6e928d6 100644 --- a/codegen/src/persist_index/generator.rs +++ b/codegen/src/persist_index/generator.rs @@ -364,12 +364,29 @@ impl Generator { if is_unsized(&ty.to_string()) { let node = if is_unique { quote! { - let node = UnsizedNode::from_inner(page.inner.get_node(), #const_name); + let node = page + .inner + .get_node() + .into_iter() + .map(|p| IndexPair { + key: p.key, + value: p.value.into(), + }) + .collect(); + let node = UnsizedNode::from_inner(node, #const_name); #i.attach_node(node); } } else { quote! { - let inner = page.inner.get_node(); + let inner: Vec<_> = page + .inner + .get_node() + .into_iter() + .map(|p| IndexPair { + key: p.key, + value: OffsetEqLink(p.value), + }) + .collect(); let mut last_key = inner.first().expect("Node should be not empty").key.clone(); let mut discriminator = 0; let mut inner = inner.into_iter().map(move |p| { @@ -390,7 +407,7 @@ impl Generator { } }; quote! { - let #i: #t<_, Link, UnsizedNode<_>> = #t::with_maximum_node_size(#const_name); + let #i: #t<_, OffsetEqLink, UnsizedNode<_>> = #t::with_maximum_node_size(#const_name); for page in persisted.#i.1 { #node } @@ -398,12 +415,28 @@ impl Generator { } else { let node = if is_unique { quote! { - let node = page.inner.get_node(); + let node = page + .inner + .get_node() + .into_iter() + .map(|p| IndexPair { + key: p.key, + value: p.value.into(), + }) + .collect(); #i.attach_node(node); } } else { quote! { - let inner = page.inner.get_node(); + let inner: Vec<_> = page + .inner + .get_node() + .into_iter() + .map(|p| IndexPair { + key: p.key, + value: OffsetEqLink(p.value), + }) + .collect(); let mut last_key = inner.first().expect("Node should be not empty").key.clone(); let mut discriminator = 0; let mut inner = inner.into_iter().map(move |p| { @@ -424,7 +457,7 @@ impl Generator { }; quote! { let size = get_index_page_size_from_data_length::<#ty>(#const_name); - let #i: #t<_, Link> = #t::with_maximum_node_size(size); + let #i: #t<_, OffsetEqLink> = #t::with_maximum_node_size(size); for page in persisted.#i.1 { #node } diff --git a/codegen/src/worktable/generator/index/cdc.rs b/codegen/src/worktable/generator/index/cdc.rs index 9a10a0d..20edfff 100644 --- a/codegen/src/worktable/generator/index/cdc.rs +++ b/codegen/src/worktable/generator/index/cdc.rs @@ -104,7 +104,7 @@ impl Generator { let remove = if idx.is_unique { quote! { if row_new.#i == row_old.#i { - let events = self.#index_field_name.insert_cdc(row_new.#i.clone(), link_new).1; + let events = TableIndexCdc::insert_cdc(&self.#index_field_name, row_new.#i.clone(), link_new).1; #index_field_name.extend(events.into_iter().map(|ev| ev.into()).collect::>()); } else { let (_, events) = TableIndexCdc::remove_cdc(&self.#index_field_name, row_old.#i.clone(), link_old); @@ -113,7 +113,7 @@ impl Generator { } } else { quote! { - let events = self.#index_field_name.insert_cdc(row_new.#i.clone(), link_new).1; + let events = TableIndexCdc::insert_cdc(&self.#index_field_name, row_new.#i.clone(), link_new).1; #index_field_name.extend(events.into_iter().map(|ev| ev.into()).collect::>()); let (_, events) = TableIndexCdc::remove_cdc(&self.#index_field_name, row_old.#i.clone(), link_old); #index_field_name.extend(events.into_iter().map(|ev| ev.into()).collect::>()); diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index 38b1d21..d567d53 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -197,7 +197,7 @@ impl Generator { pub async fn #name(&self, by: #type_) -> core::result::Result<(), WorkTableError> { let rows_to_update = self.0.indexes.#index.get(#by).map(|kv| kv.1).collect::>(); for link in rows_to_update { - let row = self.0.data.select_non_ghosted((*link).into()).map_err(WorkTableError::PagesError)?; + let row = self.0.data.select_non_ghosted(link.0).map_err(WorkTableError::PagesError)?; self.delete(row.get_primary_key()).await?; } core::result::Result::Ok(()) diff --git a/codegen/src/worktable/generator/queries/update.rs b/codegen/src/worktable/generator/queries/update.rs index e7ae775..6cbca62 100644 --- a/codegen/src/worktable/generator/queries/update.rs +++ b/codegen/src/worktable/generator/queries/update.rs @@ -609,7 +609,7 @@ impl Generator { quote! { pub async fn #method_ident(&self, row: #query_ident, by: #by_ident) -> core::result::Result<(), WorkTableError> { - let links: Vec<_> = self.0.indexes.#index.get(#by).map(|(_, l)| *l).collect(); + let links: Vec<_> = self.0.indexes.#index.get(#by).map(|(_, l)| l.0).collect(); let mut locks = std::collections::HashMap::new(); for link in links.iter() { @@ -620,7 +620,7 @@ impl Generator { locks.insert(pk, op_lock); } - let links: Vec<_> = self.0.indexes.#index.get(#by).map(|(_, l)| *l).collect(); + let links: Vec<_> = self.0.indexes.#index.get(#by).map(|(_, l)| l.0).collect(); let mut pk_to_unlock: std::collections::HashMap<_, std::sync::Arc> = std::collections::HashMap::new(); let op_id = OperationId::Multi(uuid::Uuid::now_v7()); for link in links.into_iter() { diff --git a/codegen/src/worktable/generator/table/index_fns.rs b/codegen/src/worktable/generator/table/index_fns.rs index 98782d5..52b88f3 100644 --- a/codegen/src/worktable/generator/table/index_fns.rs +++ b/codegen/src/worktable/generator/table/index_fns.rs @@ -104,7 +104,7 @@ impl Generator { let rows = self.0.indexes.#field_ident .get(#by) .into_iter() - .filter_map(|(_, link)| self.0.data.select_non_ghosted((*link).into()).ok()) + .filter_map(|(_, link)| self.0.data.select_non_ghosted(link.0).ok()) .filter(move |r| &r.#row_field_ident == &by); SelectQueryBuilder::new(rows) diff --git a/src/in_memory/empty_link_registry.rs b/src/in_memory/empty_link_registry.rs index b8c6d32..c8cd0bc 100644 --- a/src/in_memory/empty_link_registry.rs +++ b/src/in_memory/empty_link_registry.rs @@ -368,8 +368,7 @@ mod tests { registry.push(large); registry.push(medium); - assert_eq!(registry.pop_max().unwrap().length, 200); - assert_eq!(registry.pop_max().unwrap().length, 100); + assert_eq!(registry.pop_max().unwrap().length, 300); // two links were united assert_eq!(registry.pop_max().unwrap().length, 50); } diff --git a/src/index/multipair.rs b/src/index/multipair.rs index 670dd6b..ae2aca2 100644 --- a/src/index/multipair.rs +++ b/src/index/multipair.rs @@ -1,13 +1,12 @@ -use data_bucket::Link; use indexset::core::multipair::MultiPair; use indexset::core::pair::Pair; -pub trait MultiPairRecreate { - fn with_last_discriminator(self, discriminator: u64) -> MultiPair; +pub trait MultiPairRecreate { + fn with_last_discriminator(self, discriminator: u64) -> MultiPair; } -impl MultiPairRecreate for Pair { - fn with_last_discriminator(self, discriminator: u64) -> MultiPair { +impl MultiPairRecreate for Pair { + fn with_last_discriminator(self, discriminator: u64) -> MultiPair { MultiPair { key: self.key, value: self.value, diff --git a/src/util/offset_eq_link.rs b/src/util/offset_eq_link.rs index 51dd224..d93e98a 100644 --- a/src/util/offset_eq_link.rs +++ b/src/util/offset_eq_link.rs @@ -16,7 +16,7 @@ pub struct OffsetEqLink(pub Link); impl OffsetEqLink { /// Calculates the absolute index of the link. - fn absolute_index(&self) -> u64 { + pub fn absolute_index(&self) -> u64 { let page_id: u32 = self.0.page_id.into(); (page_id as u64 * DATA_LENGTH as u64) + self.0.offset as u64 } diff --git a/tests/persistence/read.rs b/tests/persistence/read.rs index 7b65805..2cc4749 100644 --- a/tests/persistence/read.rs +++ b/tests/persistence/read.rs @@ -28,7 +28,7 @@ async fn test_info_parse() { assert_eq!(info.inner.page_count, 1); assert_eq!(info.inner.name, "TestPersist"); assert_eq!(info.inner.pk_gen_state, 0); - assert_eq!(info.inner.empty_links_list, vec![]); + assert_eq!(info.inner.empty_links_list, Vec::::new()); } #[tokio::test] diff --git a/tests/worktable/base.rs b/tests/worktable/base.rs index dacc014..62c18c4 100644 --- a/tests/worktable/base.rs +++ b/tests/worktable/base.rs @@ -386,7 +386,7 @@ async fn delete_and_insert_less() { let pk = table.insert(updated.clone()).unwrap(); let new_link = table.0.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); - assert_ne!(link, new_link) + assert_ne!(link.0, new_link.0) } #[tokio::test] From 723e519c66f371118705d1d43b0fe674b39802f6 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:39:50 +0300 Subject: [PATCH 06/32] add reverse index map --- .../persist_table/generator/space_file/mod.rs | 24 +- .../generator/space_file/worktable_impls.rs | 4 +- .../src/worktable/generator/queries/delete.rs | 8 +- .../worktable/generator/queries/in_place.rs | 2 +- .../src/worktable/generator/queries/select.rs | 2 +- .../src/worktable/generator/queries/update.rs | 4 +- .../src/worktable/generator/table/impls.rs | 10 +- codegen/src/worktable/generator/table/mod.rs | 11 +- src/index/mod.rs | 2 + src/index/primary_index.rs | 468 ++++++++++++++++++ src/lib.rs | 6 +- src/table/mod.rs | 64 +-- src/table/system_info.rs | 8 +- tests/worktable/base.rs | 14 +- tests/worktable/index/insert.rs | 10 +- tests/worktable/unsized_.rs | 22 +- 16 files changed, 573 insertions(+), 86 deletions(-) create mode 100644 src/index/primary_index.rs diff --git a/codegen/src/persist_table/generator/space_file/mod.rs b/codegen/src/persist_table/generator/space_file/mod.rs index 5b30b66..9a36a5d 100644 --- a/codegen/src/persist_table/generator/space_file/mod.rs +++ b/codegen/src/persist_table/generator/space_file/mod.rs @@ -156,10 +156,10 @@ impl Generator { let const_name = name_generator.get_page_inner_size_const_ident(); let pk_type = name_generator.get_primary_key_type_ident(); - let pk_map = if self.attributes.pk_unsized { + let primary_index_init = if self.attributes.pk_unsized { let pk_ident = &self.pk_ident; quote! { - let pk_map = IndexMap::<#pk_ident, OffsetEqLink, UnsizedNode<_>>::with_maximum_node_size(#const_name); + let pk_map = IndexMap::<#pk_ident, OffsetEqLink<#const_name>, UnsizedNode<_>>::with_maximum_node_size(#const_name); for page in self.primary_index.1 { let node = page .inner @@ -172,11 +172,18 @@ impl Generator { .collect(); pk_map.attach_node(UnsizedNode::from_inner(node, #const_name)); } + // Reconstruct reverse_pk_map by iterating over pk_map + let mut reverse_pk_map = IndexMap::, #pk_ident>::new(); + for entry in pk_map.iter() { + let (pk, link) = entry; + reverse_pk_map.insert(*link, pk.clone()); + } + let primary_index = PrimaryIndex { pk_map, reverse_pk_map }; } } else { quote! { let size = get_index_page_size_from_data_length::<#pk_type>(#const_name); - let pk_map = IndexMap::<_, OffsetEqLink>::with_maximum_node_size(size); + let pk_map = IndexMap::<_, OffsetEqLink<#const_name>>::with_maximum_node_size(size); for page in self.primary_index.1 { let node = page .inner @@ -189,6 +196,13 @@ impl Generator { .collect(); pk_map.attach_node(node); } + // Reconstruct reverse_pk_map by iterating over pk_map + let mut reverse_pk_map = IndexMap::, #pk_type>::new(); + for entry in pk_map.iter() { + let (pk, link) = entry; + reverse_pk_map.insert(*link, pk.clone()); + } + let primary_index = PrimaryIndex { pk_map, reverse_pk_map }; } }; @@ -207,11 +221,11 @@ impl Generator { .with_empty_links(self.data_info.inner.empty_links_list); let indexes = #index_ident::from_persisted(self.indexes); - #pk_map + #primary_index_init let table = WorkTable { data, - pk_map, + primary_index, indexes, pk_gen: PrimaryKeyGeneratorState::from_state(self.data_info.inner.pk_gen_state), lock_map: LockMap::default(), diff --git a/codegen/src/persist_table/generator/space_file/worktable_impls.rs b/codegen/src/persist_table/generator/space_file/worktable_impls.rs index bd5efd8..9fe0a1f 100644 --- a/codegen/src/persist_table/generator/space_file/worktable_impls.rs +++ b/codegen/src/persist_table/generator/space_file/worktable_impls.rs @@ -105,7 +105,7 @@ impl Generator { quote! { pub fn get_peristed_primary_key_with_toc(&self) -> (Vec>>, Vec>>) { let mut pages = vec![]; - for node in self.0.pk_map.iter_nodes() { + for node in self.0.primary_index.pk_map.iter_nodes() { let page = UnsizedIndexPage::from_node(node.lock_arc().as_ref()); pages.push(page); } @@ -118,7 +118,7 @@ impl Generator { pub fn get_peristed_primary_key_with_toc(&self) -> (Vec>>, Vec>>) { let size = get_index_page_size_from_data_length::<#pk_type>(#const_name); let mut pages = vec![]; - for node in self.0.pk_map.iter_nodes() { + for node in self.0.primary_index.pk_map.iter_nodes() { let page = IndexPage::from_node(node.lock_arc().as_ref(), size); pages.push(page); } diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index d567d53..a810ea2 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -76,7 +76,7 @@ impl Generator { let process = if self.is_persist { quote! { let secondary_keys_events = self.0.indexes.delete_row_cdc(row, link)?; - let (_, primary_key_events) = TableIndexCdc::remove_cdc(&self.0.pk_map, pk.clone(), link); + let (_, primary_key_events) = TableIndexCdc::remove_cdc(&self.0.primary_index.pk_map, pk.clone(), link); self.0.data.delete(link).map_err(WorkTableError::PagesError)?; let mut op: Operation< <<#pk_ident as TablePrimaryKey>::Generator as PrimaryKeyGeneratorState>::State, @@ -93,14 +93,14 @@ impl Generator { } else { quote! { self.0.indexes.delete_row(row, link)?; - self.0.pk_map.remove(&pk); + self.0.primary_index.pk_map.remove(&pk); self.0.data.delete(link).map_err(WorkTableError::PagesError)?; } }; if is_locked { quote! { let link = match self.0 - .pk_map + .primary_index.pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound) { @@ -117,7 +117,7 @@ impl Generator { } else { quote! { let link = self.0 - .pk_map + .primary_index.pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; diff --git a/codegen/src/worktable/generator/queries/in_place.rs b/codegen/src/worktable/generator/queries/in_place.rs index 5338f7e..6ed1071 100644 --- a/codegen/src/worktable/generator/queries/in_place.rs +++ b/codegen/src/worktable/generator/queries/in_place.rs @@ -114,7 +114,7 @@ impl Generator { }; let link = self .0 - .pk_map + .primary_index.pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; diff --git a/codegen/src/worktable/generator/queries/select.rs b/codegen/src/worktable/generator/queries/select.rs index ebb9257..91873e9 100644 --- a/codegen/src/worktable/generator/queries/select.rs +++ b/codegen/src/worktable/generator/queries/select.rs @@ -29,7 +29,7 @@ impl Generator { #column_range_type, #row_fields_ident> { - let iter = self.0.pk_map + let iter = self.0.primary_index.pk_map .iter() .filter_map(|(_, link)| self.0.data.select_non_ghosted(link.0).ok()); diff --git a/codegen/src/worktable/generator/queries/update.rs b/codegen/src/worktable/generator/queries/update.rs index 6cbca62..2695520 100644 --- a/codegen/src/worktable/generator/queries/update.rs +++ b/codegen/src/worktable/generator/queries/update.rs @@ -93,7 +93,7 @@ impl Generator { }; let link = match self.0 - .pk_map + .primary_index.pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound) { @@ -478,7 +478,7 @@ impl Generator { }; let link = match self.0 - .pk_map + .primary_index.pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound) { diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index 3b82513..25ebe41 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -76,7 +76,7 @@ impl Generator { let mut inner = WorkTable::default(); inner.table_name = #table_name; #index_size - inner.pk_map = IndexMap::with_maximum_node_size(size); + inner.primary_index.pk_map = IndexMap::with_maximum_node_size(size); let table_files_path = format!("{}/{}", config.tables_path, #dir_name); let engine: #engine = PersistenceEngine::from_table_files_path(table_files_path).await?; core::result::Result::Ok(Self( @@ -168,7 +168,7 @@ impl Generator { pub async fn upsert(&self, row: #row_type) -> core::result::Result<(), WorkTableError> { let pk = row.get_primary_key(); let need_to_update = { - if let Some(_) = self.0.pk_map.get(&pk) + if let Some(_) = self.0.primary_index.pk_map.get(&pk) { true } else { @@ -238,7 +238,7 @@ impl Generator { fn gen_table_iter_inner(&self, func: TokenStream) -> TokenStream { quote! { - let first = self.0.pk_map.iter().next().map(|(k, v)| (k.clone(), v.0)); + let first = self.0.primary_index.pk_map.iter().next().map(|(k, v)| (k.clone(), v.0)); let Some((mut k, link)) = first else { return Ok(()) }; @@ -249,7 +249,7 @@ impl Generator { let mut ind = false; while !ind { let next = { - let mut iter = self.0.pk_map.range(k.clone()..); + let mut iter = self.0.primary_index.pk_map.range(k.clone()..); let next = iter.next().map(|(k, v)| (k.clone(), v.0)).filter(|(key, _)| key != &k); if next.is_some() { next @@ -273,7 +273,7 @@ impl Generator { fn gen_table_count_fn(&self) -> TokenStream { quote! { pub fn count(&self) -> usize { - let count = self.0.pk_map.len(); + let count = self.0.primary_index.pk_map.len(); count } } diff --git a/codegen/src/worktable/generator/table/mod.rs b/codegen/src/worktable/generator/table/mod.rs index 7f380f4..6f0562b 100644 --- a/codegen/src/worktable/generator/table/mod.rs +++ b/codegen/src/worktable/generator/table/mod.rs @@ -101,11 +101,11 @@ impl Generator { }; let node_type = if pk_types_unsized { quote! { - UnsizedNode> + UnsizedNode>> } } else { quote! { - Vec> + Vec>> } }; @@ -121,8 +121,8 @@ impl Generator { #index_type, #lock_ident, <#primary_key_type as TablePrimaryKey>::Generator, - #node_type, - #inner_const_name + #inner_const_name, + #node_type > #persist_type_part ); @@ -139,7 +139,8 @@ impl Generator { #index_type, #lock_ident, <#primary_key_type as TablePrimaryKey>::Generator, - #node_type, + { INNER_PAGE_SIZE }, + #node_type > #persist_type_part ); diff --git a/src/index/mod.rs b/src/index/mod.rs index 745614d..caafbf9 100644 --- a/src/index/mod.rs +++ b/src/index/mod.rs @@ -1,5 +1,6 @@ mod available_index; mod multipair; +mod primary_index; mod table_index; mod table_secondary_index; mod unsized_node; @@ -8,6 +9,7 @@ pub use available_index::AvailableIndex; pub use indexset::concurrent::map::BTreeMap as IndexMap; pub use indexset::concurrent::multimap::BTreeMultiMap as IndexMultiMap; pub use multipair::MultiPairRecreate; +pub use primary_index::PrimaryIndex; pub use table_index::{TableIndex, TableIndexCdc, convert_change_events}; pub use table_secondary_index::{ IndexError, TableSecondaryIndex, TableSecondaryIndexCdc, TableSecondaryIndexEventsOps, diff --git a/src/index/primary_index.rs b/src/index/primary_index.rs new file mode 100644 index 0000000..a5f174b --- /dev/null +++ b/src/index/primary_index.rs @@ -0,0 +1,468 @@ +//! Combined storage for primary and reverse indexes. +//! +//! [`PrimaryIndex`] keeps both the primary key index (PK → [`OffsetEqLink`]) +//! and the reverse index ([`OffsetEqLink`] → PK) in sync. + +use std::fmt::Debug; +use std::hash::Hash; + +use data_bucket::Link; +use indexset::cdc::change::ChangeEvent; +use indexset::core::node::NodeLike; +use indexset::core::pair::Pair; + +use crate::util::OffsetEqLink; +use crate::{IndexMap, TableIndex, TableIndexCdc, convert_change_events}; + +/// Combined storage for primary and reverse indexes. +/// +/// Maintains bidirectional mapping between primary keys and their data locations: +/// - **Forward index**: `PrimaryKey` → [`OffsetEqLink`] (primary lookups) +/// - **Reverse index**: [`OffsetEqLink`] → `PrimaryKey` (vacuum, position queries) +#[derive(Debug)] +pub struct PrimaryIndex< + PrimaryKey, + const DATA_LENGTH: usize, + PkNodeType = Vec>>, +> where + PrimaryKey: Clone + Ord + Send + 'static + std::hash::Hash, + PkNodeType: NodeLike>> + Send + 'static, +{ + pub pk_map: IndexMap, PkNodeType>, + pub reverse_pk_map: IndexMap, PrimaryKey>, +} + +impl Default + for PrimaryIndex +where + PrimaryKey: Clone + Ord + Send + 'static + std::hash::Hash, + PkNodeType: NodeLike>> + Send + 'static, +{ + fn default() -> Self { + Self { + pk_map: IndexMap::default(), + reverse_pk_map: IndexMap::default(), + } + } +} + +impl TableIndex + for PrimaryIndex +where + PrimaryKey: Debug + Eq + Hash + Clone + Send + Ord, + PkNodeType: NodeLike>> + Send + 'static, +{ + fn insert(&self, value: PrimaryKey, link: Link) -> Option { + let offset_link = OffsetEqLink(link); + let old = self.pk_map.insert(value.clone(), offset_link); + if let Some(old_link) = old { + // Update reverse index + self.reverse_pk_map.remove(&old_link); + } + self.reverse_pk_map.insert(offset_link, value); + old.map(|l| l.0) + } + + fn insert_checked(&self, value: PrimaryKey, link: Link) -> Option<()> { + let offset_link = OffsetEqLink(link); + self.pk_map.checked_insert(value.clone(), offset_link)?; + self.reverse_pk_map.checked_insert(offset_link, value)?; + Some(()) + } + + fn remove(&self, value: &PrimaryKey, _: Link) -> Option<(PrimaryKey, Link)> { + let (_, old_link) = self.pk_map.remove(value)?; + self.reverse_pk_map.remove(&old_link); + Some((value.clone(), old_link.0)) + } +} + +impl TableIndexCdc + for PrimaryIndex +where + PrimaryKey: Debug + Eq + Hash + Clone + Send + Ord, + PkNodeType: NodeLike>> + Send + 'static, +{ + fn insert_cdc( + &self, + value: PrimaryKey, + link: Link, + ) -> (Option, Vec>>) { + let offset_link = OffsetEqLink(link); + let (res, evs) = self.pk_map.insert_cdc(value.clone(), offset_link); + let res_link = res.map(|l| l.0); + + // Update reverse index based on result + if let Some(res) = res { + // Old value existed, remove it from reverse index + self.reverse_pk_map.remove(&res); + } + self.reverse_pk_map.insert(offset_link, value); + + (res_link, convert_change_events(evs)) + } + + fn insert_checked_cdc( + &self, + value: PrimaryKey, + link: Link, + ) -> Option>>> { + let offset_link = OffsetEqLink(link); + let res = self.pk_map.checked_insert_cdc(value.clone(), offset_link); + + if let Some(evs) = res { + // Insert was successful, update reverse index + self.reverse_pk_map.insert(offset_link, value); + Some(convert_change_events(evs)) + } else { + // Key already existed + None + } + } + + fn remove_cdc( + &self, + value: PrimaryKey, + _: Link, + ) -> ( + Option<(PrimaryKey, Link)>, + Vec>>, + ) { + let (res, evs) = self.pk_map.remove_cdc(&value); + + if let Some((pk, old_link)) = res { + let offset_link = OffsetEqLink(old_link.0); + self.reverse_pk_map.remove(&offset_link); + (Some((pk, old_link.0)), convert_change_events(evs)) + } else { + (None, convert_change_events(evs)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use data_bucket::page::PageId; + + const TEST_DATA_LENGTH: usize = 4096; + + type TestPrimaryIndex = + PrimaryIndex>>>; + + #[test] + fn test_default_creates_empty_indexes() { + let index = TestPrimaryIndex::default(); + assert_eq!(index.pk_map.len(), 0); + assert_eq!(index.reverse_pk_map.len(), 0); + } + + #[test] + fn test_insert_creates_bidirectional_mapping() { + let index = TestPrimaryIndex::default(); + let link = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + + index.insert(42, link); + + assert_eq!(index.pk_map.get(&42).map(|v| v.get().value.0), Some(link)); + assert_eq!( + index + .reverse_pk_map + .get(&OffsetEqLink(link)) + .map(|v| v.get().value), + Some(42) + ); + } + + #[test] + fn test_insert_returns_old_link_on_duplicate() { + let index = TestPrimaryIndex::default(); + let link1 = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + let link2 = Link { + page_id: PageId::from(2), + offset: 200, + length: 50, + }; + + index.insert(42, link1); + let old = index.insert(42, link2); + + assert_eq!(old, Some(link1)); + assert_eq!(index.pk_map.get(&42).map(|v| v.get().value.0), Some(link2)); + assert_eq!( + index + .reverse_pk_map + .get(&OffsetEqLink(link2)) + .map(|v| v.get().value), + Some(42) + ); + assert!(index.reverse_pk_map.get(&OffsetEqLink(link1)).is_none()); + } + + #[test] + fn test_insert_checked_succeeds_on_new_key() { + let index = TestPrimaryIndex::default(); + let link = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + + let result = index.insert_checked(42, link); + assert_eq!(result, Some(())); + + assert_eq!(index.pk_map.get(&42).map(|v| v.get().value.0), Some(link)); + assert_eq!( + index + .reverse_pk_map + .get(&OffsetEqLink(link)) + .map(|v| v.get().value), + Some(42) + ); + } + + #[test] + fn test_insert_checked_fails_on_duplicate() { + let index = TestPrimaryIndex::default(); + let link1 = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + let link2 = Link { + page_id: PageId::from(2), + offset: 200, + length: 50, + }; + + index.insert_checked(42, link1).unwrap(); + let result = index.insert_checked(42, link2); + + assert_eq!(result, None); + assert_eq!(index.pk_map.get(&42).map(|v| v.get().value.0), Some(link1)); + } + + #[test] + fn test_removing_existing_key() { + let index = TestPrimaryIndex::default(); + let link = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + + index.insert(42, link); + let removed = index.remove(&42, link); + + assert_eq!(removed, Some((42, link))); + assert!(index.pk_map.get(&42).is_none()); + assert!(index.reverse_pk_map.get(&OffsetEqLink(link)).is_none()); + } + + #[test] + fn test_removing_nonexistent_key_returns_none() { + let index = TestPrimaryIndex::default(); + let link = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + + let removed = index.remove(&42, link); + assert_eq!(removed, None); + } + + #[test] + fn test_insert_cdc_new_key() { + let index = TestPrimaryIndex::default(); + let link = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + + let (old_link, _events) = index.insert_cdc(42, link); + + assert_eq!(old_link, None); + assert_eq!(index.pk_map.get(&42).map(|v| v.get().value.0), Some(link)); + assert_eq!( + index + .reverse_pk_map + .get(&OffsetEqLink(link)) + .map(|v| v.get().value), + Some(42) + ); + } + + #[test] + fn test_insert_cdc_existing_key() { + let index = TestPrimaryIndex::default(); + let link1 = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + let link2 = Link { + page_id: PageId::from(2), + offset: 200, + length: 50, + }; + + index.insert_cdc(42, link1); + let (old_link, _events) = index.insert_cdc(42, link2); + + assert_eq!(old_link, Some(link1)); + assert_eq!(index.pk_map.get(&42).map(|v| v.get().value.0), Some(link2)); + assert!(index.reverse_pk_map.get(&OffsetEqLink(link1)).is_none()); + assert_eq!( + index + .reverse_pk_map + .get(&OffsetEqLink(link2)) + .map(|v| v.get().value), + Some(42) + ); + } + + #[test] + fn test_insert_checked_cdc_new_key() { + let index = TestPrimaryIndex::default(); + let link = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + + let events = index.insert_checked_cdc(42, link); + + assert!(events.is_some()); + assert_eq!(index.pk_map.get(&42).map(|v| v.get().value.0), Some(link)); + } + + #[test] + fn test_insert_checked_cdc_existing_key() { + let index = TestPrimaryIndex::default(); + let link1 = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + let link2 = Link { + page_id: PageId::from(2), + offset: 200, + length: 50, + }; + + index.insert_checked_cdc(42, link1).unwrap(); + let events = index.insert_checked_cdc(42, link2); + + assert!(events.is_none()); + assert_eq!(index.pk_map.get(&42).map(|v| v.get().value.0), Some(link1)); + } + + #[test] + fn test_remove_cdc_existing_key() { + let index = TestPrimaryIndex::default(); + let link = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + + index.insert_cdc(42, link); + let (removed, _events) = index.remove_cdc(42, link); + + assert_eq!(removed, Some((42, link))); + assert!(index.pk_map.get(&42).is_none()); + assert!(index.reverse_pk_map.get(&OffsetEqLink(link)).is_none()); + } + + #[test] + fn test_remove_cdc_nonexistent_key() { + let index = TestPrimaryIndex::default(); + let link = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + + let (removed, _events) = index.remove_cdc(42, link); + + assert_eq!(removed, None); + } + + #[test] + fn test_multiple_keys_maintain_separate_mappings() { + let index = TestPrimaryIndex::default(); + let link1 = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + let link2 = Link { + page_id: PageId::from(2), + offset: 200, + length: 50, + }; + let link3 = Link { + page_id: PageId::from(3), + offset: 300, + length: 50, + }; + + index.insert(1, link1); + index.insert(2, link2); + index.insert(3, link3); + + assert_eq!(index.pk_map.get(&1).map(|v| v.get().value.0), Some(link1)); + assert_eq!(index.pk_map.get(&2).map(|v| v.get().value.0), Some(link2)); + assert_eq!(index.pk_map.get(&3).map(|v| v.get().value.0), Some(link3)); + + assert_eq!( + index + .reverse_pk_map + .get(&OffsetEqLink(link1)) + .map(|v| v.get().value), + Some(1) + ); + assert_eq!( + index + .reverse_pk_map + .get(&OffsetEqLink(link2)) + .map(|v| v.get().value), + Some(2) + ); + assert_eq!( + index + .reverse_pk_map + .get(&OffsetEqLink(link3)) + .map(|v| v.get().value), + Some(3) + ); + } + + #[test] + fn test_reverse_lookup_by_link() { + let index = TestPrimaryIndex::default(); + let link = Link { + page_id: PageId::from(1), + offset: 100, + length: 50, + }; + + index.insert(42, link); + + let pk = index + .reverse_pk_map + .get(&OffsetEqLink(link)) + .map(|v| v.get().value); + assert_eq!(pk, Some(42)); + } +} diff --git a/src/lib.rs b/src/lib.rs index ff84ad2..fb2b300 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,9 +38,9 @@ pub mod prelude { pub use crate::util::{OffsetEqLink, OrderedF32Def, OrderedF64Def}; pub use crate::{ AvailableIndex, Difference, IndexError, IndexMap, IndexMultiMap, MultiPairRecreate, - TableIndex, TableIndexCdc, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc, - TableSecondaryIndexEventsOps, TableSecondaryIndexInfo, UnsizedNode, WorkTable, - WorkTableError, + PrimaryIndex, TableIndex, TableIndexCdc, TableRow, TableSecondaryIndex, + TableSecondaryIndexCdc, TableSecondaryIndexEventsOps, TableSecondaryIndexInfo, UnsizedNode, + WorkTable, WorkTableError, }; pub use data_bucket::{ DATA_VERSION, DataPage, GENERAL_HEADER_SIZE, GeneralHeader, GeneralPage, INNER_PAGE_SIZE, diff --git a/src/table/mod.rs b/src/table/mod.rs index d077817..9afb3ce 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -12,8 +12,8 @@ use crate::prelude::{OperationId, PrimaryKeyGeneratorState}; use crate::primary_key::{PrimaryKeyGenerator, TablePrimaryKey}; use crate::util::OffsetEqLink; use crate::{ - AvailableIndex, IndexError, IndexMap, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc, - convert_change_events, in_memory, + AvailableIndex, IndexError, IndexMap, PrimaryIndex, TableRow, TableSecondaryIndex, + TableSecondaryIndexCdc, convert_change_events, in_memory, }; use data_bucket::INNER_PAGE_SIZE; use derive_more::{Display, Error, From}; @@ -39,16 +39,16 @@ pub struct WorkTable< SecondaryIndexes = (), LockType = (), PkGen = ::Generator, - PkNodeType = Vec>, const DATA_LENGTH: usize = INNER_PAGE_SIZE, + PkNodeType = Vec>>, > where PrimaryKey: Clone + Ord + Send + 'static + std::hash::Hash, Row: StorableRow + Send + Clone + 'static, - PkNodeType: NodeLike> + Send + 'static, + PkNodeType: NodeLike>> + Send + 'static, { pub data: DataPages, - pub pk_map: IndexMap, + pub primary_index: PrimaryIndex, pub indexes: SecondaryIndexes, @@ -72,32 +72,32 @@ impl< SecondaryIndexes, LockType, PkGen, - PkNodeType, const DATA_LENGTH: usize, + PkNodeType, > Default for WorkTable< - Row, - PrimaryKey, - AvailableTypes, - AvailableIndexes, - SecondaryIndexes, - LockType, - PkGen, - PkNodeType, - DATA_LENGTH, - > + Row, + PrimaryKey, + AvailableTypes, + AvailableIndexes, + SecondaryIndexes, + LockType, + PkGen, + DATA_LENGTH, + PkNodeType, +> where PrimaryKey: Debug + Clone + Ord + Send + TablePrimaryKey + std::hash::Hash, SecondaryIndexes: Default, PkGen: Default, - PkNodeType: NodeLike> + Send + 'static, + PkNodeType: NodeLike>> + Send + 'static, Row: StorableRow + Send + Clone + 'static, ::WrappedRow: RowWrapper, { fn default() -> Self { Self { data: DataPages::new(), - pk_map: IndexMap::default(), + primary_index: PrimaryIndex::default(), indexes: SecondaryIndexes::default(), pk_gen: Default::default(), lock_map: LockMap::default(), @@ -116,8 +116,8 @@ impl< SecondaryIndexes, LockType, PkGen, - PkNodeType, const DATA_LENGTH: usize, + PkNodeType, > WorkTable< Row, @@ -127,13 +127,13 @@ impl< SecondaryIndexes, LockType, PkGen, - PkNodeType, DATA_LENGTH, + PkNodeType, > where Row: TableRow, PrimaryKey: Debug + Clone + Ord + Send + TablePrimaryKey + std::hash::Hash, - PkNodeType: NodeLike> + Send + 'static, + PkNodeType: NodeLike>> + Send + 'static, Row: StorableRow + Send + Clone + 'static, ::WrappedRow: RowWrapper, { @@ -159,7 +159,7 @@ where <::WrappedRow as Archive>::Archived: Deserialize<::WrappedRow, HighDeserializer>, { - let link = self.pk_map.get(&pk).map(|v| v.get().value.into()); + let link = self.primary_index.pk_map.get(&pk).map(|v| v.get().value.into()); if let Some(link) = link { self.data.select(link).ok() } else { @@ -195,7 +195,7 @@ where .insert(row.clone()) .map_err(WorkTableError::PagesError)?; if self - .pk_map + .primary_index.pk_map .checked_insert(pk.clone(), OffsetEqLink(link)) .is_none() { @@ -209,7 +209,7 @@ where inserted_already, } => { self.data.delete(link).map_err(WorkTableError::PagesError)?; - self.pk_map.remove(&pk); + self.primary_index.pk_map.remove(&pk); self.indexes .delete_from_indexes(row, link, inserted_already)?; @@ -261,7 +261,7 @@ where .insert_cdc(row.clone()) .map_err(WorkTableError::PagesError)?; let primary_key_events = self - .pk_map + .primary_index.pk_map .checked_insert_cdc(pk.clone(), OffsetEqLink(link)); let Some(primary_key_events) = primary_key_events else { self.data.delete(link).map_err(WorkTableError::PagesError)?; @@ -276,7 +276,7 @@ where inserted_already, } => { self.data.delete(link).map_err(WorkTableError::PagesError)?; - self.pk_map.remove(&pk); + self.primary_index.pk_map.remove(&pk); self.indexes .delete_from_indexes(row, link, inserted_already)?; @@ -340,7 +340,7 @@ where return Err(WorkTableError::PrimaryUpdateTry); } let old_link = self - .pk_map + .primary_index.pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; @@ -353,7 +353,7 @@ where .with_mut_ref(new_link, |r| r.unghost()) .map_err(WorkTableError::PagesError)? } - self.pk_map.insert(pk.clone(), OffsetEqLink(new_link)); + self.primary_index.pk_map.insert(pk.clone(), OffsetEqLink(new_link)); let indexes_res = self .indexes @@ -364,7 +364,7 @@ where at, inserted_already, } => { - self.pk_map.insert(pk.clone(), OffsetEqLink(old_link)); + self.primary_index.pk_map.insert(pk.clone(), OffsetEqLink(old_link)); self.indexes .delete_from_indexes(row_new, new_link, inserted_already)?; self.data @@ -416,7 +416,7 @@ where return Err(WorkTableError::PrimaryUpdateTry); } let old_link = self - .pk_map + .primary_index.pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; @@ -429,7 +429,7 @@ where .with_mut_ref(new_link, |r| r.unghost()) .map_err(WorkTableError::PagesError)? } - let (_, primary_key_events) = self.pk_map.insert_cdc(pk.clone(), OffsetEqLink(new_link)); + let (_, primary_key_events) = self.primary_index.pk_map.insert_cdc(pk.clone(), OffsetEqLink(new_link)); let primary_key_events = convert_change_events(primary_key_events); let indexes_res = self.indexes @@ -440,7 +440,7 @@ where at, inserted_already, } => { - self.pk_map.insert(pk.clone(), OffsetEqLink(old_link)); + self.primary_index.pk_map.insert(pk.clone(), OffsetEqLink(old_link)); self.indexes .delete_from_indexes(row_new, new_link, inserted_already)?; self.data diff --git a/src/table/system_info.rs b/src/table/system_info.rs index b0e4aca..a87d562 100644 --- a/src/table/system_info.rs +++ b/src/table/system_info.rs @@ -53,8 +53,8 @@ impl< SecondaryIndexes, LockType, PkGen, - NodeType, const DATA_LENGTH: usize, + NodeType, > WorkTable< Row, @@ -64,19 +64,19 @@ impl< SecondaryIndexes, LockType, PkGen, - NodeType, DATA_LENGTH, + NodeType, > where PrimaryKey: Debug + Clone + Ord + Send + 'static + std::hash::Hash, Row: StorableRow + Send + Clone + 'static, ::WrappedRow: RowWrapper, - NodeType: NodeLike> + Send + 'static, + NodeType: NodeLike>> + Send + 'static, SecondaryIndexes: MemStat + TableSecondaryIndexInfo, { pub fn system_info(&self) -> SystemInfo { let page_count = self.data.get_page_count(); - let row_count = self.pk_map.len(); + let row_count = self.primary_index.pk_map.len(); let empty_links = self.data.get_empty_links().len(); diff --git a/tests/worktable/base.rs b/tests/worktable/base.rs index 62c18c4..6ef72e3 100644 --- a/tests/worktable/base.rs +++ b/tests/worktable/base.rs @@ -188,7 +188,7 @@ async fn update_string() { exchange: "test".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let first_link = table.0.pk_map.get(&pk).unwrap().get().value; + let first_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let updated = TestRow { id: pk.clone().into(), test: 2, @@ -271,7 +271,7 @@ async fn delete() { exchange: "test".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let link = table.0.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); table.delete(pk.clone()).await.unwrap(); let selected_row = table.select(pk); assert!(selected_row.is_none()); @@ -287,7 +287,7 @@ async fn delete() { exchange: "test".to_string(), }; let pk = table.insert(updated.clone()).unwrap(); - let new_link = table.0.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let new_link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); assert_eq!(link, new_link) } @@ -372,7 +372,7 @@ async fn delete_and_insert_less() { exchange: "test1234567890".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let link = table.0.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); table.delete(pk.clone()).await.unwrap(); let selected_row = table.select(pk); assert!(selected_row.is_none()); @@ -384,7 +384,7 @@ async fn delete_and_insert_less() { exchange: "test1".to_string(), }; let pk = table.insert(updated.clone()).unwrap(); - let new_link = table.0.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let new_link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); assert_ne!(link.0, new_link.0) } @@ -406,7 +406,7 @@ async fn delete_and_replace() { exchange: "test".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let link = table.0.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); table.delete(pk.clone()).await.unwrap(); let selected_row = table.select(pk); assert!(selected_row.is_none()); @@ -418,7 +418,7 @@ async fn delete_and_replace() { exchange: "test".to_string(), }; let pk = table.insert(updated.clone()).unwrap(); - let new_link = table.0.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let new_link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); assert_eq!(link, new_link) } diff --git a/tests/worktable/index/insert.rs b/tests/worktable/index/insert.rs index 4dea2a0..69e557b 100644 --- a/tests/worktable/index/insert.rs +++ b/tests/worktable/index/insert.rs @@ -131,12 +131,13 @@ fn insert_when_secondary_unique_exists() { .indexes .attr2_idx .get(&row.attr2) - .map(|r| r.get().value), + .map(|r| r.get().value.0), table .0 + .primary_index .pk_map .get(&TestPrimaryKey(row.id)) - .map(|r| r.get().value) + .map(|r| r.get().value.0) ); } @@ -188,12 +189,13 @@ fn insert_when_secondary_unique_string_exists() { .indexes .attr4_idx .get(&row.attr4) - .map(|r| r.get().value), + .map(|r| r.get().value.0), table .0 + .primary_index .pk_map .get(&TestPrimaryKey(row.id)) - .map(|r| r.get().value) + .map(|r| r.get().value.0) ); } diff --git a/tests/worktable/unsized_.rs b/tests/worktable/unsized_.rs index b6dc787..c47ea61 100644 --- a/tests/worktable/unsized_.rs +++ b/tests/worktable/unsized_.rs @@ -38,7 +38,7 @@ async fn test_update_string_full_row() { exchange: "test".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let first_link = table.0.pk_map.get(&pk).unwrap().get().value; + let first_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; table .update(TestRow { @@ -74,7 +74,7 @@ async fn test_update_string_by_unique() { exchange: "test".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let first_link = table.0.pk_map.get(&pk).unwrap().get().value; + let first_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let row = ExchangeByTestQuery { exchange: "bigger test to test string update".to_string(), @@ -105,7 +105,7 @@ async fn test_update_string_by_pk() { exchange: "test".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let first_link = table.0.pk_map.get(&pk).unwrap().get().value; + let first_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let row = ExchangeByIdQuery { exchange: "bigger test to test string update".to_string(), @@ -136,7 +136,7 @@ async fn test_update_string_by_non_unique() { exchange: "test".to_string(), }; let pk = table.insert(row1.clone()).unwrap(); - let first_link = table.0.pk_map.get(&pk).unwrap().get().value; + let first_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let row2 = TestRow { id: table.get_next_pk().into(), test: 2, @@ -144,7 +144,7 @@ async fn test_update_string_by_non_unique() { exchange: "test".to_string(), }; let pk = table.insert(row2.clone()).unwrap(); - let second_link = table.0.pk_map.get(&pk).unwrap().get().value; + let second_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let row = ExchangeByAbotherQuery { exchange: "bigger test to test string update".to_string(), @@ -329,7 +329,7 @@ async fn test_update_many_strings_by_unique() { other_srting: "other".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let first_link = table.0.pk_map.get(&pk).unwrap().get().value; + let first_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let row = ExchangeAndSomeByTestQuery { exchange: "bigger test to test string update".to_string(), @@ -368,7 +368,7 @@ async fn test_update_many_strings_by_pk() { other_srting: "other".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let first_link = table.0.pk_map.get(&pk).unwrap().get().value; + let first_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let row = ExchangeAndSomeByIdQuery { exchange: "bigger test to test string update".to_string(), @@ -404,7 +404,7 @@ async fn test_update_many_strings_by_non_unique() { other_srting: "other".to_string(), }; let pk = table.insert(row1.clone()).unwrap(); - let first_link = table.0.pk_map.get(&pk).unwrap().get().value; + let first_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let row2 = TestMoreStringsRow { id: table.get_next_pk().into(), test: 2, @@ -414,7 +414,7 @@ async fn test_update_many_strings_by_non_unique() { other_srting: "other".to_string(), }; let pk = table.insert(row2.clone()).unwrap(); - let second_link = table.0.pk_map.get(&pk).unwrap().get().value; + let second_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let row = ExchangeAndSomeByAnotherQuery { exchange: "bigger test to test string update".to_string(), @@ -472,7 +472,7 @@ async fn test_update_many_strings_by_string() { other_srting: "other er".to_string(), }; let pk = table.insert(row1.clone()).unwrap(); - let first_link = table.0.pk_map.get(&pk).unwrap().get().value; + let first_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let row2 = TestMoreStringsRow { id: table.get_next_pk().into(), test: 2, @@ -482,7 +482,7 @@ async fn test_update_many_strings_by_string() { other_srting: "other".to_string(), }; let pk = table.insert(row2.clone()).unwrap(); - let second_link = table.0.pk_map.get(&pk).unwrap().get().value; + let second_link = table.0.primary_index.pk_map.get(&pk).unwrap().get().value; let row = SomeOtherByExchangeQuery { other_srting: "bigger test to test string update".to_string(), From 76e07157f686d30e72ac4382711cb927e2f89633 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:45:24 +0300 Subject: [PATCH 07/32] WIP --- .../persist_table/generator/space_file/mod.rs | 6 +- .../src/worktable/generator/table/impls.rs | 14 +- src/in_memory/data.rs | 75 ++++++ src/in_memory/pages.rs | 41 +-- src/index/table_secondary_index/cdc.rs | 42 ++- src/table/mod.rs | 73 ++++-- src/table/vacuum/mod.rs | 239 ++++++++++++++++-- tests/worktable/base.rs | 48 +++- 8 files changed, 462 insertions(+), 76 deletions(-) diff --git a/codegen/src/persist_table/generator/space_file/mod.rs b/codegen/src/persist_table/generator/space_file/mod.rs index 9a36a5d..1760015 100644 --- a/codegen/src/persist_table/generator/space_file/mod.rs +++ b/codegen/src/persist_table/generator/space_file/mod.rs @@ -224,9 +224,9 @@ impl Generator { #primary_index_init let table = WorkTable { - data, - primary_index, - indexes, + data: std::sync::Arc::new(data), + primary_index: std::sync::Arc::new(primary_index), + indexes: std::sync::Arc::new(indexes), pk_gen: PrimaryKeyGeneratorState::from_state(self.data_info.inner.pk_gen_state), lock_map: LockMap::default(), update_state: IndexMap::default(), diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index 25ebe41..493e50e 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -62,21 +62,27 @@ impl Generator { }) .collect::>(); let pk_types_unsized = is_unsized_vec(pk_types); - let index_size = if pk_types_unsized { + let index_setup = if pk_types_unsized { quote! { - let size = #const_name; + inner.primary_index = std::sync::Arc::new(PrimaryIndex { + pk_map: IndexMap::<#pk_type, OffsetEqLink<#const_name>, UnsizedNode<_>>::with_maximum_node_size(#const_name), + reverse_pk_map: IndexMap::new(), + }); } } else { quote! { let size = get_index_page_size_from_data_length::<#pk_type>(#const_name); + inner.primary_index = std::sync::Arc::new(PrimaryIndex { + pk_map: IndexMap::<_, OffsetEqLink<#const_name>>::with_maximum_node_size(size), + reverse_pk_map: IndexMap::new(), + }); } }; quote! { pub async fn new(config: PersistenceConfig) -> eyre::Result { let mut inner = WorkTable::default(); inner.table_name = #table_name; - #index_size - inner.primary_index.pk_map = IndexMap::with_maximum_node_size(size); + #index_setup let table_files_path = format!("{}/{}", config.tables_path, #dir_name); let engine: #engine = PersistenceEngine::from_table_files_path(table_files_path).await?; core::result::Result::Ok(Self( diff --git a/src/in_memory/data.rs b/src/in_memory/data.rs index ac7103b..ccdddc3 100644 --- a/src/in_memory/data.rs +++ b/src/in_memory/data.rs @@ -257,6 +257,37 @@ impl Data { Ok(bytes.to_vec()) } + /// Moves data within the page from one location to another. + /// Used for defragmentation - shifts data left to fill gaps. + /// + /// # Safety + /// Caller must ensure: + /// - Both `from` and `to` links are valid and point to the same page + /// - `from.length` equals `to.length` + /// - No other references exist during this operation + pub unsafe fn move_from_to(&self, from: Link, to: Link) -> Result<(), ExecutionError> { + if from.length != to.length { + return Err(ExecutionError::InvalidLink); + } + + let inner_data = unsafe { &mut *self.inner_data.get() }; + let src_offset = from.offset as usize; + let dst_offset = to.offset as usize; + let length = from.length as usize; + + // Use ptr::copy for overlapping memory regions (safe for shifting left) + // When moving left (dst_offset < src_offset), this works correctly + unsafe { + std::ptr::copy( + inner_data.as_ptr().add(src_offset), + inner_data.as_mut_ptr().add(dst_offset), + length, + ); + } + + Ok(()) + } + pub fn get_bytes(&self) -> [u8; DATA_LENGTH] { let data = unsafe { &*self.inner_data.get() }; data.0 @@ -293,6 +324,7 @@ mod tests { use rkyv::{Archive, Deserialize, Serialize}; use crate::in_memory::data::{Data, ExecutionError, INNER_PAGE_SIZE}; + use crate::prelude::Link; #[derive( Archive, Copy, Clone, Deserialize, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, @@ -499,4 +531,47 @@ mod tests { let _ = shared.get_row(link).unwrap(); } } + + #[test] + fn move_from_to() { + let page = Data::::new(1.into()); + + let row1 = TestRow { a: 100, b: 200 }; + let link1 = page.save_row(&row1).unwrap(); + assert_eq!(link1.offset, 0); + + let row2 = TestRow { a: 300, b: 400 }; + let link2 = page.save_row(&row2).unwrap(); + assert_eq!(link2.offset, 16); + + let new_link = Link { + page_id: link2.page_id, + offset: 0, + length: link2.length, + }; + + unsafe { page.move_from_to(link2, new_link).unwrap() }; + + let moved_row = page.get_row(new_link).unwrap(); + assert_eq!(moved_row, row2); + } + + #[test] + fn move_from_to_different_lengths() { + let page = Data::::new(1.into()); + + let from = Link { + page_id: 1.into(), + offset: 0, + length: 16, + }; + let to = Link { + page_id: 1.into(), + offset: 32, + length: 8, + }; + + let result = unsafe { page.move_from_to(from, to) }; + assert!(matches!(result, Err(ExecutionError::InvalidLink))); + } } diff --git a/src/in_memory/pages.rs b/src/in_memory/pages.rs index b710af8..ffe34e6 100644 --- a/src/in_memory/pages.rs +++ b/src/in_memory/pages.rs @@ -1,11 +1,12 @@ use std::{ fmt::Debug, + sync::Arc, sync::atomic::{AtomicU32, AtomicU64, Ordering}, - sync::{Arc, RwLock}, }; use data_bucket::page::PageId; use derive_more::{Display, Error, From}; +use parking_lot::RwLock; #[cfg(feature = "perf_measurements")] use performance_measurement_codegen::performance_measurement; use rkyv::{ @@ -103,7 +104,7 @@ where let general_row = ::WrappedRow::from_inner(row); if let Some(link) = self.empty_links.pop_max() { - let pages = self.pages.read().unwrap(); + let pages = self.pages.read(); let current_page: usize = page_id_mapper(link.page_id.into()); let page = &pages[current_page]; @@ -131,7 +132,7 @@ where loop { let (link, tried_page) = { - let pages = self.pages.read().unwrap(); + let pages = self.pages.read(); let current_page = page_id_mapper(self.current_page_id.load(Ordering::Acquire) as usize); let page = &pages[current_page]; @@ -180,7 +181,7 @@ where } fn add_next_page(&self, tried_page: usize) { - let mut pages = self.pages.write().expect("lock should be not poisoned"); + let mut pages = self.pages.write(); if tried_page == page_id_mapper(self.current_page_id.load(Ordering::Acquire) as usize) { let index = self.last_page_id.fetch_add(1, Ordering::AcqRel) + 1; @@ -202,7 +203,7 @@ where <::WrappedRow as Archive>::Archived: Portable + Deserialize<::WrappedRow, HighDeserializer>, { - let pages = self.pages.read().unwrap(); + let pages = self.pages.read(); let page = pages // - 1 is used because page ids are starting from 1. .get(page_id_mapper(link.page_id.into())) @@ -220,7 +221,7 @@ where <::WrappedRow as Archive>::Archived: Portable + Deserialize<::WrappedRow, HighDeserializer>, { - let pages = self.pages.read().unwrap(); + let pages = self.pages.read(); let page = pages // - 1 is used because page ids are starting from 1. .get(page_id_mapper(link.page_id.into())) @@ -244,7 +245,7 @@ where >, Op: Fn(&<::WrappedRow as Archive>::Archived) -> Res, { - let pages = self.pages.read().unwrap(); + let pages = self.pages.read(); let page = pages .get::(page_id_mapper(link.page_id.into())) .ok_or(ExecutionError::PageNotFound(link.page_id))?; @@ -273,7 +274,7 @@ where <::WrappedRow as Archive>::Archived: Portable, Op: FnMut(&mut <::WrappedRow as Archive>::Archived) -> Res, { - let pages = self.pages.read().unwrap(); + let pages = self.pages.read(); let page = pages .get(page_id_mapper(link.page_id.into())) .ok_or(ExecutionError::PageNotFound(link.page_id))?; @@ -304,7 +305,7 @@ where Strategy, Share>, rkyv::rancor::Error>, >, { - let pages = self.pages.read().unwrap(); + let pages = self.pages.read(); let page = pages .get(page_id_mapper(link.page_id.into())) .ok_or(ExecutionError::PageNotFound(link.page_id))?; @@ -323,7 +324,7 @@ where } pub fn select_raw(&self, link: Link) -> Result, ExecutionError> { - let pages = self.pages.read().unwrap(); + let pages = self.pages.read(); let page = pages .get(page_id_mapper(link.page_id.into())) .ok_or(ExecutionError::PageNotFound(link.page_id))?; @@ -331,8 +332,17 @@ where .map_err(ExecutionError::DataPageError) } + pub fn get_page( + &self, + page_id: PageId, + ) -> Option::WrappedRow, DATA_LENGTH>>> { + let pages = self.pages.read(); + let page = pages.get(page_id_mapper(page_id.into()))?; + Some(page.clone()) + } + pub fn get_bytes(&self) -> Vec<([u8; DATA_LENGTH], u32)> { - let pages = self.pages.read().unwrap(); + let pages = self.pages.read(); pages .iter() .map(|p| (p.get_bytes(), p.free_offset.load(Ordering::Relaxed))) @@ -340,7 +350,7 @@ where } pub fn get_page_count(&self) -> usize { - self.pages.read().unwrap().len() + self.pages.read().len() } pub fn get_empty_links(&self) -> Vec { @@ -376,11 +386,12 @@ pub enum ExecutionError { #[cfg(test)] mod tests { use std::collections::HashSet; + use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; - use std::sync::{Arc, RwLock}; use std::thread; use std::time::Instant; + use parking_lot::RwLock; use rkyv::with::{AtomicLoad, Relaxed}; use rkyv::{Archive, Deserialize, Serialize}; @@ -567,7 +578,7 @@ mod tests { for i in 0..1000 { let row = TestRow { a: i, b: j * i + 1 }; - let mut pages = pages_shared.write().unwrap(); + let mut pages = pages_shared.write(); pages.insert(row); } }); @@ -598,7 +609,7 @@ mod tests { for i in 0..1000 { let row = TestRow { a: i, b: j * i + 1 }; - let mut pages = pages_shared.write().unwrap(); + let mut pages = pages_shared.write(); pages.push(row); } }); diff --git a/src/index/table_secondary_index/cdc.rs b/src/index/table_secondary_index/cdc.rs index 8bea76b..88f7e7c 100644 --- a/src/index/table_secondary_index/cdc.rs +++ b/src/index/table_secondary_index/cdc.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use data_bucket::Link; -use crate::{Difference, IndexError}; +use crate::{Difference, IndexError, TableSecondaryIndex}; pub trait TableSecondaryIndexCdc { fn save_row_cdc( @@ -33,3 +33,43 @@ pub trait TableSecondaryIndexCdc>, ) -> Result>; } + +impl + TableSecondaryIndexCdc for T +where + T: TableSecondaryIndex, +{ + fn save_row_cdc(&self, row: Row, link: Link) -> Result<(), IndexError> { + self.save_row(row, link) + } + + fn reinsert_row_cdc( + &self, + row_old: Row, + link_old: Link, + row_new: Row, + link_new: Link, + ) -> Result<(), IndexError> { + self.reinsert_row(row_old, link_old, row_new, link_new) + } + + fn delete_row_cdc(&self, row: Row, link: Link) -> Result<(), IndexError> { + self.delete_row(row, link) + } + + fn process_difference_insert_cdc( + &self, + link: Link, + differences: HashMap<&str, Difference>, + ) -> Result<(), IndexError> { + self.process_difference_insert(link, differences) + } + + fn process_difference_remove_cdc( + &self, + link: Link, + differences: HashMap<&str, Difference>, + ) -> Result<(), IndexError> { + self.process_difference_remove(link, differences) + } +} diff --git a/src/table/mod.rs b/src/table/mod.rs index 9afb3ce..7fde532 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -2,9 +2,6 @@ pub mod select; pub mod system_info; pub mod vacuum; -use std::fmt::Debug; -use std::marker::PhantomData; - use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; use crate::lock::LockMap; use crate::persistence::{InsertOperation, Operation}; @@ -28,6 +25,9 @@ use rkyv::ser::allocator::ArenaHandle; use rkyv::ser::sharing::Share; use rkyv::util::AlignedVec; use rkyv::{Archive, Deserialize, Serialize}; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::sync::Arc; use uuid::Uuid; #[derive(Debug)] @@ -46,11 +46,11 @@ pub struct WorkTable< Row: StorableRow + Send + Clone + 'static, PkNodeType: NodeLike>> + Send + 'static, { - pub data: DataPages, + pub data: Arc>, - pub primary_index: PrimaryIndex, + pub primary_index: Arc>, - pub indexes: SecondaryIndexes, + pub indexes: Arc, pub pk_gen: PkGen, @@ -76,16 +76,16 @@ impl< PkNodeType, > Default for WorkTable< - Row, - PrimaryKey, - AvailableTypes, - AvailableIndexes, - SecondaryIndexes, - LockType, - PkGen, - DATA_LENGTH, - PkNodeType, -> + Row, + PrimaryKey, + AvailableTypes, + AvailableIndexes, + SecondaryIndexes, + LockType, + PkGen, + DATA_LENGTH, + PkNodeType, + > where PrimaryKey: Debug + Clone + Ord + Send + TablePrimaryKey + std::hash::Hash, SecondaryIndexes: Default, @@ -96,9 +96,9 @@ where { fn default() -> Self { Self { - data: DataPages::new(), - primary_index: PrimaryIndex::default(), - indexes: SecondaryIndexes::default(), + data: Arc::new(DataPages::new()), + primary_index: Arc::new(PrimaryIndex::default()), + indexes: Arc::new(SecondaryIndexes::default()), pk_gen: Default::default(), lock_map: LockMap::default(), update_state: IndexMap::default(), @@ -159,7 +159,11 @@ where <::WrappedRow as Archive>::Archived: Deserialize<::WrappedRow, HighDeserializer>, { - let link = self.primary_index.pk_map.get(&pk).map(|v| v.get().value.into()); + let link = self + .primary_index + .pk_map + .get(&pk) + .map(|v| v.get().value.into()); if let Some(link) = link { self.data.select(link).ok() } else { @@ -195,7 +199,8 @@ where .insert(row.clone()) .map_err(WorkTableError::PagesError)?; if self - .primary_index.pk_map + .primary_index + .pk_map .checked_insert(pk.clone(), OffsetEqLink(link)) .is_none() { @@ -261,7 +266,8 @@ where .insert_cdc(row.clone()) .map_err(WorkTableError::PagesError)?; let primary_key_events = self - .primary_index.pk_map + .primary_index + .pk_map .checked_insert_cdc(pk.clone(), OffsetEqLink(link)); let Some(primary_key_events) = primary_key_events else { self.data.delete(link).map_err(WorkTableError::PagesError)?; @@ -340,7 +346,8 @@ where return Err(WorkTableError::PrimaryUpdateTry); } let old_link = self - .primary_index.pk_map + .primary_index + .pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; @@ -353,7 +360,9 @@ where .with_mut_ref(new_link, |r| r.unghost()) .map_err(WorkTableError::PagesError)? } - self.primary_index.pk_map.insert(pk.clone(), OffsetEqLink(new_link)); + self.primary_index + .pk_map + .insert(pk.clone(), OffsetEqLink(new_link)); let indexes_res = self .indexes @@ -364,7 +373,9 @@ where at, inserted_already, } => { - self.primary_index.pk_map.insert(pk.clone(), OffsetEqLink(old_link)); + self.primary_index + .pk_map + .insert(pk.clone(), OffsetEqLink(old_link)); self.indexes .delete_from_indexes(row_new, new_link, inserted_already)?; self.data @@ -416,7 +427,8 @@ where return Err(WorkTableError::PrimaryUpdateTry); } let old_link = self - .primary_index.pk_map + .primary_index + .pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; @@ -429,7 +441,10 @@ where .with_mut_ref(new_link, |r| r.unghost()) .map_err(WorkTableError::PagesError)? } - let (_, primary_key_events) = self.primary_index.pk_map.insert_cdc(pk.clone(), OffsetEqLink(new_link)); + let (_, primary_key_events) = self + .primary_index + .pk_map + .insert_cdc(pk.clone(), OffsetEqLink(new_link)); let primary_key_events = convert_change_events(primary_key_events); let indexes_res = self.indexes @@ -440,7 +455,9 @@ where at, inserted_already, } => { - self.primary_index.pk_map.insert(pk.clone(), OffsetEqLink(old_link)); + self.primary_index + .pk_map + .insert(pk.clone(), OffsetEqLink(old_link)); self.indexes .delete_from_indexes(row_new, new_link, inserted_already)?; self.data diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 14e2c04..50d24e6 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -7,6 +7,7 @@ use std::marker::PhantomData; use std::sync::Arc; use data_bucket::Link; +use data_bucket::page::PageId; use indexset::core::node::NodeLike; use indexset::core::pair::Pair; use rkyv::rancor::Strategy; @@ -17,10 +18,12 @@ use rkyv::util::AlignedVec; use rkyv::{Archive, Serialize}; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; -use crate::prelude::TablePrimaryKey; +use crate::prelude::{OffsetEqLink, TablePrimaryKey}; use crate::vacuum::fragmentation_info::PageFragmentationInfo; use crate::vacuum::lock::VacuumLock; -use crate::{AvailableIndex, IndexMap, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; +use crate::{ + AvailableIndex, IndexMap, PrimaryIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc, +}; #[derive(Debug)] pub struct EmptyDataVacuum< @@ -28,19 +31,19 @@ pub struct EmptyDataVacuum< PrimaryKey, PkNodeType, SecondaryIndexes, - SecondaryEvents, AvailableTypes, AvailableIndexes, const DATA_LENGTH: usize, + SecondaryEvents = (), > where PrimaryKey: Clone + Ord + Send + 'static + std::hash::Hash, Row: StorableRow + Send + Clone + 'static, - PkNodeType: NodeLike> + Send + 'static, + PkNodeType: NodeLike>> + Send + 'static, { - data_pages: DataPages, + data_pages: Arc>, vacuum_lock: Arc, - primary_index: Arc>, + primary_index: Arc>, secondary_indexes: Arc, phantom_data: PhantomData<(SecondaryEvents, AvailableTypes, AvailableIndexes)>, @@ -51,25 +54,25 @@ impl< PrimaryKey, PkNodeType, SecondaryIndexes, - SecondaryEvents, AvailableTypes, AvailableIndexes, const DATA_LENGTH: usize, + SecondaryEvents, > EmptyDataVacuum< Row, PrimaryKey, PkNodeType, SecondaryIndexes, - SecondaryEvents, AvailableTypes, AvailableIndexes, DATA_LENGTH, + SecondaryEvents, > where Row: TableRow + StorableRow + Send + Clone + 'static, PrimaryKey: Debug + Clone + Ord + Send + TablePrimaryKey + std::hash::Hash, - PkNodeType: NodeLike> + Send + 'static, + PkNodeType: NodeLike>> + Send + 'static, ::WrappedRow: RowWrapper, Row: Archive + Clone @@ -86,8 +89,6 @@ where AvailableIndexes: Debug + AvailableIndex, { async fn defragment_page(&self, info: PageFragmentationInfo) { - let lock = self.vacuum_lock.lock_page(info.page_id); - let mut page_empty_links = self .data_pages .empty_links_registry() @@ -96,14 +97,214 @@ where .map(|(_, l)| *l) .collect::>(); page_empty_links.sort_by(|l1, l2| l1.offset.cmp(&l2.offset)); + + let _lock = self.vacuum_lock.lock_page(info.page_id); + let mut empty_links_iter = page_empty_links.into_iter(); + + let Some(mut current_empty) = empty_links_iter.next() else { + return; + }; + + let Some(mut next_empty) = empty_links_iter.next() else { + self.shift_data_in_range(current_empty, None); + return; + }; + + loop { + let offset = self.shift_data_in_range(current_empty, Some(next_empty.offset)); + + let new_next = empty_links_iter.next(); + match new_next { + Some(link) => { + current_empty = Link { + page_id: next_empty.page_id, + offset, + length: next_empty.length + (next_empty.offset - offset), + }; + next_empty = link; + } + None => { + self.shift_data_in_range(next_empty, None); + break; + } + } + } + } + + pub fn shift_data_in_range(&self, start_link: Link, end_offset: Option) -> u32 { + let page_id = start_link.page_id; + let page = self + .data_pages + .get_page(page_id) + .expect("should exist as link exists"); + let start_link = OffsetEqLink::<_>(start_link); + let mut range_iter = self.primary_index.reverse_pk_map.range(start_link..); + let mut entry_offset = start_link.0.offset; + + while let Some((link, pk)) = range_iter.next() { + let link_value = link.0; + + if let Some(end) = end_offset { + if entry_offset + link_value.length >= end { + return entry_offset; + } + } + + let new_link = Link { + page_id, + offset: entry_offset, + length: link_value.length, + }; + + // TODO: Safety comment + unsafe { + page.move_from_to(link_value, new_link) + .expect("should use valid links") + } + entry_offset += link_value.length; + self.update_index_after_move(pk.clone(), link_value, new_link); + } + + entry_offset + } + + fn update_index_after_move(&self, pk: PrimaryKey, old_link: Link, new_link: Link) { + let old_offset_link = OffsetEqLink(old_link); + let new_offset_link = OffsetEqLink(new_link); + + self.primary_index + .pk_map + .insert(pk.clone(), new_offset_link); + self.primary_index.reverse_pk_map.remove(&old_offset_link); + self.primary_index + .reverse_pk_map + .insert(new_offset_link, pk); + // TODO: update secondary indexes } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::marker::PhantomData; + use std::sync::Arc; + + use indexset::core::pair::Pair; + use worktable_codegen::{MemStat, worktable}; + + use crate::in_memory::{GhostWrapper, RowWrapper, StorableRow}; + use crate::prelude::*; + use crate::vacuum::EmptyDataVacuum; + use crate::vacuum::lock::VacuumLock; + + worktable! ( + name: Test, + columns: { + id: u64 primary_key autoincrement, + test: i64, + another: u64, + exchange: String + }, + indexes: { + test_idx: test unique, + exchnage_idx: exchange, + another_idx: another, + } + ); + + /// Creates an EmptyDataVacuum instance from a WorkTable + fn create_vacuum( + table: &TestWorkTable, + ) -> EmptyDataVacuum< + TestRow, + TestPrimaryKey, + Vec>>, + TestIndex, + TestAvaiableTypes, + TestAvailableIndexes, + TEST_INNER_SIZE, + > { + EmptyDataVacuum { + data_pages: Arc::clone(&table.0.data), + vacuum_lock: Arc::new(VacuumLock::default()), + primary_index: Arc::clone(&table.0.primary_index), + secondary_indexes: Arc::clone(&table.0.indexes), + phantom_data: PhantomData, + } + } + + #[tokio::test] + async fn test_vacuum_shift_data_in_range_single_gap() { + let table = TestWorkTable::default(); + + let mut ids = HashMap::new(); + for i in 0..10 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.insert(id, row); + } + + let first_two_ids = ids.keys().take(2).cloned().collect::>(); + + table.delete(first_two_ids[0].into()).await.unwrap(); + table.delete(first_two_ids[1].into()).await.unwrap(); - // pub fn new(data_pages: DataPages) -> Self { - // Self { - // data_pages, - // vacuum_lock: Arc::new(Default::default()), - // } - // } - // - // pub fn vacuum_pages() -> eyre::Result<()> {} + let vacuum = create_vacuum(&table); + + let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); + let info = per_page_info + .first() + .expect("at least one page should exist"); + vacuum.defragment_page(*info).await; + + for (id, expected) in ids.into_iter().skip(2) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_shift_data_middle_gap() { + let table = TestWorkTable::default(); + + let mut ids = HashMap::new(); + for i in 0..20 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i * 10, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.insert(id, row); + } + + let ids_to_delete = ids.keys().skip(5).take(2).cloned().collect::>(); + + table.delete(ids_to_delete[0].into()).await.unwrap(); + table.delete(ids_to_delete[1].into()).await.unwrap(); + + let vacuum = create_vacuum(&table); + + let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); + let info = per_page_info + .first() + .expect("at least one page should exist"); + vacuum.defragment_page(*info).await; + + for (id, expected) in ids + .into_iter() + .filter(|(i, _)| *i != ids_to_delete[0] && *i != ids_to_delete[1]) + { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } } diff --git a/tests/worktable/base.rs b/tests/worktable/base.rs index 6ef72e3..ef3675c 100644 --- a/tests/worktable/base.rs +++ b/tests/worktable/base.rs @@ -271,7 +271,13 @@ async fn delete() { exchange: "test".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let link = table + .0 + .primary_index + .pk_map + .get(&pk) + .map(|kv| kv.get().value) + .unwrap(); table.delete(pk.clone()).await.unwrap(); let selected_row = table.select(pk); assert!(selected_row.is_none()); @@ -287,7 +293,13 @@ async fn delete() { exchange: "test".to_string(), }; let pk = table.insert(updated.clone()).unwrap(); - let new_link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let new_link = table + .0 + .primary_index + .pk_map + .get(&pk) + .map(|kv| kv.get().value) + .unwrap(); assert_eq!(link, new_link) } @@ -372,7 +384,13 @@ async fn delete_and_insert_less() { exchange: "test1234567890".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let link = table + .0 + .primary_index + .pk_map + .get(&pk) + .map(|kv| kv.get().value) + .unwrap(); table.delete(pk.clone()).await.unwrap(); let selected_row = table.select(pk); assert!(selected_row.is_none()); @@ -384,7 +402,13 @@ async fn delete_and_insert_less() { exchange: "test1".to_string(), }; let pk = table.insert(updated.clone()).unwrap(); - let new_link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let new_link = table + .0 + .primary_index + .pk_map + .get(&pk) + .map(|kv| kv.get().value) + .unwrap(); assert_ne!(link.0, new_link.0) } @@ -406,7 +430,13 @@ async fn delete_and_replace() { exchange: "test".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let link = table + .0 + .primary_index + .pk_map + .get(&pk) + .map(|kv| kv.get().value) + .unwrap(); table.delete(pk.clone()).await.unwrap(); let selected_row = table.select(pk); assert!(selected_row.is_none()); @@ -418,7 +448,13 @@ async fn delete_and_replace() { exchange: "test".to_string(), }; let pk = table.insert(updated.clone()).unwrap(); - let new_link = table.0.primary_index.pk_map.get(&pk).map(|kv| kv.get().value).unwrap(); + let new_link = table + .0 + .primary_index + .pk_map + .get(&pk) + .map(|kv| kv.get().value) + .unwrap(); assert_eq!(link, new_link) } From 2cc533ad15648ad13260d17a8f732dc6ed75c8b8 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:15:02 +0300 Subject: [PATCH 08/32] add tests for one page defragementation --- src/table/vacuum/mod.rs | 158 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 50d24e6..5ee3718 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -307,4 +307,162 @@ mod tests { assert_eq!(row, Some(expected)); } } + + #[tokio::test] + async fn test_vacuum_shift_data_last_records() { + let table = TestWorkTable::default(); + + let mut ids = HashMap::new(); + for i in 0..10 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.insert(id, row); + } + + let last_two_ids = ids.keys().skip(8).take(2).cloned().collect::>(); + + table.delete(last_two_ids[1].into()).await.unwrap(); + table.delete(last_two_ids[0].into()).await.unwrap(); + + let vacuum = create_vacuum(&table); + + let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); + let info = per_page_info + .first() + .expect("at least one page should exist"); + vacuum.defragment_page(*info).await; + + for (id, expected) in ids + .into_iter() + .filter(|(i, _)| *i != last_two_ids[0] && *i != last_two_ids[1]) + { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_shift_data_multiple_gaps() { + let table = TestWorkTable::default(); + + let mut ids = HashMap::new(); + for i in 0..15 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.insert(id, row); + } + + let ids_to_delete = [1, 3, 5, 7].map(|idx| ids.keys().cloned().nth(idx).unwrap()); + + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + + let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); + let info = per_page_info + .first() + .expect("at least one page should exist"); + vacuum.defragment_page(*info).await; + + for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_shift_data_single_record_left() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + for i in 0..5 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + let remaining_id = ids[0].0; + + for (id, _) in ids.iter().skip(1) { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + + let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); + let info = per_page_info + .first() + .expect("at least one page should exist"); + vacuum.defragment_page(*info).await; + + let row = table.select(remaining_id); + assert_eq!(row, Some(ids[0].1.clone())); + } + + #[tokio::test] + async fn test_vacuum_shift_data_variable_string_lengths() { + let table = TestWorkTable::default(); + + let mut ids = HashMap::new(); + let strings = vec![ + "a", + "bbbb", + "cccccc", + "dddddddd", + "eeeeeeeeee", + "ffffffffffff", + "gggggggggggggg", + ]; + + for (i, s) in strings.iter().enumerate() { + let row = TestRow { + id: table.get_next_pk().into(), + test: i as i64, + another: i as u64, + exchange: s.to_string(), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.insert(id, row); + } + + let ids_to_delete = ids.keys().take(3).cloned().collect::>(); + + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + + let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); + let info = per_page_info + .first() + .expect("at least one page should exist"); + vacuum.defragment_page(*info).await; + + for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } } From ca0f1546f05d464c59e9dd1a8e287ff8a68c5438 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:46:54 +0300 Subject: [PATCH 09/32] set proper offset --- src/in_memory/empty_link_registry.rs | 2 +- src/table/vacuum/mod.rs | 76 +++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/in_memory/empty_link_registry.rs b/src/in_memory/empty_link_registry.rs index c8cd0bc..a4a7e26 100644 --- a/src/in_memory/empty_link_registry.rs +++ b/src/in_memory/empty_link_registry.rs @@ -97,7 +97,7 @@ impl Default for EmptyLinkRegistry { } impl EmptyLinkRegistry { - fn remove_link>(&self, link: L) { + pub fn remove_link>(&self, link: L) { let link = link.into(); self.index_ord_links.remove(&IndexOrdLink(link)); self.length_ord_links.remove(&link.length, &link); diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 5ee3718..38c0fc9 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -2,10 +2,6 @@ mod fragmentation_info; mod lock; mod page; -use std::fmt::Debug; -use std::marker::PhantomData; -use std::sync::Arc; - use data_bucket::Link; use data_bucket::page::PageId; use indexset::core::node::NodeLike; @@ -16,6 +12,10 @@ use rkyv::ser::allocator::ArenaHandle; use rkyv::ser::sharing::Share; use rkyv::util::AlignedVec; use rkyv::{Archive, Serialize}; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::sync::Arc; +use std::sync::atomic::Ordering; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; use crate::prelude::{OffsetEqLink, TablePrimaryKey}; @@ -89,9 +89,8 @@ where AvailableIndexes: Debug + AvailableIndex, { async fn defragment_page(&self, info: PageFragmentationInfo) { - let mut page_empty_links = self - .data_pages - .empty_links_registry() + let registry = self.data_pages.empty_links_registry(); + let mut page_empty_links = registry .page_links_map .get(&info.page_id) .map(|(_, l)| *l) @@ -104,11 +103,13 @@ where let Some(mut current_empty) = empty_links_iter.next() else { return; }; + registry.remove_link(current_empty); let Some(mut next_empty) = empty_links_iter.next() else { self.shift_data_in_range(current_empty, None); return; }; + registry.remove_link(next_empty); loop { let offset = self.shift_data_in_range(current_empty, Some(next_empty.offset)); @@ -116,6 +117,7 @@ where let new_next = empty_links_iter.next(); match new_next { Some(link) => { + registry.remove_link(link); current_empty = Link { page_id: next_empty.page_id, offset, @@ -165,6 +167,10 @@ where self.update_index_after_move(pk.clone(), link_value, new_link); } + // Is safe as page is locked now and we can get here only if end_offset + // is not set so we are shifting till page end. + page.free_offset.store(entry_offset, Ordering::Release); + entry_offset } @@ -465,4 +471,60 @@ mod tests { assert_eq!(row, Some(expected)); } } + + #[tokio::test] + async fn test_vacuum_insert_after_free_offset_update() { + let table = TestWorkTable::default(); + + let mut original_ids = HashMap::new(); + for i in 0..8 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("original{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + original_ids.insert(id, row); + } + + let ids_to_delete = original_ids.keys().take(3).cloned().collect::>(); + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); + let info = per_page_info + .first() + .expect("at least one page should exist"); + vacuum.defragment_page(*info).await; + + let mut new_ids = HashMap::new(); + for i in 0..3 { + let row = TestRow { + id: table.get_next_pk().into(), + test: 100 + i, + another: (100 + i) as u64, + exchange: format!("new{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + new_ids.insert(id, row); + } + + for (id, expected) in original_ids + .into_iter() + .filter(|(i, _)| !ids_to_delete.contains(i)) + { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + + for (id, expected) in new_ids { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } } From 633ca9738833567ea242c458fb53d4e82bf4dfe9 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:01:33 +0300 Subject: [PATCH 10/32] WIP --- src/in_memory/data.rs | 121 +++++++++++++++++++++++++++++++++++++ src/index/primary_index.rs | 5 -- src/lock/row_lock.rs | 6 ++ src/table/mod.rs | 33 +++------- src/table/vacuum/mod.rs | 58 +++++++++++++++--- 5 files changed, 188 insertions(+), 35 deletions(-) diff --git a/src/in_memory/data.rs b/src/in_memory/data.rs index ccdddc3..50c6edc 100644 --- a/src/in_memory/data.rs +++ b/src/in_memory/data.rs @@ -288,10 +288,43 @@ impl Data { Ok(()) } + /// Saves raw serialized bytes to the end of the page. + /// Used for moving already-serialized data without re-serialization. + pub fn save_raw_row(&self, data: &[u8]) -> Result { + let length = data.len(); + if length > DATA_LENGTH { + return Err(ExecutionError::PageTooSmall { + need: length, + allowed: DATA_LENGTH, + }); + } + let length = length as u32; + let offset = self.free_offset.fetch_add(length, Ordering::AcqRel); + if offset > DATA_LENGTH as u32 - length { + return Err(ExecutionError::PageIsFull { + need: length, + left: DATA_LENGTH as i64 - offset as i64, + }); + } + + let inner_data = unsafe { &mut *self.inner_data.get() }; + inner_data[offset as usize..][..length as usize].copy_from_slice(data); + + Ok(Link { + page_id: self.id, + offset, + length, + }) + } + pub fn get_bytes(&self) -> [u8; DATA_LENGTH] { let data = unsafe { &*self.inner_data.get() }; data.0 } + + pub fn free_space(&self) -> usize { + DATA_LENGTH.saturating_sub(self.free_offset.load(Ordering::Acquire) as usize) + } } /// Error that can appear on [`Data`] page operations. @@ -348,12 +381,16 @@ mod tests { let page = Data::::new(1.into()); let row = TestRow { a: 10, b: 20 }; + let initial_free = page.free_space(); + assert!(initial_free > 0); + let link = page.save_row(&row).unwrap(); assert_eq!(link.page_id, page.id); assert_eq!(link.length, 16); assert_eq!(link.offset, 0); assert_eq!(page.free_offset.load(Ordering::Relaxed), link.length); + assert_eq!(page.free_space(), initial_free - link.length as usize); let inner_data = unsafe { &mut *page.inner_data.get() }; let bytes = &inner_data[link.offset as usize..link.length as usize]; @@ -440,6 +477,9 @@ mod tests { fn data_page_save_many_rows() { let page = Data::::new(1.into()); + let initial_free = page.free_space(); + let mut total_used = 0; + let mut rows = Vec::new(); let mut links = Vec::new(); for i in 1..10 { @@ -450,9 +490,12 @@ mod tests { rows.push(row); let link = page.save_row(&row); + total_used += link.as_ref().unwrap().length as usize; links.push(link) } + assert_eq!(page.free_space(), initial_free - total_used); + let inner_data = unsafe { &mut *page.inner_data.get() }; for (i, link) in links.into_iter().enumerate() { @@ -574,4 +617,82 @@ mod tests { let result = unsafe { page.move_from_to(from, to) }; assert!(matches!(result, Err(ExecutionError::InvalidLink))); } + + #[test] + fn save_raw_row_appends_to_page() { + let page = Data::::new(1.into()); + let row = TestRow { a: 42, b: 99 }; + + let link = page.save_row(&row).unwrap(); + let raw_data = page.get_raw_row(link).unwrap(); + + let new_link = page.save_raw_row(&raw_data).unwrap(); + + assert_eq!(new_link.page_id, page.id); + assert_eq!(new_link.length, link.length); + assert_eq!(new_link.offset, link.length); + + let retrieved = page.get_row(new_link).unwrap(); + assert_eq!(retrieved, row); + } + + #[test] + fn save_raw_row_page_too_small() { + let page = Data::::new(1.into()); + let data = vec![0u8; 32]; + + let result = page.save_raw_row(&data); + assert!(matches!(result, Err(ExecutionError::PageTooSmall { .. }))); + } + + #[test] + fn save_raw_row_page_full() { + let page = Data::::new(1.into()); + let row = TestRow { a: 1, b: 2 }; + let _ = page.save_row(&row).unwrap(); + + let data = vec![0u8; 16]; + let result = page.save_raw_row(&data); + assert!(matches!(result, Err(ExecutionError::PageIsFull { .. }))); + } + + #[test] + fn save_raw_row_move_between_pages() { + let page1 = Data::::new(1.into()); + let page2 = Data::::new(2.into()); + + let original = TestRow { a: 123, b: 456 }; + let link1 = page1.save_row(&original).unwrap(); + + let raw = page1.get_raw_row(link1).unwrap(); + let link2 = page2.save_raw_row(&raw).unwrap(); + + let retrieved = page2.get_row(link2).unwrap(); + assert_eq!(retrieved, original); + } + + #[test] + fn save_raw_row_multiple_entries() { + let page = Data::::new(1.into()); + let row = TestRow { a: 77, b: 88 }; + + let link = page.save_row(&row).unwrap(); + let raw_data = page.get_raw_row(link).unwrap(); + let row_size = link.length as usize; + + let initial_free = page.free_space(); + + let mut links = vec![link]; + for i in 0..5 { + let new_link = page.save_raw_row(&raw_data).unwrap(); + links.push(new_link); + let expected_free = initial_free - ((i + 1) as usize * row_size); + assert_eq!(page.free_space(), expected_free); + } + + for link in links { + let retrieved = page.get_row(link).unwrap(); + assert_eq!(retrieved, row); + } + } } diff --git a/src/index/primary_index.rs b/src/index/primary_index.rs index a5f174b..c49a4c0 100644 --- a/src/index/primary_index.rs +++ b/src/index/primary_index.rs @@ -91,10 +91,7 @@ where let offset_link = OffsetEqLink(link); let (res, evs) = self.pk_map.insert_cdc(value.clone(), offset_link); let res_link = res.map(|l| l.0); - - // Update reverse index based on result if let Some(res) = res { - // Old value existed, remove it from reverse index self.reverse_pk_map.remove(&res); } self.reverse_pk_map.insert(offset_link, value); @@ -111,11 +108,9 @@ where let res = self.pk_map.checked_insert_cdc(value.clone(), offset_link); if let Some(evs) = res { - // Insert was successful, update reverse index self.reverse_pk_map.insert(offset_link, value); Some(convert_change_events(evs)) } else { - // Key already existed None } } diff --git a/src/lock/row_lock.rs b/src/lock/row_lock.rs index e8fe2dd..7335685 100644 --- a/src/lock/row_lock.rs +++ b/src/lock/row_lock.rs @@ -27,6 +27,12 @@ pub struct FullRowLock { l: Arc, } +impl FullRowLock { + pub fn unlock(&self) { + self.l.unlock(); + } +} + impl RowLock for FullRowLock { fn is_locked(&self) -> bool { self.l.is_locked() diff --git a/src/table/mod.rs b/src/table/mod.rs index 7fde532..82731bd 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -9,8 +9,8 @@ use crate::prelude::{OperationId, PrimaryKeyGeneratorState}; use crate::primary_key::{PrimaryKeyGenerator, TablePrimaryKey}; use crate::util::OffsetEqLink; use crate::{ - AvailableIndex, IndexError, IndexMap, PrimaryIndex, TableRow, TableSecondaryIndex, - TableSecondaryIndexCdc, convert_change_events, in_memory, + AvailableIndex, IndexError, IndexMap, PrimaryIndex, TableIndex, TableIndexCdc, TableRow, + TableSecondaryIndex, TableSecondaryIndexCdc, convert_change_events, in_memory, }; use data_bucket::INNER_PAGE_SIZE; use derive_more::{Display, Error, From}; @@ -200,8 +200,7 @@ where .map_err(WorkTableError::PagesError)?; if self .primary_index - .pk_map - .checked_insert(pk.clone(), OffsetEqLink(link)) + .insert_checked(pk.clone(), link) .is_none() { self.data.delete(link).map_err(WorkTableError::PagesError)?; @@ -214,7 +213,7 @@ where inserted_already, } => { self.data.delete(link).map_err(WorkTableError::PagesError)?; - self.primary_index.pk_map.remove(&pk); + self.primary_index.remove(&pk, link); self.indexes .delete_from_indexes(row, link, inserted_already)?; @@ -265,10 +264,7 @@ where .data .insert_cdc(row.clone()) .map_err(WorkTableError::PagesError)?; - let primary_key_events = self - .primary_index - .pk_map - .checked_insert_cdc(pk.clone(), OffsetEqLink(link)); + let primary_key_events = self.primary_index.insert_checked_cdc(pk.clone(), link); let Some(primary_key_events) = primary_key_events else { self.data.delete(link).map_err(WorkTableError::PagesError)?; return Err(WorkTableError::AlreadyExists("Primary".to_string())); @@ -282,7 +278,7 @@ where inserted_already, } => { self.data.delete(link).map_err(WorkTableError::PagesError)?; - self.primary_index.pk_map.remove(&pk); + self.primary_index.remove(&pk, link); self.indexes .delete_from_indexes(row, link, inserted_already)?; @@ -360,9 +356,7 @@ where .with_mut_ref(new_link, |r| r.unghost()) .map_err(WorkTableError::PagesError)? } - self.primary_index - .pk_map - .insert(pk.clone(), OffsetEqLink(new_link)); + self.primary_index.insert(pk.clone(), new_link); let indexes_res = self .indexes @@ -373,9 +367,7 @@ where at, inserted_already, } => { - self.primary_index - .pk_map - .insert(pk.clone(), OffsetEqLink(old_link)); + self.primary_index.insert(pk.clone(), old_link); self.indexes .delete_from_indexes(row_new, new_link, inserted_already)?; self.data @@ -441,10 +433,7 @@ where .with_mut_ref(new_link, |r| r.unghost()) .map_err(WorkTableError::PagesError)? } - let (_, primary_key_events) = self - .primary_index - .pk_map - .insert_cdc(pk.clone(), OffsetEqLink(new_link)); + let (_, primary_key_events) = self.primary_index.insert_cdc(pk.clone(), new_link); let primary_key_events = convert_change_events(primary_key_events); let indexes_res = self.indexes @@ -455,9 +444,7 @@ where at, inserted_already, } => { - self.primary_index - .pk_map - .insert(pk.clone(), OffsetEqLink(old_link)); + self.primary_index.insert(pk.clone(), old_link); self.indexes .delete_from_indexes(row_new, new_link, inserted_already)?; self.data diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 38c0fc9..304665e 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -12,6 +12,7 @@ use rkyv::ser::allocator::ArenaHandle; use rkyv::ser::sharing::Share; use rkyv::util::AlignedVec; use rkyv::{Archive, Serialize}; +use std::collections::VecDeque; use std::fmt::Debug; use std::marker::PhantomData; use std::sync::Arc; @@ -88,6 +89,34 @@ where + TableSecondaryIndexCdc, AvailableIndexes: Debug + AvailableIndex, { + async fn defragment(&self) { + let per_page_info = self.data_pages.empty_links_registry().get_per_page_info(); + let mut in_migration_pages = vec![]; + let mut defragmented_pages = VecDeque::new(); + + let mut info_iter = per_page_info.into_iter(); + while let Some(info) = info_iter.next() { + let page_id = info.page_id; + if let Some(id) = defragmented_pages.pop_front() { + self.move_data_from(page_id, id) + } else { + self.defragment_page(info).await; + defragmented_pages.push_back(page_id); + } + } + } + + async fn move_data_from(&self, from: PageId, to: PageId) { + let from_lock = self.vacuum_lock.lock_page(from); + let to_lock = self.vacuum_lock.lock_page(to); + + let to_page = self + .data_pages + .get_page(to) + .expect("should exist as link exists"); + let to_free_space = to_page.free_space(); + } + async fn defragment_page(&self, info: PageFragmentationInfo) { let registry = self.data_pages.empty_links_registry(); let mut page_empty_links = registry @@ -97,7 +126,7 @@ where .collect::>(); page_empty_links.sort_by(|l1, l2| l1.offset.cmp(&l2.offset)); - let _lock = self.vacuum_lock.lock_page(info.page_id); + let lock = self.vacuum_lock.lock_page(info.page_id); let mut empty_links_iter = page_empty_links.into_iter(); let Some(mut current_empty) = empty_links_iter.next() else { @@ -131,18 +160,33 @@ where } } } + + let l = lock.read().await; + l.unlock(); } - pub fn shift_data_in_range(&self, start_link: Link, end_offset: Option) -> u32 { + fn shift_data_in_range(&self, start_link: Link, end_offset: Option) -> u32 { let page_id = start_link.page_id; let page = self .data_pages .get_page(page_id) .expect("should exist as link exists"); let start_link = OffsetEqLink::<_>(start_link); - let mut range_iter = self.primary_index.reverse_pk_map.range(start_link..); - let mut entry_offset = start_link.0.offset; + let range = if let Some(offset) = end_offset { + let end = OffsetEqLink::<_>(Link { + page_id, + offset, + length: 0, + }); + self.primary_index.reverse_pk_map.range(start_link..end) + } else { + self.primary_index.reverse_pk_map.range(start_link..) + } + .map(|(l, pk)| (*l, pk.clone())) + .collect::>(); + let mut range_iter = range.into_iter(); + let mut entry_offset = start_link.0.offset; while let Some((link, pk)) = range_iter.next() { let link_value = link.0; @@ -243,7 +287,7 @@ mod tests { async fn test_vacuum_shift_data_in_range_single_gap() { let table = TestWorkTable::default(); - let mut ids = HashMap::new(); + let mut ids = Vec::new(); for i in 0..10 { let row = TestRow { id: table.get_next_pk().into(), @@ -253,10 +297,10 @@ mod tests { }; let id = row.id; table.insert(row.clone()).unwrap(); - ids.insert(id, row); + ids.push((id, row)); } - let first_two_ids = ids.keys().take(2).cloned().collect::>(); + let first_two_ids = ids.iter().take(2).map(|(i, _)| *i).collect::>(); table.delete(first_two_ids[0].into()).await.unwrap(); table.delete(first_two_ids[1].into()).await.unwrap(); From f6d14486eedd2ff39d06b76236778cf019ccfc63 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:16:58 +0300 Subject: [PATCH 11/32] add tests coverage --- .../src/worktable/generator/queries/delete.rs | 10 +- src/table/vacuum/mod.rs | 225 ++++++++++++++---- 2 files changed, 182 insertions(+), 53 deletions(-) diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index a810ea2..51d47a1 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -76,7 +76,7 @@ impl Generator { let process = if self.is_persist { quote! { let secondary_keys_events = self.0.indexes.delete_row_cdc(row, link)?; - let (_, primary_key_events) = TableIndexCdc::remove_cdc(&self.0.primary_index.pk_map, pk.clone(), link); + let (_, primary_key_events) = TableIndexCdc::remove_cdc(&self.0.primary_index, pk.clone(), link); self.0.data.delete(link).map_err(WorkTableError::PagesError)?; let mut op: Operation< <<#pk_ident as TablePrimaryKey>::Generator as PrimaryKeyGeneratorState>::State, @@ -93,14 +93,15 @@ impl Generator { } else { quote! { self.0.indexes.delete_row(row, link)?; - self.0.primary_index.pk_map.remove(&pk); + self.0.primary_index.remove(&pk, link); self.0.data.delete(link).map_err(WorkTableError::PagesError)?; } }; if is_locked { quote! { let link = match self.0 - .primary_index.pk_map + .primary_index + .pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound) { @@ -117,7 +118,8 @@ impl Generator { } else { quote! { let link = self.0 - .primary_index.pk_map + .primary_index + .pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 304665e..5091f3e 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -92,21 +92,47 @@ where async fn defragment(&self) { let per_page_info = self.data_pages.empty_links_registry().get_per_page_info(); let mut in_migration_pages = vec![]; + let mut free_pages = vec![]; let mut defragmented_pages = VecDeque::new(); let mut info_iter = per_page_info.into_iter(); while let Some(info) = info_iter.next() { let page_id = info.page_id; if let Some(id) = defragmented_pages.pop_front() { - self.move_data_from(page_id, id) + match self.move_data_from(page_id, id).await { + (true, true) => { + // from moved fully and on to no more space + free_pages.push(page_id); + } + (true, false) => { + // from moved fully but to has space + defragmented_pages.push_back(id); + } + (false, true) => { + // from was not moved but to have NO space + in_migration_pages.push(page_id); + } + (false, false) => unreachable!( + "at least one of two situations should appear to break from while cycle" + ), + } } else { self.defragment_page(info).await; defragmented_pages.push_back(page_id); } } + + for in_migration_pages in in_migration_pages { + let page_start = Link { + page_id: in_migration_pages, + offset: 0, + length: 0, + }; + self.shift_data_in_range(page_start, None); + } } - async fn move_data_from(&self, from: PageId, to: PageId) { + async fn move_data_from(&self, from: PageId, to: PageId) -> (bool, bool) { let from_lock = self.vacuum_lock.lock_page(from); let to_lock = self.vacuum_lock.lock_page(to); @@ -114,7 +140,63 @@ where .data_pages .get_page(to) .expect("should exist as link exists"); + let from_page = self + .data_pages + .get_page(from) + .expect("should exist as link exists"); let to_free_space = to_page.free_space(); + + let page_start = OffsetEqLink::<_>(Link { + page_id: from, + offset: 0, + length: 0, + }); + + let mut range = self.primary_index.reverse_pk_map.range(page_start..); + let mut sum_links_len = 0; + let mut links = vec![]; + let mut from_page_will_be_moved = false; + let mut to_page_will_be_filled = false; + + loop { + let Some((next, pk)) = range.next() else { + from_page_will_be_moved = true; + break; + }; + + if sum_links_len + next.length > to_free_space as u32 { + to_page_will_be_filled = true; + if range.next().is_none() { + from_page_will_be_moved = true; + } + break; + } + sum_links_len += next.length; + links.push((*next, pk.clone())); + } + + drop(range); + + for (from_link, pk) in links { + let raw_data = from_page + .get_raw_row(from_link.0) + .expect("link is not bigger than free offset"); + let new_link = to_page + .save_raw_row(&raw_data) + .expect("page is not full as checked on links collection"); + self.update_index_after_move(pk, from_link.0, new_link); + } + + { + let g = from_lock.read().await; + g.unlock() + } + { + let g = to_lock.read().await; + g.unlock() + } + + (from_page_will_be_moved, to_page_will_be_filled) } async fn defragment_page(&self, info: PageFragmentationInfo) { @@ -155,7 +237,12 @@ where next_empty = link; } None => { - self.shift_data_in_range(next_empty, None); + let from = Link { + page_id: next_empty.page_id, + offset, + length: next_empty.length + (next_empty.offset - offset), + }; + self.shift_data_in_range(from, None); break; } } @@ -180,7 +267,12 @@ where }); self.primary_index.reverse_pk_map.range(start_link..end) } else { - self.primary_index.reverse_pk_map.range(start_link..) + let end = OffsetEqLink::<_>(Link { + page_id: page_id.next(), + offset: 0, + length: 0, + }); + self.primary_index.reverse_pk_map.range(start_link..end) } .map(|(l, pk)| (*l, pk.clone())) .collect::>(); @@ -211,9 +303,11 @@ where self.update_index_after_move(pk.clone(), link_value, new_link); } - // Is safe as page is locked now and we can get here only if end_offset - // is not set so we are shifting till page end. - page.free_offset.store(entry_offset, Ordering::Release); + if end_offset.is_none() { + // Is safe as page is locked now and we can get here only if end_offset + // is not set so we are shifting till page end. + page.free_offset.store(entry_offset, Ordering::Release); + } entry_offset } @@ -247,7 +341,7 @@ mod tests { use crate::vacuum::EmptyDataVacuum; use crate::vacuum::lock::VacuumLock; - worktable! ( + worktable!( name: Test, columns: { id: u64 primary_key autoincrement, @@ -306,12 +400,7 @@ mod tests { table.delete(first_two_ids[1].into()).await.unwrap(); let vacuum = create_vacuum(&table); - - let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); - let info = per_page_info - .first() - .expect("at least one page should exist"); - vacuum.defragment_page(*info).await; + vacuum.defragment().await; for (id, expected) in ids.into_iter().skip(2) { let row = table.select(id); @@ -342,12 +431,7 @@ mod tests { table.delete(ids_to_delete[1].into()).await.unwrap(); let vacuum = create_vacuum(&table); - - let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); - let info = per_page_info - .first() - .expect("at least one page should exist"); - vacuum.defragment_page(*info).await; + vacuum.defragment().await; for (id, expected) in ids .into_iter() @@ -381,12 +465,7 @@ mod tests { table.delete(last_two_ids[0].into()).await.unwrap(); let vacuum = create_vacuum(&table); - - let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); - let info = per_page_info - .first() - .expect("at least one page should exist"); - vacuum.defragment_page(*info).await; + vacuum.defragment().await; for (id, expected) in ids .into_iter() @@ -421,12 +500,7 @@ mod tests { } let vacuum = create_vacuum(&table); - - let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); - let info = per_page_info - .first() - .expect("at least one page should exist"); - vacuum.defragment_page(*info).await; + vacuum.defragment().await; for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { let row = table.select(id); @@ -458,12 +532,7 @@ mod tests { } let vacuum = create_vacuum(&table); - - let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); - let info = per_page_info - .first() - .expect("at least one page should exist"); - vacuum.defragment_page(*info).await; + vacuum.defragment().await; let row = table.select(remaining_id); assert_eq!(row, Some(ids[0].1.clone())); @@ -503,12 +572,7 @@ mod tests { } let vacuum = create_vacuum(&table); - - let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); - let info = per_page_info - .first() - .expect("at least one page should exist"); - vacuum.defragment_page(*info).await; + vacuum.defragment().await; for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { let row = table.select(id); @@ -539,11 +603,7 @@ mod tests { } let vacuum = create_vacuum(&table); - let per_page_info = table.0.data.empty_links_registry().get_per_page_info(); - let info = per_page_info - .first() - .expect("at least one page should exist"); - vacuum.defragment_page(*info).await; + vacuum.defragment().await; let mut new_ids = HashMap::new(); for i in 0..3 { @@ -571,4 +631,71 @@ mod tests { assert_eq!(row, Some(expected)); } } + + #[tokio::test] + async fn test_vacuum_multi_page_data_migration() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + // row is ~40 bytes so ~409 rows per page + for i in 0..500 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + let ids_to_delete: Vec<_> = ids.iter().map(|(i, _)| *i).take(20).collect(); + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_multi_page_alternating_deletes() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + // row is ~40 bytes so ~409 rows per page + for i in 0..500 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + let ids_to_delete: Vec<_> = ids.iter().step_by(20).map(|(id, _)| *id).collect(); + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids + .into_iter() + .filter(|(id, _)| !ids_to_delete.contains(id)) + { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } } From 6d8c9c745f1aea21df0a0c68c4db23cde0e93ac0 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:56:30 +0300 Subject: [PATCH 12/32] add more tests and fix move logic --- .../src/worktable/generator/queries/delete.rs | 2 +- src/in_memory/pages.rs | 40 +++-- src/table/vacuum/mod.rs | 155 ++++++++++++++++-- 3 files changed, 174 insertions(+), 23 deletions(-) diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index 51d47a1..3f76431 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -76,7 +76,7 @@ impl Generator { let process = if self.is_persist { quote! { let secondary_keys_events = self.0.indexes.delete_row_cdc(row, link)?; - let (_, primary_key_events) = TableIndexCdc::remove_cdc(&self.0.primary_index, pk.clone(), link); + let (_, primary_key_events) = self.0.primary_index.remove_cdc(pk.clone(), link); self.0.data.delete(link).map_err(WorkTableError::PagesError)?; let mut op: Operation< <<#pk_ident as TablePrimaryKey>::Generator as PrimaryKeyGeneratorState>::State, diff --git a/src/in_memory/pages.rs b/src/in_memory/pages.rs index ffe34e6..081ce9d 100644 --- a/src/in_memory/pages.rs +++ b/src/in_memory/pages.rs @@ -1,9 +1,3 @@ -use std::{ - fmt::Debug, - sync::Arc, - sync::atomic::{AtomicU32, AtomicU64, Ordering}, -}; - use data_bucket::page::PageId; use derive_more::{Display, Error, From}; use parking_lot::RwLock; @@ -16,6 +10,12 @@ use rkyv::{ ser::{Serializer, allocator::ArenaHandle, sharing::Share}, util::AlignedVec, }; +use std::collections::VecDeque; +use std::{ + fmt::Debug, + sync::Arc, + sync::atomic::{AtomicU32, AtomicU64, Ordering}, +}; use crate::in_memory::empty_link_registry::EmptyLinkRegistry; use crate::{ @@ -40,6 +40,8 @@ where empty_links: EmptyLinkRegistry, + empty_pages: Arc>>, + /// Count of saved rows. row_count: AtomicU64, @@ -68,6 +70,7 @@ where // We are starting ID's from `1` because `0`'s page in file is info page. pages: RwLock::new(vec![Arc::new(Data::new(1.into()))]), empty_links: EmptyLinkRegistry::::default(), + empty_pages: Default::default(), row_count: AtomicU64::new(0), last_page_id: AtomicU32::new(1), current_page_id: AtomicU32::new(1), @@ -83,6 +86,7 @@ where Self { pages: RwLock::new(vec), empty_links: EmptyLinkRegistry::default(), + empty_pages: Default::default(), row_count: AtomicU64::new(0), last_page_id: AtomicU32::new(last_page_id as u32), current_page_id: AtomicU32::new(last_page_id as u32), @@ -118,9 +122,7 @@ where // Ok(l) => return Ok(l), Err(e) => match e { DataExecutionError::InvalidLink => { - //println!("Pushing empty link"); self.empty_links.push(link); - //println!("Pushed empty link"); } DataExecutionError::PageIsFull { .. } | DataExecutionError::PageTooSmall { .. } @@ -149,7 +151,15 @@ where if tried_page == page_id_mapper(self.current_page_id.load(Ordering::Relaxed) as usize) { - self.add_next_page(tried_page); + let mut g = self.empty_pages.write(); + if let Some(page_id) = g.pop_front() { + let _pages = self.pages.write(); + self.current_page_id + .store(page_id.into(), Ordering::Release); + } else { + drop(g); + self.add_next_page(tried_page); + } } } DataExecutionError::PageTooSmall { .. } @@ -186,7 +196,7 @@ where let index = self.last_page_id.fetch_add(1, Ordering::AcqRel) + 1; pages.push(Arc::new(Data::new(index.into()))); - self.current_page_id.fetch_add(1, Ordering::AcqRel); + self.current_page_id.store(index, Ordering::Release); } } @@ -332,6 +342,16 @@ where .map_err(ExecutionError::DataPageError) } + pub fn mark_page_empty(&self, page_id: PageId) { + let mut g = self.empty_pages.write(); + g.push_back(page_id); + } + + pub fn get_empty_pages(&self) -> Vec { + let g = self.empty_pages.read(); + g.iter().map(|p| *p).collect() + } + pub fn get_page( &self, page_id: PageId, diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 5091f3e..e2250c4 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -2,6 +2,12 @@ mod fragmentation_info; mod lock; mod page; +use std::collections::VecDeque; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::sync::Arc; +use std::sync::atomic::Ordering; + use data_bucket::Link; use data_bucket::page::PageId; use indexset::core::node::NodeLike; @@ -12,19 +18,12 @@ use rkyv::ser::allocator::ArenaHandle; use rkyv::ser::sharing::Share; use rkyv::util::AlignedVec; use rkyv::{Archive, Serialize}; -use std::collections::VecDeque; -use std::fmt::Debug; -use std::marker::PhantomData; -use std::sync::Arc; -use std::sync::atomic::Ordering; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; use crate::prelude::{OffsetEqLink, TablePrimaryKey}; use crate::vacuum::fragmentation_info::PageFragmentationInfo; use crate::vacuum::lock::VacuumLock; -use crate::{ - AvailableIndex, IndexMap, PrimaryIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc, -}; +use crate::{AvailableIndex, PrimaryIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; #[derive(Debug)] pub struct EmptyDataVacuum< @@ -91,7 +90,7 @@ where { async fn defragment(&self) { let per_page_info = self.data_pages.empty_links_registry().get_per_page_info(); - let mut in_migration_pages = vec![]; + let mut in_migration_pages = VecDeque::new(); let mut free_pages = vec![]; let mut defragmented_pages = VecDeque::new(); @@ -106,19 +105,42 @@ where } (true, false) => { // from moved fully but to has space + free_pages.push(id); defragmented_pages.push_back(id); } (false, true) => { // from was not moved but to have NO space - in_migration_pages.push(page_id); + in_migration_pages.push_back(page_id); } (false, false) => unreachable!( "at least one of two situations should appear to break from while cycle" ), } } else { + let page_id = info.page_id; self.defragment_page(info).await; - defragmented_pages.push_back(page_id); + if let Some(id) = in_migration_pages.pop_front() { + match self.move_data_from(id, page_id).await { + (true, true) => { + // from moved fully and on to no more space + free_pages.push(id); + } + (true, false) => { + // from moved fully but to has space + free_pages.push(id); + defragmented_pages.push_back(page_id); + } + (false, true) => { + // from was not moved but to have NO space + in_migration_pages.push_back(id); + } + (false, false) => unreachable!( + "at least one of two situations should appear to break from while cycle" + ), + } + } else { + defragmented_pages.push_back(page_id); + } } } @@ -130,6 +152,10 @@ where }; self.shift_data_in_range(page_start, None); } + + for id in free_pages { + self.data_pages.mark_page_empty(id) + } } async fn move_data_from(&self, from: PageId, to: PageId) -> (bool, bool) { @@ -152,7 +178,16 @@ where length: 0, }); - let mut range = self.primary_index.reverse_pk_map.range(page_start..); + let page_end = OffsetEqLink::<_>(Link { + page_id: from.next(), + offset: 0, + length: 0, + }); + + let mut range = self + .primary_index + .reverse_pk_map + .range(page_start..page_end); let mut sum_links_len = 0; let mut links = vec![]; let mut from_page_will_be_moved = false; @@ -538,6 +573,34 @@ mod tests { assert_eq!(row, Some(ids[0].1.clone())); } + #[tokio::test] + async fn test_vacuum_defragment_on_delete_last() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + for i in 0..5 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + table.delete(ids.last().unwrap().0.into()).await.unwrap(); + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids.into_iter().take(4) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + #[tokio::test] async fn test_vacuum_shift_data_variable_string_lengths() { let table = TestWorkTable::default(); @@ -698,4 +761,72 @@ mod tests { assert_eq!(row, Some(expected)); } } + + #[tokio::test] + async fn test_vacuum_multi_page_last() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + // row is ~40 bytes so ~409 rows per page + for i in 0..500 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + table.delete(ids.last().unwrap().0.into()).await.unwrap(); + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids.into_iter().take(499) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_multi_page_free_page() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + // row is ~40 bytes so ~409 rows per page + for i in 0..1000 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + let mut ids_to_delete: Vec<_> = ids.iter().skip(300).take(400).map(|(id, _)| *id).collect(); + // remove last too to trigger vacuum for last page too. + ids_to_delete.push(ids.last().unwrap().0); + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + assert!(table.0.data.get_empty_pages().len() > 0); + + for (id, expected) in ids + .into_iter() + .filter(|(id, _)| !ids_to_delete.contains(id)) + { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } } From cd5312e3b82aa98347740ac442dc6f12fd8ac33c Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:50:49 +0300 Subject: [PATCH 13/32] refactor lock logic --- .../src/worktable/generator/queries/delete.rs | 4 +- .../worktable/generator/queries/in_place.rs | 2 +- .../src/worktable/generator/queries/locks.rs | 12 +- .../src/worktable/generator/queries/update.rs | 20 +- src/lock/map.rs | 3 +- src/lock/mod.rs | 6 +- src/lock/row_lock.rs | 6 +- src/lock/table_lock.rs | 216 ++++++++++++++++++ src/table/mod.rs | 6 +- src/table/vacuum/lock.rs | 196 ---------------- src/table/vacuum/mod.rs | 31 ++- 11 files changed, 267 insertions(+), 235 deletions(-) create mode 100644 src/lock/table_lock.rs delete mode 100644 src/table/vacuum/lock.rs diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index 3f76431..7032d53 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -48,7 +48,7 @@ impl Generator { #delete_logic lock.unlock(); // Releases locks - self.0.lock_map.remove_with_lock_check(&pk).await; // Removes locks + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks core::result::Result::Ok(()) } @@ -108,7 +108,7 @@ impl Generator { Ok(l) => l, Err(e) => { lock.unlock(); // Releases locks - self.0.lock_map.remove_with_lock_check(&pk).await; // Removes locks + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks return Err(e); } }; diff --git a/codegen/src/worktable/generator/queries/in_place.rs b/codegen/src/worktable/generator/queries/in_place.rs index 6ed1071..8ce9a2c 100644 --- a/codegen/src/worktable/generator/queries/in_place.rs +++ b/codegen/src/worktable/generator/queries/in_place.rs @@ -126,7 +126,7 @@ impl Generator { }; lock.unlock(); - self.0.lock_map.remove_with_lock_check(&pk).await; + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); Ok(()) } diff --git a/codegen/src/worktable/generator/queries/locks.rs b/codegen/src/worktable/generator/queries/locks.rs index 34ba174..33bfe16 100644 --- a/codegen/src/worktable/generator/queries/locks.rs +++ b/codegen/src/worktable/generator/queries/locks.rs @@ -126,8 +126,8 @@ impl Generator { let lock_ident = name_generator.get_lock_type_ident(); quote! { - let lock_id = self.0.lock_map.next_id(); - if let Some(lock) = self.0.lock_map.get(&pk) { + let lock_id = self.0.lock_manager.row_locks.next_id(); + if let Some(lock) = self.0.lock_manager.row_locks.get(&pk) { let mut lock_guard = lock.write().await; #[allow(clippy::mutable_key_type)] let (locks, op_lock) = lock_guard.lock(lock_id); @@ -140,7 +140,7 @@ impl Generator { let (lock, op_lock) = #lock_ident::with_lock(lock_id); let mut lock = std::sync::Arc::new(tokio::sync::RwLock::new(lock)); let mut guard = lock.write().await; - if let Some(old_lock) = self.0.lock_map.insert(pk.clone(), lock.clone()) { + if let Some(old_lock) = self.0.lock_manager.row_locks.insert(pk.clone(), lock.clone()) { let mut old_lock_guard = old_lock.write().await; #[allow(clippy::mutable_key_type)] let locks = guard.merge(&mut *old_lock_guard); @@ -160,8 +160,8 @@ impl Generator { let lock_ident = name_generator.get_lock_type_ident(); quote! { - let lock_id = self.0.lock_map.next_id(); - if let Some(lock) = self.0.lock_map.get(&pk) { + let lock_id = self.0.lock_manager.row_locks.next_id(); + if let Some(lock) = self.0.lock_manager.row_locks.get(&pk) { let mut lock_guard = lock.write().await; #[allow(clippy::mutable_key_type)] let (locks, op_lock) = lock_guard.#ident(lock_id); @@ -174,7 +174,7 @@ impl Generator { let (_, op_lock) = lock.#ident(lock_id); let lock = std::sync::Arc::new(tokio::sync::RwLock::new(lock)); let mut guard = lock.write().await; - if let Some(old_lock) = self.0.lock_map.insert(pk.clone(), lock.clone()) { + if let Some(old_lock) = self.0.lock_manager.row_locks.insert(pk.clone(), lock.clone()) { let mut old_lock_guard = old_lock.write().await; #[allow(clippy::mutable_key_type)] let locks = guard.merge(&mut *old_lock_guard); diff --git a/codegen/src/worktable/generator/queries/update.rs b/codegen/src/worktable/generator/queries/update.rs index 2695520..88271b5 100644 --- a/codegen/src/worktable/generator/queries/update.rs +++ b/codegen/src/worktable/generator/queries/update.rs @@ -78,7 +78,7 @@ impl Generator { self.0.update_state.remove(&pk); lock.unlock(); - self.0.lock_map.remove_with_lock_check(&pk).await; // Removes locks + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks return core::result::Result::Ok(()); } @@ -100,7 +100,7 @@ impl Generator { Ok(l) => l, Err(e) => { lock.unlock(); - self.0.lock_map.remove_with_lock_check(&pk).await; + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); return Err(e); } @@ -127,7 +127,7 @@ impl Generator { self.0.update_state.remove(&pk); lock.unlock(); // Releases locks - self.0.lock_map.remove_with_lock_check(&pk).await; // Removes locks + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks #persist_call @@ -281,7 +281,7 @@ impl Generator { } lock.unlock(); // Releases locks - self.0.lock_map.remove_with_lock_check(&pk).await; // Removes locks + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks return core::result::Result::Ok(()); } @@ -485,7 +485,7 @@ impl Generator { Ok(l) => l, Err(e) => { lock.unlock(); - self.0.lock_map.remove_with_lock_check(&pk).await; + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); return Err(e); } @@ -506,7 +506,7 @@ impl Generator { #diff_process_remove lock.unlock(); - self.0.lock_map.remove_with_lock_check(&pk).await; + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); #persist_call @@ -582,7 +582,7 @@ impl Generator { } lock.unlock(); // Releases locks - self.0.lock_map.remove_with_lock_check(&pk).await; // Removes locks + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks continue; } else { @@ -649,7 +649,7 @@ impl Generator { } for (pk, lock) in pk_to_unlock { lock.unlock(); - self.0.lock_map.remove_with_lock_check(&pk).await; + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); } core::result::Result::Ok(()) } @@ -725,7 +725,7 @@ impl Generator { Ok(l) => l, Err(e) => { lock.unlock(); - self.0.lock_map.remove_with_lock_check(&pk).await; + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); return Err(e); } @@ -745,7 +745,7 @@ impl Generator { #diff_process_remove lock.unlock(); - self.0.lock_map.remove_with_lock_check(&pk).await; + self.0.lock_manager.row_locks.remove_with_lock_check(&pk); #persist_call diff --git a/src/lock/map.rs b/src/lock/map.rs index c8f88fa..7be0bf7 100644 --- a/src/lock/map.rs +++ b/src/lock/map.rs @@ -43,8 +43,7 @@ where self.map.write().remove(key); } - #[allow(clippy::await_holding_lock)] - pub async fn remove_with_lock_check(&self, key: &PrimaryKey) + pub fn remove_with_lock_check(&self, key: &PrimaryKey) where LockType: RowLock, { diff --git a/src/lock/mod.rs b/src/lock/mod.rs index 959b7a6..a12c66f 100644 --- a/src/lock/mod.rs +++ b/src/lock/mod.rs @@ -1,5 +1,6 @@ mod map; mod row_lock; +mod table_lock; use std::future::Future; use std::hash::{Hash, Hasher}; @@ -13,6 +14,7 @@ use parking_lot::Mutex; pub use map::LockMap; pub use row_lock::{FullRowLock, RowLock}; +pub use table_lock::WorkTableLock; #[derive(Debug)] pub struct Lock { @@ -91,13 +93,13 @@ impl Future for LockWait { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - if !self.locked.load(Ordering::Relaxed) { + if !self.locked.load(Ordering::Acquire) { return Poll::Ready(()); } self.waker.register(cx.waker()); - if self.locked.load(Ordering::Relaxed) { + if self.locked.load(Ordering::Acquire) { Poll::Pending } else { Poll::Ready(()) diff --git a/src/lock/row_lock.rs b/src/lock/row_lock.rs index 7335685..05107e4 100644 --- a/src/lock/row_lock.rs +++ b/src/lock/row_lock.rs @@ -1,4 +1,4 @@ -use crate::lock::Lock; +use crate::lock::{Lock, LockWait}; use std::collections::HashSet; use std::sync::Arc; @@ -31,6 +31,10 @@ impl FullRowLock { pub fn unlock(&self) { self.l.unlock(); } + + pub fn wait(&self) -> LockWait { + self.l.wait() + } } impl RowLock for FullRowLock { diff --git a/src/lock/table_lock.rs b/src/lock/table_lock.rs new file mode 100644 index 0000000..5639880 --- /dev/null +++ b/src/lock/table_lock.rs @@ -0,0 +1,216 @@ +use std::fmt::Debug; +use std::hash::Hash; +use std::sync::Arc; + +use data_bucket::page::PageId; + +use crate::lock::map::LockMap; +use crate::lock::row_lock::{FullRowLock, RowLock}; + +/// Unified lock manager for WorkTable operations. +/// +/// Combines row-level locking with +/// page-level locking (for defragmentation operations). +#[derive(Debug)] +pub struct WorkTableLock { + pub row_locks: LockMap, + pub vacuum_lock: Arc>, +} + +impl Default for WorkTableLock { + fn default() -> Self { + Self { + row_locks: LockMap::default(), + vacuum_lock: Arc::new(LockMap::default()), + } + } +} + +impl WorkTableLock +where + PrimaryKey: Hash + Eq + Debug + Clone, +{ + /// Locks a page for vacuum operations, returning the [`FullRowLock`]. + /// + /// If a lock already exists for the page, it returns the existing lock. + /// Otherwise, creates a new lock. + pub fn lock_page(&self, page_id: PageId) -> Arc> { + if let Some(lock) = self.vacuum_lock.get(&page_id) { + return lock; + } + + let (row_lock, _) = FullRowLock::with_lock(self.vacuum_lock.next_id()); + let lock = Arc::new(tokio::sync::RwLock::new(row_lock)); + self.vacuum_lock.insert(page_id, lock.clone()); + lock + } + + pub fn get_page_lock(&self, page_id: PageId) -> Option>> { + self.vacuum_lock.get(&page_id) + } + + pub fn remove_page_lock(&self, page_id: &PageId) { + self.vacuum_lock.remove_with_lock_check(page_id); + } + + /// Checks if a page is locked by vacuum operations and awaits the lock if it is. + /// + /// This should be called before any operation that accesses data on a specific page. + pub async fn await_page_lock(&self, page_id: PageId) { + if let Some(lock) = self.get_page_lock(page_id) { + let guard = lock.read().await; + let wait = guard.wait(); + drop(guard); + wait.await; + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::time::Duration; + + use tokio::time::timeout; + + use super::*; + + #[tokio::test] + async fn test_default_creates_empty_locks() { + let locks: WorkTableLock<(), String> = WorkTableLock::default(); + + assert!(locks.get_page_lock(1.into()).is_none()); + assert!(locks.get_page_lock(2.into()).is_none()); + } + + #[tokio::test] + async fn test_lock_page_returns_lock() { + let locks: WorkTableLock<(), String> = WorkTableLock::default(); + + let lock = locks.lock_page(5.into()); + + let guard = lock.read().await; + assert!(guard.is_locked()); + } + + #[tokio::test] + async fn test_lock_page_same_id_returns_same_lock() { + let locks: WorkTableLock<(), String> = WorkTableLock::default(); + + let lock1 = locks.lock_page(10.into()); + let lock2 = locks.lock_page(10.into()); + + let ptr1 = Arc::as_ptr(&lock1); + let ptr2 = Arc::as_ptr(&lock2); + assert_eq!(ptr1, ptr2); + } + + #[tokio::test] + async fn test_lock_page_different_ids_returns_different_locks() { + let locks: WorkTableLock<(), String> = WorkTableLock::default(); + + let lock1 = locks.lock_page(1.into()); + let lock2 = locks.lock_page(2.into()); + + let ptr1 = Arc::as_ptr(&lock1); + let ptr2 = Arc::as_ptr(&lock2); + assert_ne!(ptr1, ptr2); + } + + #[tokio::test] + async fn test_get_page_lock_returns_none_when_no_lock() { + let locks: WorkTableLock<(), String> = WorkTableLock::default(); + + assert!(locks.get_page_lock(999.into()).is_none()); + } + + #[tokio::test] + async fn test_get_page_lock_returns_some_when_locked() { + let locks: WorkTableLock<(), String> = WorkTableLock::default(); + + locks.lock_page(42.into()); + let retrieved = locks.get_page_lock(42.into()); + + assert!(retrieved.is_some()); + } + + #[tokio::test] + async fn test_remove_page_lock_removes_unlocked_lock() { + let locks: WorkTableLock<(), String> = WorkTableLock::default(); + + let lock = locks.lock_page(7.into()); + + { + let guard = lock.read().await; + guard.unlock(); + } + + locks.remove_page_lock(&7.into()); + + assert!(locks.get_page_lock(7.into()).is_none()); + } + + #[tokio::test] + async fn test_remove_page_lock_does_not_remove_locked_lock() { + let locks: WorkTableLock<(), String> = WorkTableLock::default(); + + let lock = locks.lock_page(8.into()); + + let _guard = lock.write().await; + + locks.remove_page_lock(&8.into()); + + assert!(locks.get_page_lock(8.into()).is_some()); + } + + #[tokio::test] + async fn test_await_page_lock_returns_immediately_when_no_lock() { + let locks: WorkTableLock<(), String> = WorkTableLock::default(); + + let result = timeout(Duration::from_millis(10), locks.await_page_lock(100.into())).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_await_page_lock_blocks_when_locked() { + let locks = Arc::new(WorkTableLock::<(), String>::default()); + + let lock = locks.lock_page(99.into()); + + let guard = lock.write().await; + + let locks_clone = locks.clone(); + let await_task = tokio::spawn(async move { + locks_clone.await_page_lock(99.into()).await; + }); + + let result = timeout(Duration::from_millis(50), await_task).await; + assert!(result.is_err()); + + guard.unlock(); + drop(guard); + + let result = timeout(Duration::from_millis(100), async move { + locks.await_page_lock(99.into()).await; + }) + .await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_multiple_pages_can_be_locked_independently() { + let locks: WorkTableLock<(), String> = WorkTableLock::default(); + + let page1 = locks.lock_page(1.into()); + let page2 = locks.lock_page(2.into()); + + let lock1 = page1.write().await; + let lock2 = page2.write().await; + + assert!(lock1.is_locked()); + assert!(lock2.is_locked()); + + drop(lock2); + drop(lock1); + } +} diff --git a/src/table/mod.rs b/src/table/mod.rs index 82731bd..4625d41 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -3,7 +3,7 @@ pub mod system_info; pub mod vacuum; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; -use crate::lock::LockMap; +use crate::lock::WorkTableLock; use crate::persistence::{InsertOperation, Operation}; use crate::prelude::{OperationId, PrimaryKeyGeneratorState}; use crate::primary_key::{PrimaryKeyGenerator, TablePrimaryKey}; @@ -54,7 +54,7 @@ pub struct WorkTable< pub pk_gen: PkGen, - pub lock_map: LockMap, + pub lock_manager: Arc>, pub update_state: IndexMap, @@ -100,7 +100,7 @@ where primary_index: Arc::new(PrimaryIndex::default()), indexes: Arc::new(SecondaryIndexes::default()), pk_gen: Default::default(), - lock_map: LockMap::default(), + lock_manager: Default::default(), update_state: IndexMap::default(), table_name: "", pk_phantom: PhantomData, diff --git a/src/table/vacuum/lock.rs b/src/table/vacuum/lock.rs deleted file mode 100644 index 0f8dd74..0000000 --- a/src/table/vacuum/lock.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::sync::Arc; - -use data_bucket::Link; -use data_bucket::page::PageId; - -use crate::lock::{FullRowLock, LockMap, RowLock}; - -/// Lock manager for vacuum operations. -/// Supports locking at both page and link granularity. -#[derive(Debug, Default)] -pub struct VacuumLock { - per_link_lock: Arc>, - per_page_lock: Arc>, -} - -impl VacuumLock { - /// Locks a page, returning the [`FullRowLock`]. - pub fn lock_page(&self, page_id: PageId) -> Arc> { - if let Some(lock) = self.per_page_lock.get(&page_id) { - return lock; - } - - let (row_lock, _) = FullRowLock::with_lock(self.per_page_lock.next_id()); - let lock = Arc::new(tokio::sync::RwLock::new(row_lock)); - self.per_page_lock.insert(page_id, lock.clone()); - lock - } - - /// Locks a [`Link`], returning the [`FullRowLock`]. - pub fn lock_link(&self, link: Link) -> Arc> { - if let Some(lock) = self.per_link_lock.get(&link) { - return lock; - } - - let (row_lock, _lock) = FullRowLock::with_lock(self.per_link_lock.next_id()); - let lock = Arc::new(tokio::sync::RwLock::new(row_lock)); - self.per_link_lock.insert(link, lock.clone()); - lock - } - - /// Checks if a [`Link`] is locked. - /// [`Link`] is locked if it was locked OR its page is locked. - pub fn is_link_locked(&self, link: &Link) -> bool { - if let Some(page_lock) = self.per_page_lock.get(&link.page_id) { - match page_lock.try_read() { - Ok(guard) => { - if guard.is_locked() { - return true; - } - } - Err(_) => return true, // write lock held - } - } - - if let Some(link_lock) = self.per_link_lock.get(link) { - match link_lock.try_read() { - Ok(guard) => { - if guard.is_locked() { - return true; - } - } - Err(_) => return true, // write lock held - } - } - - false - } - - /// Checks if a page is locked. - pub fn is_page_locked(&self, page_id: &PageId) -> bool { - if let Some(page_lock) = self.per_page_lock.get(page_id) { - match page_lock.try_read() { - Ok(guard) => guard.is_locked(), - Err(_) => true, // write lock held - } - } else { - false - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_is_page_locked_not_locked() { - let vacuum_lock = VacuumLock::default(); - let page_id = PageId::from(1); - - assert!(!vacuum_lock.is_page_locked(&page_id)); - } - - #[tokio::test] - async fn test_is_page_locked_after_lock() { - let vacuum_lock = VacuumLock::default(); - let page_id = PageId::from(1); - - let _lock = vacuum_lock.lock_page(page_id); - - assert!(vacuum_lock.is_page_locked(&page_id)); - } - - #[tokio::test] - async fn test_is_page_locked_with_write_lock() { - let vacuum_lock = VacuumLock::default(); - let page_id = PageId::from(1); - - let lock = vacuum_lock.lock_page(page_id); - let _write_guard = lock.write().await; - - assert!(vacuum_lock.is_page_locked(&page_id)); - } - - #[test] - fn test_is_link_locked_not_locked() { - let vacuum_lock = VacuumLock::default(); - let link = Link { - page_id: PageId::from(1), - offset: 0, - length: 100, - }; - - assert!(!vacuum_lock.is_link_locked(&link)); - } - - #[tokio::test] - async fn test_is_link_locked_by_link() { - let vacuum_lock = VacuumLock::default(); - let link = Link { - page_id: PageId::from(1), - offset: 0, - length: 100, - }; - - let _lock = vacuum_lock.lock_link(link); - - assert!(vacuum_lock.is_link_locked(&link)); - } - - #[tokio::test] - async fn test_is_link_locked_by_page() { - let vacuum_lock = VacuumLock::default(); - let link = Link { - page_id: PageId::from(1), - offset: 0, - length: 100, - }; - - let _lock = vacuum_lock.lock_page(link.page_id); - - assert!(vacuum_lock.is_link_locked(&link)); - } - - #[tokio::test] - async fn test_is_link_locked_with_link_write_lock() { - let vacuum_lock = VacuumLock::default(); - let link = Link { - page_id: PageId::from(1), - offset: 0, - length: 100, - }; - - let lock = vacuum_lock.lock_link(link); - let _write_guard = lock.write().await; - - assert!(vacuum_lock.is_link_locked(&link)); - } - - #[tokio::test] - async fn test_lock_page_returns_same_lock() { - let vacuum_lock = VacuumLock::default(); - let page_id = PageId::from(1); - - let lock1 = vacuum_lock.lock_page(page_id); - let lock2 = vacuum_lock.lock_page(page_id); - - // Same pointer = same lock instance - assert!(Arc::ptr_eq(&lock1, &lock2)); - } - - #[tokio::test] - async fn test_lock_link_returns_same_lock() { - let vacuum_lock = VacuumLock::default(); - let link = Link { - page_id: PageId::from(1), - offset: 0, - length: 100, - }; - - let lock1 = vacuum_lock.lock_link(link); - let lock2 = vacuum_lock.lock_link(link); - - assert!(Arc::ptr_eq(&lock1, &lock2)); - } -} diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index e2250c4..1dfa7ca 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -1,5 +1,4 @@ mod fragmentation_info; -mod lock; mod page; use std::collections::VecDeque; @@ -20,9 +19,9 @@ use rkyv::util::AlignedVec; use rkyv::{Archive, Serialize}; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; +use crate::lock::WorkTableLock; use crate::prelude::{OffsetEqLink, TablePrimaryKey}; use crate::vacuum::fragmentation_info::PageFragmentationInfo; -use crate::vacuum::lock::VacuumLock; use crate::{AvailableIndex, PrimaryIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; #[derive(Debug)] @@ -33,6 +32,7 @@ pub struct EmptyDataVacuum< SecondaryIndexes, AvailableTypes, AvailableIndexes, + LockType, const DATA_LENGTH: usize, SecondaryEvents = (), > where @@ -41,7 +41,7 @@ pub struct EmptyDataVacuum< PkNodeType: NodeLike>> + Send + 'static, { data_pages: Arc>, - vacuum_lock: Arc, + lock_manager: Arc>, primary_index: Arc>, secondary_indexes: Arc, @@ -56,6 +56,7 @@ impl< SecondaryIndexes, AvailableTypes, AvailableIndexes, + LockType, const DATA_LENGTH: usize, SecondaryEvents, > @@ -66,6 +67,7 @@ impl< SecondaryIndexes, AvailableTypes, AvailableIndexes, + LockType, DATA_LENGTH, SecondaryEvents, > @@ -159,8 +161,8 @@ where } async fn move_data_from(&self, from: PageId, to: PageId) -> (bool, bool) { - let from_lock = self.vacuum_lock.lock_page(from); - let to_lock = self.vacuum_lock.lock_page(to); + let from_lock = self.lock_manager.lock_page(from); + let to_lock = self.lock_manager.lock_page(to); let to_page = self .data_pages @@ -224,11 +226,13 @@ where { let g = from_lock.read().await; - g.unlock() + g.unlock(); + self.lock_manager.remove_page_lock(&from) } { let g = to_lock.read().await; - g.unlock() + g.unlock(); + self.lock_manager.remove_page_lock(&to) } (from_page_will_be_moved, to_page_will_be_filled) @@ -243,7 +247,7 @@ where .collect::>(); page_empty_links.sort_by(|l1, l2| l1.offset.cmp(&l2.offset)); - let lock = self.vacuum_lock.lock_page(info.page_id); + let lock = self.lock_manager.lock_page(info.page_id); let mut empty_links_iter = page_empty_links.into_iter(); let Some(mut current_empty) = empty_links_iter.next() else { @@ -283,8 +287,11 @@ where } } - let l = lock.read().await; - l.unlock(); + { + let l = lock.read().await; + l.unlock(); + self.lock_manager.remove_page_lock(&info.page_id) + } } fn shift_data_in_range(&self, start_link: Link, end_offset: Option) -> u32 { @@ -374,7 +381,6 @@ mod tests { use crate::in_memory::{GhostWrapper, RowWrapper, StorableRow}; use crate::prelude::*; use crate::vacuum::EmptyDataVacuum; - use crate::vacuum::lock::VacuumLock; worktable!( name: Test, @@ -401,11 +407,12 @@ mod tests { TestIndex, TestAvaiableTypes, TestAvailableIndexes, + TestLock, TEST_INNER_SIZE, > { EmptyDataVacuum { data_pages: Arc::clone(&table.0.data), - vacuum_lock: Arc::new(VacuumLock::default()), + lock_manager: Arc::clone(&table.0.lock_manager), primary_index: Arc::clone(&table.0.primary_index), secondary_indexes: Arc::clone(&table.0.indexes), phantom_data: PhantomData, From dc761a06a83f5dd969ff0ffcc5b603dce64a5851 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:00:30 +0300 Subject: [PATCH 14/32] add vacuum manager --- Cargo.toml | 4 +- .../persist_table/generator/space_file/mod.rs | 5 +- .../src/worktable/generator/table/impls.rs | 19 + src/lib.rs | 7 +- src/table/vacuum/fragmentation_info.rs | 67 +- src/table/vacuum/manager.rs | 46 + src/table/vacuum/mod.rs | 865 +--------------- src/table/vacuum/page.rs | 1 - src/table/vacuum/vacuum.rs | 937 ++++++++++++++++++ 9 files changed, 1103 insertions(+), 848 deletions(-) create mode 100644 src/table/vacuum/manager.rs delete mode 100644 src/table/vacuum/page.rs create mode 100644 src/table/vacuum/vacuum.rs diff --git a/Cargo.toml b/Cargo.toml index afc8875..d5e5754 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,9 @@ perf_measurements = ["dep:performance_measurement", "dep:performance_measurement [dependencies] worktable_codegen = { path = "codegen", version = "=0.8.14" } +async-trait = "0.1.89" eyre = "0.6.12" -derive_more = { version = "2.0.1", features = ["from", "error", "display", "into"] } +derive_more = { version = "2.0.1", features = ["from", "error", "display", "debug", "into"] } tokio = { version = "1", features = ["full"] } tracing = "0.1" rkyv = { version = "0.8.9", features = ["uuid-1"] } @@ -39,6 +40,7 @@ convert_case = "0.6.0" ordered-float = "5.0.0" parking_lot = "0.12.3" prettytable-rs = "^0.10" +smart-default = "0.7.1" [dev-dependencies] rand = "0.9.1" diff --git a/codegen/src/persist_table/generator/space_file/mod.rs b/codegen/src/persist_table/generator/space_file/mod.rs index 1760015..b45c4b2 100644 --- a/codegen/src/persist_table/generator/space_file/mod.rs +++ b/codegen/src/persist_table/generator/space_file/mod.rs @@ -155,6 +155,7 @@ impl Generator { let dir_name = name_generator.get_dir_name(); let const_name = name_generator.get_page_inner_size_const_ident(); let pk_type = name_generator.get_primary_key_type_ident(); + let table_name = name_generator.get_work_table_literal_name(); let primary_index_init = if self.attributes.pk_unsized { let pk_ident = &self.pk_ident; @@ -228,9 +229,9 @@ impl Generator { primary_index: std::sync::Arc::new(primary_index), indexes: std::sync::Arc::new(indexes), pk_gen: PrimaryKeyGeneratorState::from_state(self.data_info.inner.pk_gen_state), - lock_map: LockMap::default(), + lock_manager: std::sync::Arc::new(worktable::lock::WorkTableLock::default()), update_state: IndexMap::default(), - table_name: "", + table_name: #table_name, pk_phantom: std::marker::PhantomData, }; diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index 493e50e..e8b45ae 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -21,6 +21,7 @@ impl Generator { let iter_with_async_fn = self.gen_table_iter_with_async_fn(); let count_fn = self.gen_table_count_fn(); let system_info_fn = self.gen_system_info_fn(); + let vacuum_fn = self.gen_table_vacuum_fn(); quote! { impl #ident { @@ -35,6 +36,7 @@ impl Generator { #iter_with_fn #iter_with_async_fn #system_info_fn + #vacuum_fn } } } @@ -292,4 +294,21 @@ impl Generator { } } } + + fn gen_table_vacuum_fn(&self) -> TokenStream { + let name_generator = WorktableNameGenerator::from_table_name(self.name.to_string()); + let table_name = name_generator.get_work_table_literal_name(); + + quote! { + pub fn vacuum(&self) -> Box { + Box::new(EmptyDataVacuum::new( + #table_name, + std::sync::Arc::clone(&self.0.data), + std::sync::Arc::clone(&self.0.lock_manager), + std::sync::Arc::clone(&self.0.primary_index), + std::sync::Arc::clone(&self.0.indexes), + )) + } + } + } } diff --git a/src/lib.rs b/src/lib.rs index fb2b300..be174b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,11 +9,6 @@ mod mem_stat; mod persistence; mod util; -// mod ty; -// mod value; -// -// pub use column::*; -// pub use field::*; pub use index::*; pub use row::*; pub use table::*; @@ -40,7 +35,7 @@ pub mod prelude { AvailableIndex, Difference, IndexError, IndexMap, IndexMultiMap, MultiPairRecreate, PrimaryIndex, TableIndex, TableIndexCdc, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc, TableSecondaryIndexEventsOps, TableSecondaryIndexInfo, UnsizedNode, - WorkTable, WorkTableError, + WorkTable, WorkTableError, vacuum::EmptyDataVacuum, vacuum::WorkTableVacuum, }; pub use data_bucket::{ DATA_VERSION, DataPage, GENERAL_HEADER_SIZE, GeneralHeader, GeneralPage, INNER_PAGE_SIZE, diff --git a/src/table/vacuum/fragmentation_info.rs b/src/table/vacuum/fragmentation_info.rs index f52106a..dd6d3b2 100644 --- a/src/table/vacuum/fragmentation_info.rs +++ b/src/table/vacuum/fragmentation_info.rs @@ -1,22 +1,74 @@ +//! Fragmentation analysis for vacuum operations. +//! +//! This module provides types and methods for analyzing data page fragmentation +//! in [`WorkTable`]. Fragmentation information is used by the vacuum system to +//! identify pages that need defragmentation or migration. +//! +//! # Overview +//! +//! - [`FragmentationInfo`] - Aggregated fragmentation metrics for an entire table +//! - [`PageFragmentationInfo`] - Per-page fragmentation details +//! - Extension methods on [`EmptyLinkRegistry`] for calculating fragmentation +//! +//! [`WorkTable`]: crate::table::WorkTable + use std::collections::HashMap; -use data_bucket::Link; use data_bucket::page::PageId; +use data_bucket::{INNER_PAGE_SIZE, Link}; use crate::in_memory::EmptyLinkRegistry; -/// Fragmentation info for a single data [`Page`]. +/// Aggregated fragmentation information for a full [`WorkTable`]. +/// +/// [`WorkTable`]: crate::table::WorkTable +#[derive(Debug, Clone)] +pub struct FragmentationInfo { + pub table_name: &'static str, + pub total_pages: usize, + pub page_size: usize, + pub per_page_info: Vec, + pub overall_fragmentation_ratio: f64, + pub total_empty_bytes: u64, +} + +impl FragmentationInfo { + /// Creates new fragmentation info from component parts of + /// [`PageFragmentationInfo`]. + pub fn new( + table_name: &'static str, + total_pages: usize, + per_page_info: Vec, + ) -> Self { + let page_size = per_page_info + .first() + .map(|i| i.page_size) + .unwrap_or(INNER_PAGE_SIZE); + let total_empty_bytes: u64 = per_page_info.iter().map(|i| i.empty_bytes as u64).sum(); + Self { + page_size, + table_name, + total_pages, + per_page_info, + overall_fragmentation_ratio: total_empty_bytes as f64 / page_size as f64, + total_empty_bytes, + } + } +} + +/// Fragmentation information for a single data [`Page`]. /// /// [`Page`]: crate::in_memory::Data #[derive(Debug, Copy, Clone)] -pub struct PageFragmentationInfo { +pub struct PageFragmentationInfo { pub page_id: PageId, + pub page_size: usize, pub empty_bytes: u32, - /// Ratio of filled bytes to empty bytes. Higher means more utilized. pub filled_empty_ratio: f64, } impl EmptyLinkRegistry { + /// Returns all empty [`Link`]s for a specific page. pub fn get_page_empty_links(&self, page_id: PageId) -> Vec { self.page_links_map .get(&page_id) @@ -24,7 +76,9 @@ impl EmptyLinkRegistry { .collect() } - pub fn get_per_page_info(&self) -> Vec> { + /// Calculates [`PageFragmentationInfo`] information for all pages with + /// empty [`Link`]s. + pub fn get_per_page_info(&self) -> Vec { let mut page_empty_bytes: HashMap = HashMap::new(); for (page_id, link) in self.page_links_map.iter() { @@ -32,7 +86,7 @@ impl EmptyLinkRegistry { *entry += link.length; } - let mut per_page_data: Vec> = page_empty_bytes + let mut per_page_data: Vec = page_empty_bytes .into_iter() .map(|(page_id, empty_bytes)| { let filled_empty_ratio = if empty_bytes > 0 { @@ -43,6 +97,7 @@ impl EmptyLinkRegistry { }; PageFragmentationInfo { + page_size: DATA_LENGTH, page_id, empty_bytes, filled_empty_ratio, diff --git a/src/table/vacuum/manager.rs b/src/table/vacuum/manager.rs new file mode 100644 index 0000000..09388bc --- /dev/null +++ b/src/table/vacuum/manager.rs @@ -0,0 +1,46 @@ +use crate::vacuum::WorkTableVacuum; +use parking_lot::RwLock; +use smart_default::SmartDefault; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::AtomicU64; +use std::time::Duration; + +/// Configuration for [`VacuumManager`]. +#[derive(Debug, Clone, SmartDefault)] +pub struct VacuumManagerConfig { + #[default(Duration::from_secs(60))] + pub check_interval: Duration, + #[default(3.0)] + pub low_fragmentation_threshold: f64, + #[default(1.5)] + pub normal_fragmentation_threshold: f64, + #[default(1.0)] + pub high_fragmentation_threshold: f64, + #[default(0.7)] + pub critical_fragmentation_threshold: f64, +} + +#[derive(derive_more::Debug)] +pub struct VacuumManager { + pub config: VacuumManagerConfig, + pub id_gen: AtomicU64, + #[debug(ignore)] + pub vacuums: Arc>>>, +} + +impl VacuumManager { + /// Creates a new vacuum manager with default configuration. + pub fn new() -> Self { + Self::with_config(VacuumManagerConfig::default()) + } + + /// Creates a new vacuum manager with the given configuration. + pub fn with_config(config: VacuumManagerConfig) -> Self { + Self { + config, + id_gen: Default::default(), + vacuums: Arc::default(), + } + } +} diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 1dfa7ca..42d728f 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -1,839 +1,40 @@ -mod fragmentation_info; -mod page; - -use std::collections::VecDeque; -use std::fmt::Debug; -use std::marker::PhantomData; -use std::sync::Arc; -use std::sync::atomic::Ordering; - -use data_bucket::Link; -use data_bucket::page::PageId; -use indexset::core::node::NodeLike; -use indexset::core::pair::Pair; -use rkyv::rancor::Strategy; -use rkyv::ser::Serializer; -use rkyv::ser::allocator::ArenaHandle; -use rkyv::ser::sharing::Share; -use rkyv::util::AlignedVec; -use rkyv::{Archive, Serialize}; - -use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; -use crate::lock::WorkTableLock; -use crate::prelude::{OffsetEqLink, TablePrimaryKey}; -use crate::vacuum::fragmentation_info::PageFragmentationInfo; -use crate::{AvailableIndex, PrimaryIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; +use async_trait::async_trait; -#[derive(Debug)] -pub struct EmptyDataVacuum< - Row, - PrimaryKey, - PkNodeType, - SecondaryIndexes, - AvailableTypes, - AvailableIndexes, - LockType, - const DATA_LENGTH: usize, - SecondaryEvents = (), -> where - PrimaryKey: Clone + Ord + Send + 'static + std::hash::Hash, - Row: StorableRow + Send + Clone + 'static, - PkNodeType: NodeLike>> + Send + 'static, -{ - data_pages: Arc>, - lock_manager: Arc>, +use crate::vacuum::fragmentation_info::FragmentationInfo; - primary_index: Arc>, - secondary_indexes: Arc, - - phantom_data: PhantomData<(SecondaryEvents, AvailableTypes, AvailableIndexes)>, +mod fragmentation_info; +mod manager; +mod vacuum; + +pub use vacuum::EmptyDataVacuum; + +/// Trait for unifying different [`WorkTable`] related [`EmptyDataVacuum`]'s. +/// +/// [`WorkTable`]: crate::prelude::WorkTable +/// [`EmptyDataVacuum`]: vacuum::EmptyDataVacuum +#[async_trait] +pub trait WorkTableVacuum { + /// Get table name for diagnostics + fn table_name(&self) -> &str; + /// Analyze current fragmentation state + fn analyze_fragmentation(&self) -> FragmentationInfo; + /// Run vacuum operation + async fn vacuum(&self) -> eyre::Result; } -impl< - Row, - PrimaryKey, - PkNodeType, - SecondaryIndexes, - AvailableTypes, - AvailableIndexes, - LockType, - const DATA_LENGTH: usize, - SecondaryEvents, -> - EmptyDataVacuum< - Row, - PrimaryKey, - PkNodeType, - SecondaryIndexes, - AvailableTypes, - AvailableIndexes, - LockType, - DATA_LENGTH, - SecondaryEvents, - > -where - Row: TableRow + StorableRow + Send + Clone + 'static, - PrimaryKey: Debug + Clone + Ord + Send + TablePrimaryKey + std::hash::Hash, - PkNodeType: NodeLike>> + Send + 'static, - ::WrappedRow: RowWrapper, - Row: Archive - + Clone - + for<'a> Serialize< - Strategy, Share>, rkyv::rancor::Error>, - >, - ::WrappedRow: Archive - + for<'a> Serialize< - Strategy, Share>, rkyv::rancor::Error>, - >, - <::WrappedRow as Archive>::Archived: GhostWrapper, - SecondaryIndexes: TableSecondaryIndex - + TableSecondaryIndexCdc, - AvailableIndexes: Debug + AvailableIndex, -{ - async fn defragment(&self) { - let per_page_info = self.data_pages.empty_links_registry().get_per_page_info(); - let mut in_migration_pages = VecDeque::new(); - let mut free_pages = vec![]; - let mut defragmented_pages = VecDeque::new(); - - let mut info_iter = per_page_info.into_iter(); - while let Some(info) = info_iter.next() { - let page_id = info.page_id; - if let Some(id) = defragmented_pages.pop_front() { - match self.move_data_from(page_id, id).await { - (true, true) => { - // from moved fully and on to no more space - free_pages.push(page_id); - } - (true, false) => { - // from moved fully but to has space - free_pages.push(id); - defragmented_pages.push_back(id); - } - (false, true) => { - // from was not moved but to have NO space - in_migration_pages.push_back(page_id); - } - (false, false) => unreachable!( - "at least one of two situations should appear to break from while cycle" - ), - } - } else { - let page_id = info.page_id; - self.defragment_page(info).await; - if let Some(id) = in_migration_pages.pop_front() { - match self.move_data_from(id, page_id).await { - (true, true) => { - // from moved fully and on to no more space - free_pages.push(id); - } - (true, false) => { - // from moved fully but to has space - free_pages.push(id); - defragmented_pages.push_back(page_id); - } - (false, true) => { - // from was not moved but to have NO space - in_migration_pages.push_back(id); - } - (false, false) => unreachable!( - "at least one of two situations should appear to break from while cycle" - ), - } - } else { - defragmented_pages.push_back(page_id); - } - } - } - - for in_migration_pages in in_migration_pages { - let page_start = Link { - page_id: in_migration_pages, - offset: 0, - length: 0, - }; - self.shift_data_in_range(page_start, None); - } - - for id in free_pages { - self.data_pages.mark_page_empty(id) - } - } - - async fn move_data_from(&self, from: PageId, to: PageId) -> (bool, bool) { - let from_lock = self.lock_manager.lock_page(from); - let to_lock = self.lock_manager.lock_page(to); - - let to_page = self - .data_pages - .get_page(to) - .expect("should exist as link exists"); - let from_page = self - .data_pages - .get_page(from) - .expect("should exist as link exists"); - let to_free_space = to_page.free_space(); - - let page_start = OffsetEqLink::<_>(Link { - page_id: from, - offset: 0, - length: 0, - }); - - let page_end = OffsetEqLink::<_>(Link { - page_id: from.next(), - offset: 0, - length: 0, - }); - - let mut range = self - .primary_index - .reverse_pk_map - .range(page_start..page_end); - let mut sum_links_len = 0; - let mut links = vec![]; - let mut from_page_will_be_moved = false; - let mut to_page_will_be_filled = false; - - loop { - let Some((next, pk)) = range.next() else { - from_page_will_be_moved = true; - break; - }; - - if sum_links_len + next.length > to_free_space as u32 { - to_page_will_be_filled = true; - if range.next().is_none() { - from_page_will_be_moved = true; - } - break; - } - sum_links_len += next.length; - links.push((*next, pk.clone())); - } - - drop(range); - - for (from_link, pk) in links { - let raw_data = from_page - .get_raw_row(from_link.0) - .expect("link is not bigger than free offset"); - let new_link = to_page - .save_raw_row(&raw_data) - .expect("page is not full as checked on links collection"); - self.update_index_after_move(pk, from_link.0, new_link); - } - - { - let g = from_lock.read().await; - g.unlock(); - self.lock_manager.remove_page_lock(&from) - } - { - let g = to_lock.read().await; - g.unlock(); - self.lock_manager.remove_page_lock(&to) - } - - (from_page_will_be_moved, to_page_will_be_filled) - } - - async fn defragment_page(&self, info: PageFragmentationInfo) { - let registry = self.data_pages.empty_links_registry(); - let mut page_empty_links = registry - .page_links_map - .get(&info.page_id) - .map(|(_, l)| *l) - .collect::>(); - page_empty_links.sort_by(|l1, l2| l1.offset.cmp(&l2.offset)); - - let lock = self.lock_manager.lock_page(info.page_id); - let mut empty_links_iter = page_empty_links.into_iter(); - - let Some(mut current_empty) = empty_links_iter.next() else { - return; - }; - registry.remove_link(current_empty); - - let Some(mut next_empty) = empty_links_iter.next() else { - self.shift_data_in_range(current_empty, None); - return; - }; - registry.remove_link(next_empty); - - loop { - let offset = self.shift_data_in_range(current_empty, Some(next_empty.offset)); - - let new_next = empty_links_iter.next(); - match new_next { - Some(link) => { - registry.remove_link(link); - current_empty = Link { - page_id: next_empty.page_id, - offset, - length: next_empty.length + (next_empty.offset - offset), - }; - next_empty = link; - } - None => { - let from = Link { - page_id: next_empty.page_id, - offset, - length: next_empty.length + (next_empty.offset - offset), - }; - self.shift_data_in_range(from, None); - break; - } - } - } - - { - let l = lock.read().await; - l.unlock(); - self.lock_manager.remove_page_lock(&info.page_id) - } - } - - fn shift_data_in_range(&self, start_link: Link, end_offset: Option) -> u32 { - let page_id = start_link.page_id; - let page = self - .data_pages - .get_page(page_id) - .expect("should exist as link exists"); - let start_link = OffsetEqLink::<_>(start_link); - let range = if let Some(offset) = end_offset { - let end = OffsetEqLink::<_>(Link { - page_id, - offset, - length: 0, - }); - self.primary_index.reverse_pk_map.range(start_link..end) - } else { - let end = OffsetEqLink::<_>(Link { - page_id: page_id.next(), - offset: 0, - length: 0, - }); - self.primary_index.reverse_pk_map.range(start_link..end) - } - .map(|(l, pk)| (*l, pk.clone())) - .collect::>(); - let mut range_iter = range.into_iter(); - - let mut entry_offset = start_link.0.offset; - while let Some((link, pk)) = range_iter.next() { - let link_value = link.0; - - if let Some(end) = end_offset { - if entry_offset + link_value.length >= end { - return entry_offset; - } - } - - let new_link = Link { - page_id, - offset: entry_offset, - length: link_value.length, - }; - - // TODO: Safety comment - unsafe { - page.move_from_to(link_value, new_link) - .expect("should use valid links") - } - entry_offset += link_value.length; - self.update_index_after_move(pk.clone(), link_value, new_link); - } - - if end_offset.is_none() { - // Is safe as page is locked now and we can get here only if end_offset - // is not set so we are shifting till page end. - page.free_offset.store(entry_offset, Ordering::Release); - } - - entry_offset - } - - fn update_index_after_move(&self, pk: PrimaryKey, old_link: Link, new_link: Link) { - let old_offset_link = OffsetEqLink(old_link); - let new_offset_link = OffsetEqLink(new_link); - - self.primary_index - .pk_map - .insert(pk.clone(), new_offset_link); - self.primary_index.reverse_pk_map.remove(&old_offset_link); - self.primary_index - .reverse_pk_map - .insert(new_offset_link, pk); - // TODO: update secondary indexes - } +/// Represents vacuum statistics after a vacuum operation +#[derive(Debug, Clone)] +pub struct VacuumStats { + pub pages_processed: usize, + pub pages_freed: usize, + pub bytes_freed: u64, + pub duration_ns: u128, } -#[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::marker::PhantomData; - use std::sync::Arc; - - use indexset::core::pair::Pair; - use worktable_codegen::{MemStat, worktable}; - - use crate::in_memory::{GhostWrapper, RowWrapper, StorableRow}; - use crate::prelude::*; - use crate::vacuum::EmptyDataVacuum; - - worktable!( - name: Test, - columns: { - id: u64 primary_key autoincrement, - test: i64, - another: u64, - exchange: String - }, - indexes: { - test_idx: test unique, - exchnage_idx: exchange, - another_idx: another, - } - ); - - /// Creates an EmptyDataVacuum instance from a WorkTable - fn create_vacuum( - table: &TestWorkTable, - ) -> EmptyDataVacuum< - TestRow, - TestPrimaryKey, - Vec>>, - TestIndex, - TestAvaiableTypes, - TestAvailableIndexes, - TestLock, - TEST_INNER_SIZE, - > { - EmptyDataVacuum { - data_pages: Arc::clone(&table.0.data), - lock_manager: Arc::clone(&table.0.lock_manager), - primary_index: Arc::clone(&table.0.primary_index), - secondary_indexes: Arc::clone(&table.0.indexes), - phantom_data: PhantomData, - } - } - - #[tokio::test] - async fn test_vacuum_shift_data_in_range_single_gap() { - let table = TestWorkTable::default(); - - let mut ids = Vec::new(); - for i in 0..10 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i, - another: i as u64, - exchange: format!("test{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.push((id, row)); - } - - let first_two_ids = ids.iter().take(2).map(|(i, _)| *i).collect::>(); - - table.delete(first_two_ids[0].into()).await.unwrap(); - table.delete(first_two_ids[1].into()).await.unwrap(); - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - for (id, expected) in ids.into_iter().skip(2) { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } - - #[tokio::test] - async fn test_vacuum_shift_data_middle_gap() { - let table = TestWorkTable::default(); - - let mut ids = HashMap::new(); - for i in 0..20 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i * 10, - another: i as u64, - exchange: format!("test{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.insert(id, row); - } - - let ids_to_delete = ids.keys().skip(5).take(2).cloned().collect::>(); - - table.delete(ids_to_delete[0].into()).await.unwrap(); - table.delete(ids_to_delete[1].into()).await.unwrap(); - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - for (id, expected) in ids - .into_iter() - .filter(|(i, _)| *i != ids_to_delete[0] && *i != ids_to_delete[1]) - { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } - - #[tokio::test] - async fn test_vacuum_shift_data_last_records() { - let table = TestWorkTable::default(); - - let mut ids = HashMap::new(); - for i in 0..10 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i, - another: i as u64, - exchange: format!("test{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.insert(id, row); - } - - let last_two_ids = ids.keys().skip(8).take(2).cloned().collect::>(); - - table.delete(last_two_ids[1].into()).await.unwrap(); - table.delete(last_two_ids[0].into()).await.unwrap(); - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - for (id, expected) in ids - .into_iter() - .filter(|(i, _)| *i != last_two_ids[0] && *i != last_two_ids[1]) - { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } - - #[tokio::test] - async fn test_vacuum_shift_data_multiple_gaps() { - let table = TestWorkTable::default(); - - let mut ids = HashMap::new(); - for i in 0..15 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i, - another: i as u64, - exchange: format!("test{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.insert(id, row); - } - - let ids_to_delete = [1, 3, 5, 7].map(|idx| ids.keys().cloned().nth(idx).unwrap()); - - for id in &ids_to_delete { - table.delete((*id).into()).await.unwrap(); - } - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } - - #[tokio::test] - async fn test_vacuum_shift_data_single_record_left() { - let table = TestWorkTable::default(); - - let mut ids = Vec::new(); - for i in 0..5 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i, - another: i as u64, - exchange: format!("test{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.push((id, row)); - } - - let remaining_id = ids[0].0; - - for (id, _) in ids.iter().skip(1) { - table.delete((*id).into()).await.unwrap(); - } - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - let row = table.select(remaining_id); - assert_eq!(row, Some(ids[0].1.clone())); - } - - #[tokio::test] - async fn test_vacuum_defragment_on_delete_last() { - let table = TestWorkTable::default(); - - let mut ids = Vec::new(); - for i in 0..5 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i, - another: i as u64, - exchange: format!("test{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.push((id, row)); - } - - table.delete(ids.last().unwrap().0.into()).await.unwrap(); - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - for (id, expected) in ids.into_iter().take(4) { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } - - #[tokio::test] - async fn test_vacuum_shift_data_variable_string_lengths() { - let table = TestWorkTable::default(); - - let mut ids = HashMap::new(); - let strings = vec![ - "a", - "bbbb", - "cccccc", - "dddddddd", - "eeeeeeeeee", - "ffffffffffff", - "gggggggggggggg", - ]; - - for (i, s) in strings.iter().enumerate() { - let row = TestRow { - id: table.get_next_pk().into(), - test: i as i64, - another: i as u64, - exchange: s.to_string(), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.insert(id, row); - } - - let ids_to_delete = ids.keys().take(3).cloned().collect::>(); - - for id in &ids_to_delete { - table.delete((*id).into()).await.unwrap(); - } - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } - - #[tokio::test] - async fn test_vacuum_insert_after_free_offset_update() { - let table = TestWorkTable::default(); - - let mut original_ids = HashMap::new(); - for i in 0..8 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i, - another: i as u64, - exchange: format!("original{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - original_ids.insert(id, row); - } - - let ids_to_delete = original_ids.keys().take(3).cloned().collect::>(); - for id in &ids_to_delete { - table.delete((*id).into()).await.unwrap(); - } - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - let mut new_ids = HashMap::new(); - for i in 0..3 { - let row = TestRow { - id: table.get_next_pk().into(), - test: 100 + i, - another: (100 + i) as u64, - exchange: format!("new{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - new_ids.insert(id, row); - } - - for (id, expected) in original_ids - .into_iter() - .filter(|(i, _)| !ids_to_delete.contains(i)) - { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - - for (id, expected) in new_ids { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } - - #[tokio::test] - async fn test_vacuum_multi_page_data_migration() { - let table = TestWorkTable::default(); - - let mut ids = Vec::new(); - // row is ~40 bytes so ~409 rows per page - for i in 0..500 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i, - another: i as u64, - exchange: format!("test{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.push((id, row)); - } - - let ids_to_delete: Vec<_> = ids.iter().map(|(i, _)| *i).take(20).collect(); - for id in &ids_to_delete { - table.delete((*id).into()).await.unwrap(); - } - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } - - #[tokio::test] - async fn test_vacuum_multi_page_alternating_deletes() { - let table = TestWorkTable::default(); - - let mut ids = Vec::new(); - // row is ~40 bytes so ~409 rows per page - for i in 0..500 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i, - another: i as u64, - exchange: format!("test{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.push((id, row)); - } - - let ids_to_delete: Vec<_> = ids.iter().step_by(20).map(|(id, _)| *id).collect(); - for id in &ids_to_delete { - table.delete((*id).into()).await.unwrap(); - } - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - for (id, expected) in ids - .into_iter() - .filter(|(id, _)| !ids_to_delete.contains(id)) - { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } - - #[tokio::test] - async fn test_vacuum_multi_page_last() { - let table = TestWorkTable::default(); - - let mut ids = Vec::new(); - // row is ~40 bytes so ~409 rows per page - for i in 0..500 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i, - another: i as u64, - exchange: format!("test{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.push((id, row)); - } - - table.delete(ids.last().unwrap().0.into()).await.unwrap(); - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - for (id, expected) in ids.into_iter().take(499) { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } - - #[tokio::test] - async fn test_vacuum_multi_page_free_page() { - let table = TestWorkTable::default(); - - let mut ids = Vec::new(); - // row is ~40 bytes so ~409 rows per page - for i in 0..1000 { - let row = TestRow { - id: table.get_next_pk().into(), - test: i, - another: i as u64, - exchange: format!("test{}", i), - }; - let id = row.id; - table.insert(row.clone()).unwrap(); - ids.push((id, row)); - } - - let mut ids_to_delete: Vec<_> = ids.iter().skip(300).take(400).map(|(id, _)| *id).collect(); - // remove last too to trigger vacuum for last page too. - ids_to_delete.push(ids.last().unwrap().0); - for id in &ids_to_delete { - table.delete((*id).into()).await.unwrap(); - } - - let vacuum = create_vacuum(&table); - vacuum.defragment().await; - - assert!(table.0.data.get_empty_pages().len() > 0); - - for (id, expected) in ids - .into_iter() - .filter(|(id, _)| !ids_to_delete.contains(id)) - { - let row = table.select(id); - assert_eq!(row, Some(expected)); - } - } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum VacuumPriority { + Low = 0, + Normal = 1, + High = 2, + Critical = 3, } diff --git a/src/table/vacuum/page.rs b/src/table/vacuum/page.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/table/vacuum/page.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/table/vacuum/vacuum.rs b/src/table/vacuum/vacuum.rs new file mode 100644 index 0000000..146191a --- /dev/null +++ b/src/table/vacuum/vacuum.rs @@ -0,0 +1,937 @@ +use std::collections::VecDeque; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use std::time::Instant; + +use data_bucket::Link; +use data_bucket::page::PageId; +use indexset::core::node::NodeLike; +use indexset::core::pair::Pair; +use rkyv::rancor::Strategy; +use rkyv::ser::Serializer; +use rkyv::ser::allocator::ArenaHandle; +use rkyv::ser::sharing::Share; +use rkyv::util::AlignedVec; +use rkyv::{Archive, Serialize}; + +use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; +use crate::lock::WorkTableLock; +use crate::prelude::{OffsetEqLink, TablePrimaryKey}; +use async_trait::async_trait; + +use crate::vacuum::VacuumStats; +use crate::vacuum::WorkTableVacuum; +use crate::vacuum::fragmentation_info::{FragmentationInfo, PageFragmentationInfo}; +use crate::{AvailableIndex, PrimaryIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; + +#[derive(Debug)] +pub struct EmptyDataVacuum< + Row, + PrimaryKey, + PkNodeType, + SecondaryIndexes, + AvailableTypes, + AvailableIndexes, + LockType, + const DATA_LENGTH: usize, + SecondaryEvents = (), +> where + PrimaryKey: Clone + Ord + Send + 'static + std::hash::Hash, + Row: StorableRow + Send + Clone + 'static, + PkNodeType: NodeLike>> + Send + 'static, +{ + table_name: &'static str, + + data_pages: Arc>, + lock_manager: Arc>, + + primary_index: Arc>, + secondary_indexes: Arc, + + phantom_data: PhantomData<(SecondaryEvents, AvailableTypes, AvailableIndexes)>, +} + +impl< + Row, + PrimaryKey, + PkNodeType, + SecondaryIndexes, + AvailableTypes, + AvailableIndexes, + LockType, + const DATA_LENGTH: usize, + SecondaryEvents, +> + EmptyDataVacuum< + Row, + PrimaryKey, + PkNodeType, + SecondaryIndexes, + AvailableTypes, + AvailableIndexes, + LockType, + DATA_LENGTH, + SecondaryEvents, + > +where + Row: TableRow + StorableRow + Send + Clone + 'static, + PrimaryKey: Debug + Clone + Ord + Send + TablePrimaryKey + std::hash::Hash, + PkNodeType: NodeLike>> + Send + 'static, + ::WrappedRow: RowWrapper, + Row: Archive + + Clone + + for<'a> Serialize< + Strategy, Share>, rkyv::rancor::Error>, + >, + ::WrappedRow: Archive + + for<'a> Serialize< + Strategy, Share>, rkyv::rancor::Error>, + >, + <::WrappedRow as Archive>::Archived: GhostWrapper, + SecondaryIndexes: TableSecondaryIndex + + TableSecondaryIndexCdc, + AvailableIndexes: Debug + AvailableIndex, +{ + async fn defragment(&self) -> VacuumStats { + let now = Instant::now(); + + let per_page_info = self.data_pages.empty_links_registry().get_per_page_info(); + let initial_bytes_freed: u64 = per_page_info.iter().map(|i| i.empty_bytes as u64).sum(); + + let mut in_migration_pages = VecDeque::new(); + let mut free_pages = vec![]; + let mut defragmented_pages = VecDeque::new(); + + let pages_processed = per_page_info.len(); + + let mut info_iter = per_page_info.into_iter(); + while let Some(info) = info_iter.next() { + let page_id = info.page_id; + if let Some(id) = defragmented_pages.pop_front() { + match self.move_data_from(page_id, id).await { + (true, true) => { + // from moved fully and on to no more space + free_pages.push(page_id); + } + (true, false) => { + // from moved fully but to has space + free_pages.push(id); + defragmented_pages.push_back(id); + } + (false, true) => { + // from was not moved but to have NO space + in_migration_pages.push_back(page_id); + } + (false, false) => unreachable!( + "at least one of two situations should appear to break from while cycle" + ), + } + } else { + let page_id = info.page_id; + self.defragment_page(info).await; + if let Some(id) = in_migration_pages.pop_front() { + match self.move_data_from(id, page_id).await { + (true, true) => { + // from moved fully and on to no more space + free_pages.push(id); + } + (true, false) => { + // from moved fully but to has space + free_pages.push(id); + defragmented_pages.push_back(page_id); + } + (false, true) => { + // from was not moved but to have NO space + in_migration_pages.push_back(id); + } + (false, false) => unreachable!( + "at least one of two situations should appear to break from while cycle" + ), + } + } else { + defragmented_pages.push_back(page_id); + } + } + } + + for in_migration_pages in in_migration_pages { + let page_start = Link { + page_id: in_migration_pages, + offset: 0, + length: 0, + }; + self.shift_data_in_range(page_start, None); + } + + let pages_freed = free_pages.len(); + for id in free_pages { + self.data_pages.mark_page_empty(id) + } + + VacuumStats { + pages_processed, + pages_freed, + bytes_freed: initial_bytes_freed, + duration_ns: now.elapsed().as_nanos(), + } + } + + async fn move_data_from(&self, from: PageId, to: PageId) -> (bool, bool) { + let from_lock = self.lock_manager.lock_page(from); + let to_lock = self.lock_manager.lock_page(to); + + let to_page = self + .data_pages + .get_page(to) + .expect("should exist as link exists"); + let from_page = self + .data_pages + .get_page(from) + .expect("should exist as link exists"); + let to_free_space = to_page.free_space(); + + let page_start = OffsetEqLink::<_>(Link { + page_id: from, + offset: 0, + length: 0, + }); + + let page_end = OffsetEqLink::<_>(Link { + page_id: from.next(), + offset: 0, + length: 0, + }); + + let mut range = self + .primary_index + .reverse_pk_map + .range(page_start..page_end); + let mut sum_links_len = 0; + let mut links = vec![]; + let mut from_page_will_be_moved = false; + let mut to_page_will_be_filled = false; + + loop { + let Some((next, pk)) = range.next() else { + from_page_will_be_moved = true; + break; + }; + + if sum_links_len + next.length > to_free_space as u32 { + to_page_will_be_filled = true; + if range.next().is_none() { + from_page_will_be_moved = true; + } + break; + } + sum_links_len += next.length; + links.push((*next, pk.clone())); + } + + drop(range); + + for (from_link, pk) in links { + let raw_data = from_page + .get_raw_row(from_link.0) + .expect("link is not bigger than free offset"); + let new_link = to_page + .save_raw_row(&raw_data) + .expect("page is not full as checked on links collection"); + self.update_index_after_move(pk, from_link.0, new_link); + } + + { + let g = from_lock.read().await; + g.unlock(); + self.lock_manager.remove_page_lock(&from) + } + { + let g = to_lock.read().await; + g.unlock(); + self.lock_manager.remove_page_lock(&to) + } + + (from_page_will_be_moved, to_page_will_be_filled) + } + + async fn defragment_page(&self, info: PageFragmentationInfo) { + let registry = self.data_pages.empty_links_registry(); + let mut page_empty_links = registry + .page_links_map + .get(&info.page_id) + .map(|(_, l)| *l) + .collect::>(); + page_empty_links.sort_by(|l1, l2| l1.offset.cmp(&l2.offset)); + + let lock = self.lock_manager.lock_page(info.page_id); + let mut empty_links_iter = page_empty_links.into_iter(); + + let Some(mut current_empty) = empty_links_iter.next() else { + return; + }; + registry.remove_link(current_empty); + + let Some(mut next_empty) = empty_links_iter.next() else { + self.shift_data_in_range(current_empty, None); + return; + }; + registry.remove_link(next_empty); + + loop { + let offset = self.shift_data_in_range(current_empty, Some(next_empty.offset)); + + let new_next = empty_links_iter.next(); + match new_next { + Some(link) => { + registry.remove_link(link); + current_empty = Link { + page_id: next_empty.page_id, + offset, + length: next_empty.length + (next_empty.offset - offset), + }; + next_empty = link; + } + None => { + let from = Link { + page_id: next_empty.page_id, + offset, + length: next_empty.length + (next_empty.offset - offset), + }; + self.shift_data_in_range(from, None); + break; + } + } + } + + { + let l = lock.read().await; + l.unlock(); + self.lock_manager.remove_page_lock(&info.page_id) + } + } + + fn shift_data_in_range(&self, start_link: Link, end_offset: Option) -> u32 { + let page_id = start_link.page_id; + let page = self + .data_pages + .get_page(page_id) + .expect("should exist as link exists"); + let start_link = OffsetEqLink::<_>(start_link); + let range = if let Some(offset) = end_offset { + let end = OffsetEqLink::<_>(Link { + page_id, + offset, + length: 0, + }); + self.primary_index.reverse_pk_map.range(start_link..end) + } else { + let end = OffsetEqLink::<_>(Link { + page_id: page_id.next(), + offset: 0, + length: 0, + }); + self.primary_index.reverse_pk_map.range(start_link..end) + } + .map(|(l, pk)| (*l, pk.clone())) + .collect::>(); + let mut range_iter = range.into_iter(); + + let mut entry_offset = start_link.0.offset; + while let Some((link, pk)) = range_iter.next() { + let link_value = link.0; + + if let Some(end) = end_offset { + if entry_offset + link_value.length >= end { + return entry_offset; + } + } + + let new_link = Link { + page_id, + offset: entry_offset, + length: link_value.length, + }; + + // TODO: Safety comment + unsafe { + page.move_from_to(link_value, new_link) + .expect("should use valid links") + } + entry_offset += link_value.length; + self.update_index_after_move(pk.clone(), link_value, new_link); + } + + if end_offset.is_none() { + // Is safe as page is locked now and we can get here only if end_offset + // is not set so we are shifting till page end. + page.free_offset.store(entry_offset, Ordering::Release); + } + + entry_offset + } + + fn update_index_after_move(&self, pk: PrimaryKey, old_link: Link, new_link: Link) { + let old_offset_link = OffsetEqLink(old_link); + let new_offset_link = OffsetEqLink(new_link); + + self.primary_index + .pk_map + .insert(pk.clone(), new_offset_link); + self.primary_index.reverse_pk_map.remove(&old_offset_link); + self.primary_index + .reverse_pk_map + .insert(new_offset_link, pk); + // TODO: update secondary indexes + } + + /// Creates a new [`EmptyDataVacuum`] from the given [`WorkTable`] components. + pub fn new( + table_name: &'static str, + data_pages: Arc>, + lock_manager: Arc>, + primary_index: Arc>, + secondary_indexes: Arc, + ) -> Self { + Self { + table_name, + data_pages, + lock_manager, + primary_index, + secondary_indexes, + phantom_data: PhantomData, + } + } +} + +#[async_trait] +impl< + Row, + PrimaryKey, + PkNodeType, + SecondaryIndexes, + AvailableTypes, + AvailableIndexes, + LockType, + const DATA_LENGTH: usize, + SecondaryEvents, +> WorkTableVacuum + for EmptyDataVacuum< + Row, + PrimaryKey, + PkNodeType, + SecondaryIndexes, + AvailableTypes, + AvailableIndexes, + LockType, + DATA_LENGTH, + SecondaryEvents, + > +where + Row: TableRow + StorableRow + Send + Sync + Clone + 'static, + PrimaryKey: Debug + Clone + Ord + Send + Sync + TablePrimaryKey + std::hash::Hash, + PkNodeType: NodeLike>> + Send + Sync + 'static, + ::WrappedRow: RowWrapper, + Row: Archive + + Clone + + for<'a> Serialize< + Strategy, Share>, rkyv::rancor::Error>, + >, + ::WrappedRow: Archive + + for<'a> Serialize< + Strategy, Share>, rkyv::rancor::Error>, + > + Send + + Sync, + <::WrappedRow as Archive>::Archived: GhostWrapper, + SecondaryIndexes: TableSecondaryIndex + + TableSecondaryIndexCdc + + Send + + Sync, + AvailableIndexes: Debug + AvailableIndex, + SecondaryEvents: Send + Sync + 'static, + AvailableTypes: Send + Sync + 'static, + AvailableIndexes: Send + Sync + 'static, + LockType: Send + Sync, +{ + fn table_name(&self) -> &str { + self.table_name + } + + fn analyze_fragmentation(&self) -> FragmentationInfo { + let per_page_info = self.data_pages.empty_links_registry().get_per_page_info(); + FragmentationInfo::new(self.table_name, per_page_info.len(), per_page_info) + } + + async fn vacuum(&self) -> eyre::Result { + Ok(self.defragment().await) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::sync::Arc; + + use indexset::core::pair::Pair; + use worktable_codegen::{MemStat, worktable}; + + use crate::in_memory::{GhostWrapper, RowWrapper, StorableRow}; + use crate::prelude::*; + use crate::vacuum::vacuum::EmptyDataVacuum; + + worktable!( + name: Test, + columns: { + id: u64 primary_key autoincrement, + test: i64, + another: u64, + exchange: String + }, + indexes: { + test_idx: test unique, + exchnage_idx: exchange, + another_idx: another, + } + ); + + /// Creates an EmptyDataVacuum instance from a WorkTable + fn create_vacuum( + table: &TestWorkTable, + ) -> EmptyDataVacuum< + TestRow, + TestPrimaryKey, + Vec>>, + TestIndex, + TestAvaiableTypes, + TestAvailableIndexes, + TestLock, + TEST_INNER_SIZE, + > { + EmptyDataVacuum::new( + table.name(), + Arc::clone(&table.0.data), + Arc::clone(&table.0.lock_manager), + Arc::clone(&table.0.primary_index), + Arc::clone(&table.0.indexes), + ) + } + + #[tokio::test] + async fn test_vacuum_shift_data_in_range_single_gap() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + for i in 0..10 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + let first_two_ids = ids.iter().take(2).map(|(i, _)| *i).collect::>(); + + table.delete(first_two_ids[0].into()).await.unwrap(); + table.delete(first_two_ids[1].into()).await.unwrap(); + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids.into_iter().skip(2) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_shift_data_middle_gap() { + let table = TestWorkTable::default(); + + let mut ids = HashMap::new(); + for i in 0..20 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i * 10, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.insert(id, row); + } + + let ids_to_delete = ids.keys().skip(5).take(2).cloned().collect::>(); + + table.delete(ids_to_delete[0].into()).await.unwrap(); + table.delete(ids_to_delete[1].into()).await.unwrap(); + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids + .into_iter() + .filter(|(i, _)| *i != ids_to_delete[0] && *i != ids_to_delete[1]) + { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_shift_data_last_records() { + let table = TestWorkTable::default(); + + let mut ids = HashMap::new(); + for i in 0..10 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.insert(id, row); + } + + let last_two_ids = ids.keys().skip(8).take(2).cloned().collect::>(); + + table.delete(last_two_ids[1].into()).await.unwrap(); + table.delete(last_two_ids[0].into()).await.unwrap(); + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids + .into_iter() + .filter(|(i, _)| *i != last_two_ids[0] && *i != last_two_ids[1]) + { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_shift_data_multiple_gaps() { + let table = TestWorkTable::default(); + + let mut ids = HashMap::new(); + for i in 0..15 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.insert(id, row); + } + + let ids_to_delete = [1, 3, 5, 7].map(|idx| ids.keys().cloned().nth(idx).unwrap()); + + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_shift_data_single_record_left() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + for i in 0..5 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + let remaining_id = ids[0].0; + + for (id, _) in ids.iter().skip(1) { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + let row = table.select(remaining_id); + assert_eq!(row, Some(ids[0].1.clone())); + } + + #[tokio::test] + async fn test_vacuum_defragment_on_delete_last() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + for i in 0..5 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + table.delete(ids.last().unwrap().0.into()).await.unwrap(); + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids.into_iter().take(4) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_shift_data_variable_string_lengths() { + let table = TestWorkTable::default(); + + let mut ids = HashMap::new(); + let strings = vec![ + "a", + "bbbb", + "cccccc", + "dddddddd", + "eeeeeeeeee", + "ffffffffffff", + "gggggggggggggg", + ]; + + for (i, s) in strings.iter().enumerate() { + let row = TestRow { + id: table.get_next_pk().into(), + test: i as i64, + another: i as u64, + exchange: s.to_string(), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.insert(id, row); + } + + let ids_to_delete = ids.keys().take(3).cloned().collect::>(); + + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_insert_after_free_offset_update() { + let table = TestWorkTable::default(); + + let mut original_ids = HashMap::new(); + for i in 0..8 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("original{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + original_ids.insert(id, row); + } + + let ids_to_delete = original_ids.keys().take(3).cloned().collect::>(); + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + let mut new_ids = HashMap::new(); + for i in 0..3 { + let row = TestRow { + id: table.get_next_pk().into(), + test: 100 + i, + another: (100 + i) as u64, + exchange: format!("new{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + new_ids.insert(id, row); + } + + for (id, expected) in original_ids + .into_iter() + .filter(|(i, _)| !ids_to_delete.contains(i)) + { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + + for (id, expected) in new_ids { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_multi_page_data_migration() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + // row is ~40 bytes so ~409 rows per page + for i in 0..500 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + let ids_to_delete: Vec<_> = ids.iter().map(|(i, _)| *i).take(20).collect(); + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_multi_page_alternating_deletes() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + // row is ~40 bytes so ~409 rows per page + for i in 0..500 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + let ids_to_delete: Vec<_> = ids.iter().step_by(20).map(|(id, _)| *id).collect(); + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids + .into_iter() + .filter(|(id, _)| !ids_to_delete.contains(id)) + { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_multi_page_last() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + // row is ~40 bytes so ~409 rows per page + for i in 0..500 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + table.delete(ids.last().unwrap().0.into()).await.unwrap(); + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + for (id, expected) in ids.into_iter().take(499) { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } + + #[tokio::test] + async fn test_vacuum_multi_page_free_page() { + let table = TestWorkTable::default(); + + let mut ids = Vec::new(); + // row is ~40 bytes so ~409 rows per page + for i in 0..1000 { + let row = TestRow { + id: table.get_next_pk().into(), + test: i, + another: i as u64, + exchange: format!("test{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + ids.push((id, row)); + } + + let mut ids_to_delete: Vec<_> = ids.iter().skip(300).take(400).map(|(id, _)| *id).collect(); + // remove last too to trigger vacuum for last page too. + ids_to_delete.push(ids.last().unwrap().0); + for id in &ids_to_delete { + table.delete((*id).into()).await.unwrap(); + } + + let vacuum = create_vacuum(&table); + vacuum.defragment().await; + + assert!(table.0.data.get_empty_pages().len() > 0); + + for (id, expected) in ids + .into_iter() + .filter(|(id, _)| !ids_to_delete.contains(id)) + { + let row = table.select(id); + assert_eq!(row, Some(expected)); + } + } +} From 733c85dfc920586912a34247ea0d5a5464cb6f5f Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:28:37 +0300 Subject: [PATCH 15/32] WIP --- .../src/worktable/generator/queries/delete.rs | 6 +-- .../src/worktable/generator/queries/update.rs | 10 ++-- .../src/worktable/generator/table/impls.rs | 51 ++++++++++++++----- src/lock/table_lock.rs | 8 ++- src/persistence/operation/batch.rs | 6 +-- src/persistence/task.rs | 3 +- src/table/mod.rs | 29 +++++++++-- src/table/vacuum/vacuum.rs | 37 +++++++++----- .../sync/string_secondary_index.rs | 2 +- tests/worktable/tuple_primary_key.rs | 8 +-- 10 files changed, 111 insertions(+), 49 deletions(-) diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index 7032d53..fb566e4 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -61,7 +61,7 @@ impl Generator { let delete_logic = self.gen_delete_logic(false); quote! { - pub fn delete_without_lock(&self, pk: #pk_ident) -> core::result::Result<(), WorkTableError> { + pub async fn delete_without_lock(&self, pk: #pk_ident) -> core::result::Result<(), WorkTableError> { #delete_logic core::result::Result::Ok(()) } @@ -112,7 +112,7 @@ impl Generator { return Err(e); } }; - let row = self.select(pk.clone()).unwrap(); + let row = self.select(pk.clone()).await.unwrap(); #process } } else { @@ -123,7 +123,7 @@ impl Generator { .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; - let row = self.select(pk.clone()).unwrap(); + let row = self.select(pk.clone()).await.unwrap(); #process } } diff --git a/codegen/src/worktable/generator/queries/update.rs b/codegen/src/worktable/generator/queries/update.rs index 88271b5..9b9d4ae 100644 --- a/codegen/src/worktable/generator/queries/update.rs +++ b/codegen/src/worktable/generator/queries/update.rs @@ -69,7 +69,7 @@ impl Generator { #full_row_lock }; let row_old = self.0.data.select_non_ghosted(link)?; - if let Err(e) = self.reinsert(row_old, row) { + if let Err(e) = self.reinsert(row_old, row).await { self.0.update_state.remove(&pk); lock.unlock(); @@ -269,11 +269,11 @@ impl Generator { #full_row_lock }; - let row_old = self.0.select(pk.clone()).expect("should not be deleted by other thread"); + let row_old = self.0.select(pk.clone()).await.expect("should not be deleted by other thread"); let mut row_new = row_old.clone(); let pk = row_old.get_primary_key().clone(); #(#row_updates)* - if let Err(e) = self.reinsert(row_old, row_new) { + if let Err(e) = self.reinsert(row_old, row_new).await { self.0.update_state.remove(&pk); lock.unlock(); @@ -571,10 +571,10 @@ impl Generator { let lock = { #full_row_lock }; - let row_old = self.select(pk.clone()).expect("should not be deleted by other thread"); + let row_old = self.0.select(pk.clone()).await.expect("should not be deleted by other thread"); let mut row_new = row_old.clone(); #(#row_updates)* - if let Err(e) = self.reinsert(row_old, row_new) { + if let Err(e) = self.reinsert(row_old, row_new).await { self.0.update_state.remove(&pk); lock.unlock(); diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index e8b45ae..0364714 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -113,9 +113,9 @@ impl Generator { let primary_key_type = name_generator.get_primary_key_type_ident(); quote! { - pub fn select(&self, pk: Pk) -> Option<#row_type> + pub async fn select(&self, pk: Pk) -> Option<#row_type> where #primary_key_type: From { - self.0.select(pk.into()) + self.0.select(pk.into()).await } } } @@ -157,12 +157,12 @@ impl Generator { } } else { quote! { - self.0.reinsert(row_old, row_new) + self.0.reinsert(row_old, row_new).await } }; quote! { - pub fn reinsert(&self, row_old: #row_type, row_new: #row_type) -> core::result::Result<#primary_key_type, WorkTableError> { + pub async fn reinsert(&self, row_old: #row_type, row_new: #row_type) -> core::result::Result<#primary_key_type, WorkTableError> { #reinsert } } @@ -298,16 +298,41 @@ impl Generator { fn gen_table_vacuum_fn(&self) -> TokenStream { let name_generator = WorktableNameGenerator::from_table_name(self.name.to_string()); let table_name = name_generator.get_work_table_literal_name(); + let secondary_index_events = name_generator.get_space_secondary_index_events_ident(); - quote! { - pub fn vacuum(&self) -> Box { - Box::new(EmptyDataVacuum::new( - #table_name, - std::sync::Arc::clone(&self.0.data), - std::sync::Arc::clone(&self.0.lock_manager), - std::sync::Arc::clone(&self.0.primary_index), - std::sync::Arc::clone(&self.0.indexes), - )) + if self.is_persist { + quote! { + pub fn vacuum(&self) -> Box { + Box::new(EmptyDataVacuum::< + _, + _, + _, + _, + _, + _, + _, + _, + #secondary_index_events + >::new( + #table_name, + std::sync::Arc::clone(&self.0.data), + std::sync::Arc::clone(&self.0.lock_manager), + std::sync::Arc::clone(&self.0.primary_index), + std::sync::Arc::clone(&self.0.indexes), + )) + } + } + } else { + quote! { + pub fn vacuum(&self) -> Box { + Box::new(EmptyDataVacuum::new( + #table_name, + std::sync::Arc::clone(&self.0.data), + std::sync::Arc::clone(&self.0.lock_manager), + std::sync::Arc::clone(&self.0.primary_index), + std::sync::Arc::clone(&self.0.indexes), + )) + } } } } diff --git a/src/lock/table_lock.rs b/src/lock/table_lock.rs index 5639880..0c28f5d 100644 --- a/src/lock/table_lock.rs +++ b/src/lock/table_lock.rs @@ -56,12 +56,18 @@ where /// Checks if a page is locked by vacuum operations and awaits the lock if it is. /// /// This should be called before any operation that accesses data on a specific page. - pub async fn await_page_lock(&self, page_id: PageId) { + /// + /// Returns `false` if not waited at all, `true` if waited. + pub async fn await_page_lock(&self, page_id: PageId) -> bool { if let Some(lock) = self.get_page_lock(page_id) { let guard = lock.read().await; let wait = guard.wait(); drop(guard); wait.await; + + true + } else { + false } } } diff --git a/src/persistence/operation/batch.rs b/src/persistence/operation/batch.rs index 12c118a..0ea195e 100644 --- a/src/persistence/operation/batch.rs +++ b/src/persistence/operation/batch.rs @@ -112,7 +112,7 @@ where self.ops } - fn remove_operations_from_events( + async fn remove_operations_from_events( &mut self, invalid_events: PreparedIndexEvents, ) -> HashSet> { @@ -151,7 +151,7 @@ where .select_by_operation_id(op.operation_id()) .expect("exists as all should be inserted on prepare step") .id; - self.info_wt.delete_without_lock(pk.into()).unwrap(); + self.info_wt.delete_without_lock(pk.into()).await.unwrap(); let prepared_evs = self .prepared_index_evs .as_mut() @@ -237,7 +237,7 @@ where primary_evs: primary_invalid_events, secondary_evs: secondary_invalid_events, }; - let ops = self.remove_operations_from_events(events_to_remove); + let ops = self.remove_operations_from_events(events_to_remove).await; ops_to_remove.extend(ops); } diff --git a/src/persistence/task.rs b/src/persistence/task.rs index ea90f2a..3db8db8 100644 --- a/src/persistence/task.rs +++ b/src/persistence/task.rs @@ -243,6 +243,7 @@ where let mut row: BatchInnerRow = self .queue_inner_wt .select(id) + .await .expect("exists as Id exists") .into(); let op = self @@ -253,7 +254,7 @@ where row.op_type = op.operation_type(); ops.push(op); info_wt.insert(row)?; - self.queue_inner_wt.delete_without_lock(id.into())? + self.queue_inner_wt.delete_without_lock(id.into()).await? } // println!("New wt generated {:?}", start.elapsed()); // return ops sorted by `OperationId` diff --git a/src/table/mod.rs b/src/table/mod.rs index 4625d41..2def132 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -5,7 +5,7 @@ pub mod vacuum; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; use crate::lock::WorkTableLock; use crate::persistence::{InsertOperation, Operation}; -use crate::prelude::{OperationId, PrimaryKeyGeneratorState}; +use crate::prelude::{Link, OperationId, PrimaryKeyGeneratorState}; use crate::primary_key::{PrimaryKeyGenerator, TablePrimaryKey}; use crate::util::OffsetEqLink; use crate::{ @@ -149,7 +149,7 @@ where feature = "perf_measurements", performance_measurement(prefix_name = "WorkTable") )] - pub fn select(&self, pk: PrimaryKey) -> Option + pub async fn select(&self, pk: PrimaryKey) -> Option where LockType: 'static, Row: Archive @@ -159,11 +159,21 @@ where <::WrappedRow as Archive>::Archived: Deserialize<::WrappedRow, HighDeserializer>, { - let link = self + let mut link: Option = self .primary_index .pk_map .get(&pk) .map(|v| v.get().value.into()); + if let Some(l) = link { + if self.lock_manager.await_page_lock(l.page_id).await { + // We waited for vacuum to complete, need to re-lookup the link + link = self + .primary_index + .pk_map + .get(&pk) + .map(|v| v.get().value.into()); + } + } if let Some(link) = link { self.data.select(link).ok() } else { @@ -319,7 +329,7 @@ where /// new [`Link`]'s, goal will be achieved. /// /// [`Link`]: data_bucket::Link - pub fn reinsert(&self, row_old: Row, row_new: Row) -> Result + pub async fn reinsert(&self, row_old: Row, row_new: Row) -> Result where Row: Archive + Clone @@ -341,12 +351,21 @@ where if pk != row_old.get_primary_key() { return Err(WorkTableError::PrimaryUpdateTry); } - let old_link = self + let mut old_link: Link = self .primary_index .pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; + if self.lock_manager.await_page_lock(old_link.page_id).await { + // We waited for vacuum to complete, need to re-lookup the link + old_link = self + .primary_index + .pk_map + .get(&pk) + .map(|v| v.get().value.into()) + .ok_or(WorkTableError::NotFound)?; + } let new_link = self .data .insert(row_new.clone()) diff --git a/src/table/vacuum/vacuum.rs b/src/table/vacuum/vacuum.rs index 146191a..cf8a77a 100644 --- a/src/table/vacuum/vacuum.rs +++ b/src/table/vacuum/vacuum.rs @@ -269,12 +269,23 @@ where let mut empty_links_iter = page_empty_links.into_iter(); let Some(mut current_empty) = empty_links_iter.next() else { + // TODO: create some kind of guard that will Drop himself + { + let l = lock.read().await; + l.unlock(); + self.lock_manager.remove_page_lock(&info.page_id) + } return; }; registry.remove_link(current_empty); let Some(mut next_empty) = empty_links_iter.next() else { self.shift_data_in_range(current_empty, None); + { + let l = lock.read().await; + l.unlock(); + self.lock_manager.remove_page_lock(&info.page_id) + } return; }; registry.remove_link(next_empty); @@ -543,7 +554,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().skip(2) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } @@ -577,7 +588,7 @@ mod tests { .into_iter() .filter(|(i, _)| *i != ids_to_delete[0] && *i != ids_to_delete[1]) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } @@ -611,7 +622,7 @@ mod tests { .into_iter() .filter(|(i, _)| *i != last_two_ids[0] && *i != last_two_ids[1]) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } @@ -643,7 +654,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } @@ -674,7 +685,7 @@ mod tests { let vacuum = create_vacuum(&table); vacuum.defragment().await; - let row = table.select(remaining_id); + let row = table.select(remaining_id).await; assert_eq!(row, Some(ids[0].1.clone())); } @@ -701,7 +712,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().take(4) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } @@ -743,7 +754,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } @@ -790,12 +801,12 @@ mod tests { .into_iter() .filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } for (id, expected) in new_ids { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } @@ -827,7 +838,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } @@ -862,7 +873,7 @@ mod tests { .into_iter() .filter(|(id, _)| !ids_to_delete.contains(id)) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } @@ -891,7 +902,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().take(499) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } @@ -930,7 +941,7 @@ mod tests { .into_iter() .filter(|(id, _)| !ids_to_delete.contains(id)) { - let row = table.select(id); + let row = table.select(id).await; assert_eq!(row, Some(expected)); } } diff --git a/tests/persistence/sync/string_secondary_index.rs b/tests/persistence/sync/string_secondary_index.rs index 60d6816..af98565 100644 --- a/tests/persistence/sync/string_secondary_index.rs +++ b/tests/persistence/sync/string_secondary_index.rs @@ -61,7 +61,7 @@ fn test_space_insert_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); + assert!(table.select(pk).await.is_some()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); diff --git a/tests/worktable/tuple_primary_key.rs b/tests/worktable/tuple_primary_key.rs index 49c98b1..57ba76a 100644 --- a/tests/worktable/tuple_primary_key.rs +++ b/tests/worktable/tuple_primary_key.rs @@ -10,8 +10,8 @@ worktable! ( } ); -#[test] -fn insert() { +#[tokio::test] +async fn insert() { let table = TestWorkTable::default(); let row = TestRow { id: 1, @@ -19,8 +19,8 @@ fn insert() { another: 1, }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, row); - assert!(table.select((1, 0)).is_none()) + assert!(table.select((1, 0)).await.is_none()) } From 77e91ada0eac4946fb5967c305c16cb9605e5d9a Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:46:36 +0300 Subject: [PATCH 16/32] fix tests --- tests/persistence/sync/many_strings.rs | 20 ++-- tests/persistence/sync/mod.rs | 24 ++--- .../persistence/sync/string_primary_index.rs | 24 ++--- tests/persistence/sync/string_re_read.rs | 18 ++-- .../sync/string_secondary_index.rs | 24 ++--- tests/worktable/array.rs | 36 +++---- tests/worktable/base.rs | 100 +++++++++--------- tests/worktable/in_place.rs | 22 ++-- tests/worktable/index/insert.rs | 26 ++--- tests/worktable/index/update_full.rs | 12 +-- tests/worktable/option.rs | 6 +- tests/worktable/unsized_.rs | 20 ++-- tests/worktable/uuid.rs | 8 +- tests/worktable/with_enum.rs | 12 +-- 14 files changed, 176 insertions(+), 176 deletions(-) diff --git a/tests/persistence/sync/many_strings.rs b/tests/persistence/sync/many_strings.rs index 3ae5e7c..96d97a0 100644 --- a/tests/persistence/sync/many_strings.rs +++ b/tests/persistence/sync/many_strings.rs @@ -61,8 +61,8 @@ fn test_space_update_query_pk_sync() { let table = TestSyncWorkTable::load_from_file(config.clone()) .await .unwrap(); - assert!(table.select(pk.clone()).is_some()); - assert_eq!(table.select(pk.clone()).unwrap().another, 43); + assert!(table.select(pk.clone()).await.is_some()); + assert_eq!(table.select(pk.clone()).await.unwrap().another, 43); let q = FieldAnotherByIdQuery { field: "Some field value".to_string(), another: 0, @@ -75,10 +75,10 @@ fn test_space_update_query_pk_sync() { } { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).is_some()); - assert_eq!(table.select(pk.clone()).unwrap().another, 0); + assert!(table.select(pk.clone()).await.is_some()); + assert_eq!(table.select(pk.clone()).await.unwrap().another, 0); assert_eq!( - table.select(pk).unwrap().field, + table.select(pk).await.unwrap().field, "Some field value".to_string() ); } @@ -128,8 +128,8 @@ fn test_space_update_query_pk_many_times_sync() { let table = TestSyncWorkTable::load_from_file(config.clone()) .await .unwrap(); - assert!(table.select(pk.clone()).is_some()); - assert_eq!(table.select(pk.clone()).unwrap().another, 43); + assert!(table.select(pk.clone()).await.is_some()); + assert_eq!(table.select(pk.clone()).await.unwrap().another, 43); for i in 0..512 { let q = FieldAnotherByIdQuery { field: "Some field value".to_string(), @@ -145,10 +145,10 @@ fn test_space_update_query_pk_many_times_sync() { } { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).is_some()); - assert_eq!(table.select(pk.clone()).unwrap().another, 511); + assert!(table.select(pk.clone()).await.is_some()); + assert_eq!(table.select(pk.clone()).await.unwrap().another, 511); assert_eq!( - table.select(pk).unwrap().field, + table.select(pk).await.unwrap().field, "Some field value".to_string() ); } diff --git a/tests/persistence/sync/mod.rs b/tests/persistence/sync/mod.rs index ce3e97b..8b3a780 100644 --- a/tests/persistence/sync/mod.rs +++ b/tests/persistence/sync/mod.rs @@ -87,7 +87,7 @@ fn test_space_insert_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); + assert!(table.select(pk).await.is_some()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -133,7 +133,7 @@ fn test_space_insert_many_sync() { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); let last = *pks.last().unwrap(); for pk in pks { - assert!(table.select(pk).is_some()); + assert!(table.select(pk).await.is_some()); } assert_eq!(table.0.pk_gen.get_state(), last + 1) } @@ -180,8 +180,8 @@ fn test_space_update_full_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); - assert_eq!(table.select(pk).unwrap().another, 13); + assert!(table.select(pk).await.is_some()); + assert_eq!(table.select(pk).await.unwrap().another, 13); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -224,8 +224,8 @@ fn test_space_update_query_pk_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); - assert_eq!(table.select(pk).unwrap().another, 13); + assert!(table.select(pk).await.is_some()); + assert_eq!(table.select(pk).await.unwrap().another, 13); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -268,8 +268,8 @@ fn test_space_update_query_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); - assert_eq!(table.select(pk).unwrap().field, 1.0); + assert!(table.select(pk).await.is_some()); + assert_eq!(table.select(pk).await.unwrap().field, 1.0); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -312,8 +312,8 @@ fn test_space_update_query_non_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); - assert_eq!(table.select(pk).unwrap().another, 13); + assert!(table.select(pk).await.is_some()); + assert_eq!(table.select(pk).await.unwrap().another, 13); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -350,7 +350,7 @@ fn test_space_delete_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_none()); + assert!(table.select(pk).await.is_none()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -390,7 +390,7 @@ fn test_space_delete_query_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_none()); + assert!(table.select(pk).await.is_none()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); diff --git a/tests/persistence/sync/string_primary_index.rs b/tests/persistence/sync/string_primary_index.rs index 4fe857c..0b1fe59 100644 --- a/tests/persistence/sync/string_primary_index.rs +++ b/tests/persistence/sync/string_primary_index.rs @@ -61,7 +61,7 @@ fn test_space_insert_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); + assert!(table.select(pk).await.is_some()); } }); } @@ -107,7 +107,7 @@ fn test_space_insert_many_sync() { { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); for pk in pks { - assert!(table.select(pk).is_some()); + assert!(table.select(pk).await.is_some()); } } }); @@ -155,8 +155,8 @@ fn test_space_update_full_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).is_some()); - assert_eq!(table.select(pk).unwrap().another, 13); + assert!(table.select(pk.clone()).await.is_some()); + assert_eq!(table.select(pk).await.unwrap().another, 13); } }); } @@ -198,8 +198,8 @@ fn test_space_update_query_pk_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).is_some()); - assert_eq!(table.select(pk).unwrap().another, 13); + assert!(table.select(pk.clone()).await.is_some()); + assert_eq!(table.select(pk).await.unwrap().another, 13); } }); } @@ -242,8 +242,8 @@ fn test_space_update_query_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).is_some()); - assert_eq!(table.select(pk).unwrap().field, 1.0); + assert!(table.select(pk.clone()).await.is_some()); + assert_eq!(table.select(pk).await.unwrap().field, 1.0); } }); } @@ -286,8 +286,8 @@ fn test_space_update_query_non_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).is_some()); - assert_eq!(table.select(pk).unwrap().another, 13); + assert!(table.select(pk.clone()).await.is_some()); + assert_eq!(table.select(pk).await.unwrap().another, 13); } }); } @@ -333,7 +333,7 @@ fn test_space_delete_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_none()); + assert!(table.select(pk).await.is_none()); } }); } @@ -372,7 +372,7 @@ fn test_space_delete_query_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_none()); + assert!(table.select(pk).await.is_none()); } }); } diff --git a/tests/persistence/sync/string_re_read.rs b/tests/persistence/sync/string_re_read.rs index 5414af3..57b6021 100644 --- a/tests/persistence/sync/string_re_read.rs +++ b/tests/persistence/sync/string_re_read.rs @@ -145,7 +145,7 @@ fn test_key_delete_scenario() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1); - assert!(table.select(pk).is_none()); + assert!(table.select(pk).await.is_none()); assert_eq!( table .select_by_first("first".to_string()) @@ -181,7 +181,7 @@ fn test_key_delete_scenario() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1); - assert!(table.select(pk0).is_none()); + assert!(table.select(pk0).await.is_none()); assert_eq!( table .select_by_first("first".to_string()) @@ -256,7 +256,7 @@ fn test_key_delete() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1); - assert!(table.select(pk).is_none()); + assert!(table.select(pk).await.is_none()); assert_eq!( table .select_by_first("first".to_string()) @@ -325,8 +325,8 @@ fn test_key_delete_all() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 0); - assert!(table.select(pk0).is_none()); - assert!(table.select(pk1).is_none()); + assert!(table.select(pk0).await.is_none()); + assert!(table.select(pk1).await.is_none()); assert_eq!( table .select_by_first("first".to_string()) @@ -419,7 +419,7 @@ fn test_key_delete_all_and_insert() { assert_eq!(table.select_all().execute().unwrap().len(), 1); - assert!(table.select(pk).is_some()); + assert!(table.select(pk).await.is_some()); assert_eq!( table .select_by_first("first".to_string()) @@ -493,7 +493,7 @@ fn test_key_delete_by_unique() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1); - assert!(table.select(pk).is_none()); + assert!(table.select(pk).await.is_none()); assert_eq!( table .select_by_first("first".to_string()) @@ -564,8 +564,8 @@ fn test_key_delete_by_non_unique() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 0); - assert!(table.select(pk0).is_none()); - assert!(table.select(pk1).is_none()); + assert!(table.select(pk0).await.is_none()); + assert!(table.select(pk1).await.is_none()); assert_eq!( table .select_by_first("first".to_string()) diff --git a/tests/persistence/sync/string_secondary_index.rs b/tests/persistence/sync/string_secondary_index.rs index af98565..34cb6aa 100644 --- a/tests/persistence/sync/string_secondary_index.rs +++ b/tests/persistence/sync/string_secondary_index.rs @@ -109,7 +109,7 @@ fn test_space_insert_many_sync() { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); let last = *pks.last().unwrap(); for pk in pks { - assert!(table.select(pk).is_some()); + assert!(table.select(pk).await.is_some()); } assert_eq!(table.0.pk_gen.get_state(), last + 1) } @@ -155,16 +155,16 @@ fn test_space_update_full_sync() { .unwrap(); table.wait_for_ops().await; assert_eq!( - table.select(row.id).unwrap().another, + table.select(row.id).await.unwrap().another, "Some string to test updated".to_string() ); row.id }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); + assert!(table.select(pk).await.is_some()); assert_eq!( - table.select(pk).unwrap().another, + table.select(pk).await.unwrap().another, "Some string to test updated".to_string() ); assert_eq!(table.0.pk_gen.get_state(), pk + 1) @@ -214,9 +214,9 @@ fn test_space_update_query_pk_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); + assert!(table.select(pk).await.is_some()); assert_eq!( - table.select(pk).unwrap().another, + table.select(pk).await.unwrap().another, "Some string to test updated".to_string() ); assert_eq!(table.0.pk_gen.get_state(), pk + 1) @@ -265,8 +265,8 @@ fn test_space_update_query_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); - assert_eq!(table.select(pk).unwrap().field, 1.0); + assert!(table.select(pk).await.is_some()); + assert_eq!(table.select(pk).await.unwrap().field, 1.0); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -317,9 +317,9 @@ fn test_space_update_query_non_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_some()); + assert!(table.select(pk).await.is_some()); assert_eq!( - table.select(pk).unwrap().another, + table.select(pk).await.unwrap().another, "Some string to test updated".to_string() ); assert_eq!(table.0.pk_gen.get_state(), pk + 1) @@ -361,7 +361,7 @@ fn test_space_delete_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_none()); + assert!(table.select(pk).await.is_none()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -401,7 +401,7 @@ fn test_space_delete_query_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).is_none()); + assert!(table.select(pk).await.is_none()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); diff --git a/tests/worktable/array.rs b/tests/worktable/array.rs index 0e61214..2be037b 100644 --- a/tests/worktable/array.rs +++ b/tests/worktable/array.rs @@ -16,18 +16,18 @@ worktable! ( } ); -#[test] -fn insert() { +#[tokio::test] +async fn insert() { let table = TestWorkTable::default(); let row = TestRow { id: 1, test: [1; 20], }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, row); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } #[tokio::test] @@ -43,10 +43,10 @@ async fn update() { test: [2; 20], }; table.update(new_row.clone()).await.unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, new_row); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } #[tokio::test] @@ -64,7 +64,7 @@ async fn update_in_a_middle() { test: [1; 20], }; table.update(new_row.clone()).await.unwrap(); - let selected_row = table.select(3).unwrap(); + let selected_row = table.select(3).await.unwrap(); assert_eq!(selected_row, new_row); } @@ -82,10 +82,10 @@ async fn update_query() { .update_test_by_id(q.clone(), pk.clone()) .await .unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row.test, q.test); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } type ArrI = [i64; 20]; @@ -103,18 +103,18 @@ worktable! ( } ); -#[test] -fn insert_i() { +#[tokio::test] +async fn insert_i() { let table = TestIWorkTable::default(); let row = TestIRow { id: 1, test: [1; 20], }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, row); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } #[tokio::test] @@ -130,10 +130,10 @@ async fn update_i() { test: [2; 20], }; table.update(new_row.clone()).await.unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, new_row); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } #[tokio::test] @@ -151,7 +151,7 @@ async fn update_in_a_middle_i() { test: [1; 20], }; table.update(new_row.clone()).await.unwrap(); - let selected_row = table.select(3).unwrap(); + let selected_row = table.select(3).await.unwrap(); assert_eq!(selected_row, new_row); } @@ -169,8 +169,8 @@ async fn update_query_i() { .update_test_i_by_id(q.clone(), pk.clone()) .await .unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row.test, q.test); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } diff --git a/tests/worktable/base.rs b/tests/worktable/base.rs index ef3675c..317f1bb 100644 --- a/tests/worktable/base.rs +++ b/tests/worktable/base.rs @@ -121,10 +121,10 @@ async fn update_spawn() { .await .unwrap() .unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, updated); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } #[tokio::test] @@ -149,10 +149,10 @@ async fn upsert_spawn() { .await .unwrap() .unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, updated); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } #[tokio::test] @@ -172,10 +172,10 @@ async fn update() { exchange: "test".to_string(), }; table.update(updated.clone()).await.unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, updated); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } #[tokio::test] @@ -196,11 +196,11 @@ async fn update_string() { exchange: "much bigger test to make size of new row bigger than previous one".to_string(), }; table.update(updated.clone()).await.unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, updated); assert_eq!(table.0.data.get_empty_links().first().unwrap(), &first_link); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -279,7 +279,7 @@ async fn delete() { .map(|kv| kv.get().value) .unwrap(); table.delete(pk.clone()).await.unwrap(); - let selected_row = table.select(pk); + let selected_row = table.select(pk).await; assert!(selected_row.is_none()); let selected_row = table.select_by_test(1); assert!(selected_row.is_none()); @@ -392,7 +392,7 @@ async fn delete_and_insert_less() { .map(|kv| kv.get().value) .unwrap(); table.delete(pk.clone()).await.unwrap(); - let selected_row = table.select(pk); + let selected_row = table.select(pk).await; assert!(selected_row.is_none()); let updated = TestRow { @@ -438,7 +438,7 @@ async fn delete_and_replace() { .map(|kv| kv.get().value) .unwrap(); table.delete(pk.clone()).await.unwrap(); - let selected_row = table.select(pk); + let selected_row = table.select(pk).await; assert!(selected_row.is_none()); let updated = TestRow { @@ -476,10 +476,10 @@ async fn upsert() { exchange: "test".to_string(), }; table.upsert(updated.clone()).await.unwrap(); - let selected_row = table.select(row.id).unwrap(); + let selected_row = table.select(row.id).await.unwrap(); assert_eq!(selected_row, updated); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } #[test] @@ -516,8 +516,8 @@ fn insert_exchange_same() { assert!(res.is_err()) } -#[test] -fn select_by_exchange() { +#[tokio::test] +async fn select_by_exchange() { let table = TestWorkTable::default(); let row = TestRow { id: table.get_next_pk().into(), @@ -542,8 +542,8 @@ fn select_by_exchange() { ) } -#[test] -fn select_multiple_by_exchange() { +#[tokio::test] +async fn select_multiple_by_exchange() { let table = TestWorkTable::default(); let row = TestRow { id: table.get_next_pk().into(), @@ -576,8 +576,8 @@ fn select_multiple_by_exchange() { ) } -#[test] -fn select_by_test() { +#[tokio::test] +async fn select_by_test() { let table = TestWorkTable::default(); let row = TestRow { id: table.get_next_pk().into(), @@ -592,8 +592,8 @@ fn select_by_test() { assert!(table.select_by_test(2).is_none()) } -#[test] -fn select_all_test() { +#[tokio::test] +async fn select_all_test() { let table = TestWorkTable::default(); let row1 = TestRow { id: table.get_next_pk().into(), @@ -617,8 +617,8 @@ fn select_all_test() { assert_eq!(&all[1], &row2) } -#[test] -fn select_all_range_test() { +#[tokio::test] +async fn select_all_range_test() { let table = TestWorkTable::default(); let row1 = TestRow { @@ -653,8 +653,8 @@ fn select_all_range_test() { assert_eq!(all.len(), 2); } -#[test] -fn select_all_range_inclusive_test() { +#[tokio::test] +async fn select_all_range_inclusive_test() { let table = TestWorkTable::default(); let row1 = TestRow { @@ -689,8 +689,8 @@ fn select_all_range_inclusive_test() { assert_eq!(all.len(), 2); } -#[test] -fn select_all_where_by_eq_string_test() { +#[tokio::test] +async fn select_all_where_by_eq_string_test() { let table = TestWorkTable::default(); let row1 = TestRow { @@ -722,8 +722,8 @@ fn select_all_where_by_eq_string_test() { assert_eq!(equal.len(), 1); } -#[test] -fn select_all_where_by_contains_string_test() { +#[tokio::test] +async fn select_all_where_by_contains_string_test() { let table = TestWorkTable::default(); let row1 = TestRow { @@ -758,8 +758,8 @@ fn select_all_where_by_contains_string_test() { assert_eq!(contains.len(), 3); } -#[test] -fn select_all_where_by_gt_string_number_test() { +#[tokio::test] +async fn select_all_where_by_gt_string_number_test() { let table = TestWorkTable::default(); let row1 = TestRow { @@ -791,8 +791,8 @@ fn select_all_where_by_gt_string_number_test() { assert_eq!(equal.len(), 2); } -#[test] -fn select_all_where_by_eq_string_number_test() { +#[tokio::test] +async fn select_all_where_by_eq_string_number_test() { let table = TestWorkTable::default(); let row1 = TestRow { @@ -824,8 +824,8 @@ fn select_all_where_by_eq_string_number_test() { assert_eq!(equal.len(), 2); } -#[test] -fn select_all_order_multiple_test() { +#[tokio::test] +async fn select_all_order_multiple_test() { let table = TestWorkTable::default(); let row1 = TestRow { @@ -864,8 +864,8 @@ fn select_all_order_multiple_test() { assert_eq!(&all[2], &row2); } -#[test] -fn select_all_limit_test() { +#[tokio::test] +async fn select_all_limit_test() { let table = TestWorkTable::default(); let row1 = TestRow { id: table.get_next_pk().into(), @@ -898,8 +898,8 @@ fn select_all_limit_test() { assert_eq!(&all[1], &row2) } -#[test] -fn select_all_offset_test() { +#[tokio::test] +async fn select_all_offset_test() { let table = TestWorkTable::default(); let row1 = TestRow { id: table.get_next_pk().into(), @@ -924,8 +924,8 @@ fn select_all_offset_test() { assert_eq!(all.len(), 0); } -#[test] -fn select_all_order_on_unique_test() { +#[tokio::test] +async fn select_all_order_on_unique_test() { let table = TestWorkTable::default(); let row1 = TestRow { id: table.get_next_pk().into(), @@ -963,8 +963,8 @@ fn select_all_order_on_unique_test() { assert_eq!(&all[1].test, &2) } -#[test] -fn select_all_order_on_non_unique_test() { +#[tokio::test] +async fn select_all_order_on_non_unique_test() { let table = TestWorkTable::default(); let row1 = TestRow { id: table.get_next_pk().into(), @@ -1002,8 +1002,8 @@ fn select_all_order_on_non_unique_test() { assert_eq!(&all[1].exchange, &"a_test".to_string()) } -#[test] -fn select_all_order_two_test() { +#[tokio::test] +async fn select_all_order_two_test() { let table = TestWorkTable::default(); let row1 = TestRow { id: table.get_next_pk().into(), @@ -1086,8 +1086,8 @@ fn select_by_order_on_test() { assert_eq!(&all[2].test, &97) } -#[test] -fn select_by_offset_test() { +#[tokio::test] +async fn select_by_offset_test() { let table = TestWorkTable::default(); let row1 = TestRow { id: table.get_next_pk().into(), @@ -1231,8 +1231,8 @@ async fn test_update_by_pk() { ) } -//#[test] -fn _bench() { +//#[tokio::test] +async fn _bench() { let table = TestWorkTable::default(); let mut v = Vec::with_capacity(10000); @@ -1250,6 +1250,6 @@ fn _bench() { } for a in v { - table.select(a).expect("TODO: panic message"); + let _ = table.select(a).await.expect("TODO: panic message"); } } diff --git a/tests/worktable/in_place.rs b/tests/worktable/in_place.rs index 28f536f..c81042d 100644 --- a/tests/worktable/in_place.rs +++ b/tests/worktable/in_place.rs @@ -45,7 +45,7 @@ async fn test_update_val_by_id() -> eyre::Result<()> { .update_val_by_id_in_place(|val| *val += 1, pk.0) .await? } - let row = table.select(pk).unwrap(); + let row = table.select(pk).await.unwrap(); assert_eq!(row.val, 10000); Ok(()) } @@ -67,7 +67,7 @@ async fn test_update_val2_by_id() -> eyre::Result<()> { .update_val_2_by_id_in_place(|val| *val += 1, pk.0) .await? } - let row = table.select(pk).unwrap(); + let row = table.select(pk).await.unwrap(); assert_eq!(row.val2, 100); Ok(()) } @@ -99,7 +99,7 @@ async fn test_update_val_by_id_two_thread() -> eyre::Result<()> { .await? } h.await?; - let row = table.select(pk).unwrap(); + let row = table.select(pk).await.unwrap(); assert_eq!(row.val, 20_000); Ok(()) } @@ -151,7 +151,7 @@ async fn test_update_val_and_val2_by_id_four_thread() -> eyre::Result<()> { h1.await?; h2.await?; h3.await?; - let row = table.select(pk).unwrap(); + let row = table.select(pk).await.unwrap(); assert_eq!(row.val, 20_000); assert_eq!(row.val2, 20_000); Ok(()) @@ -204,7 +204,7 @@ async fn test_update_val_by_id_four_thread() -> eyre::Result<()> { h1.await?; h2.await?; h3.await?; - let row = table.select(pk).unwrap(); + let row = table.select(pk).await.unwrap(); assert_eq!(row.val, 40_000); Ok(()) } @@ -283,15 +283,15 @@ async fn test_update_in_place_and_update_sized_multithread() -> eyre::Result<()> h2.await?; for (id, smth) in i_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.something, smth); } for (id, val) in val2_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.val2, val); } for (id, val) in val_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.val, val); } Ok(()) @@ -376,12 +376,12 @@ async fn test_update_in_place_and_update_unsized_multithread() -> eyre::Result<( h2.await?; for (id, smth) in i_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.another, smth); } let mut errors = 0; for (id, val) in val2_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); if &row.val2 != val { errors += 1; } @@ -389,7 +389,7 @@ async fn test_update_in_place_and_update_unsized_multithread() -> eyre::Result<( assert_eq!(errors, 0); let mut errors = 0; for (id, val) in val_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); if &row.val != val { errors += 1; } diff --git a/tests/worktable/index/insert.rs b/tests/worktable/index/insert.rs index 69e557b..62b6ad6 100644 --- a/tests/worktable/index/insert.rs +++ b/tests/worktable/index/insert.rs @@ -20,8 +20,8 @@ worktable!( } ); -#[test] -fn insert() { +#[tokio::test] +async fn insert() { let table = TestWorkTable::default(); let row = TestRow { id: table.get_next_pk().into(), @@ -32,14 +32,14 @@ fn insert() { attr4: "Attribute4".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, row); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } -#[test] -fn insert_when_pk_exists() { +#[tokio::test] +async fn insert_when_pk_exists() { let table = TestWorkTable::default(); let row = TestRow { id: table.get_next_pk().into(), @@ -60,7 +60,7 @@ fn insert_when_pk_exists() { attr4: "Attributee".to_string(), }; assert!(table.insert(next_row.clone()).is_err()); - assert_eq!(table.select(pk.clone()).unwrap(), row); + assert_eq!(table.select(pk.clone()).await.unwrap(), row); assert!( table .0 @@ -199,8 +199,8 @@ fn insert_when_secondary_unique_string_exists() { ); } -#[test] -fn insert_when_unique_violated() { +#[tokio::test] +async fn insert_when_unique_violated() { let table = Arc::new(TestWorkTable::default()); let row = TestRow { @@ -232,7 +232,7 @@ fn insert_when_unique_violated() { }); for _ in 0..5000 { - let sel_row = table.select(row.id); + let sel_row = table.select(row.id).await; assert_eq!(sel_row, Some(row.clone())); let attr_1_rows = table.select_by_attr1(row.attr1.clone()).execute().unwrap(); assert_eq!(attr_1_rows.len(), 1); @@ -291,8 +291,8 @@ fn insert_after_unique_violated() { } } -#[test] -fn insert_when_pk_violated() { +#[tokio::test] +async fn insert_when_pk_violated() { let table = Arc::new(TestWorkTable::default()); let row = TestRow { @@ -323,7 +323,7 @@ fn insert_when_pk_violated() { }); for _ in 0..5000 { - let sel_row = table.select(id); + let sel_row = table.select(id).await; assert!(sel_row.is_some()); assert_eq!(sel_row, Some(row.clone())) } diff --git a/tests/worktable/index/update_full.rs b/tests/worktable/index/update_full.rs index fa57ca1..1e3a67a 100644 --- a/tests/worktable/index/update_full.rs +++ b/tests/worktable/index/update_full.rs @@ -310,7 +310,7 @@ async fn update_by_full_row_with_reinsert_and_primary_key_violation() { update.attr1 = "TEST_______________________1".to_string(); assert!(test_table.update(update).await.is_err()); - assert_eq!(test_table.select(row1.id).unwrap(), row1); + assert_eq!(test_table.select(row1.id).await.unwrap(), row1); assert_eq!( test_table.select_by_attr1(row1.attr1.clone()).unwrap(), row1 @@ -318,7 +318,7 @@ async fn update_by_full_row_with_reinsert_and_primary_key_violation() { assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); - assert_eq!(test_table.select(row2.id).unwrap(), row2); + assert_eq!(test_table.select(row2.id).await.unwrap(), row2); assert_eq!( test_table.select_by_attr1(row2.attr1.clone()).unwrap(), row2 @@ -351,7 +351,7 @@ async fn update_by_full_row_with_reinsert_and_secondary_unique_violation() { update.attr1 = row2.attr1.clone(); assert!(test_table.update(update).await.is_err()); - assert_eq!(test_table.select(row1.id).unwrap(), row1); + assert_eq!(test_table.select(row1.id).await.unwrap(), row1); assert_eq!( test_table.select_by_attr1(row1.attr1.clone()).unwrap(), row1 @@ -359,7 +359,7 @@ async fn update_by_full_row_with_reinsert_and_secondary_unique_violation() { assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); - assert_eq!(test_table.select(row2.id).unwrap(), row2); + assert_eq!(test_table.select(row2.id).await.unwrap(), row2); assert_eq!( test_table.select_by_attr1(row2.attr1.clone()).unwrap(), row2 @@ -392,7 +392,7 @@ async fn update_by_full_row_with_secondary_unique_violation() { update.attr2 = row2.attr2; assert!(test_table.update(update).await.is_err()); - assert_eq!(test_table.select(row1.id).unwrap(), row1); + assert_eq!(test_table.select(row1.id).await.unwrap(), row1); assert_eq!( test_table.select_by_attr1(row1.attr1.clone()).unwrap(), row1 @@ -400,7 +400,7 @@ async fn update_by_full_row_with_secondary_unique_violation() { assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); - assert_eq!(test_table.select(row2.id).unwrap(), row2); + assert_eq!(test_table.select(row2.id).await.unwrap(), row2); assert_eq!( test_table.select_by_attr1(row2.attr1.clone()).unwrap(), row2 diff --git a/tests/worktable/option.rs b/tests/worktable/option.rs index 378240b..86480dc 100644 --- a/tests/worktable/option.rs +++ b/tests/worktable/option.rs @@ -39,7 +39,7 @@ async fn update() { exchange: 1, }; table.update(new_row.clone()).await.unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, new_row); } @@ -57,7 +57,7 @@ async fn update_by_another() { .update_test_by_another(TestByAnotherQuery { test: Some(1) }, 1) .await .unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row.test, Some(1)); } @@ -75,6 +75,6 @@ async fn update_by_exchange() { .update_test_by_exchange(TestByExchangeQuery { test: Some(1) }, 1) .await .unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row.test, Some(1)); } diff --git a/tests/worktable/unsized_.rs b/tests/worktable/unsized_.rs index c47ea61..c9edf1f 100644 --- a/tests/worktable/unsized_.rs +++ b/tests/worktable/unsized_.rs @@ -591,11 +591,11 @@ async fn update_parallel_more_strings() { h.await.unwrap(); for (id, e) in e_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.exchange, e) } for (id, s) in s_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.some_string, s) } } @@ -684,15 +684,15 @@ async fn update_parallel_more_strings_more_threads() { h2.await.unwrap(); for (id, e) in e_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.exchange, e) } for (id, s) in s_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.some_string, s) } for (id, a) in a_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.another, a) } } @@ -772,11 +772,11 @@ async fn update_parallel_more_strings_with_select_non_unique() { h2.await.unwrap(); for (id, e) in e_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.exchange, e) } for (id, a) in a_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.another, a) } } @@ -840,7 +840,7 @@ async fn delete_parallel() { h2.await.unwrap(); for id in deleted_state.lock_arc().iter() { - let row = table.select(*id); + let row = table.select(*id).await; assert!(row.is_none()) } } @@ -915,7 +915,7 @@ async fn update_parallel_more_strings_with_select_unique() { h2.await.unwrap(); for (id, e) in e_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.exchange, e) } } @@ -969,7 +969,7 @@ async fn upsert_parallel() { h1.await.unwrap(); for (id, e) in e_state.lock_arc().iter() { - let row = table.select(*id).unwrap(); + let row = table.select(*id).await.unwrap(); assert_eq!(&row.exchange, e) } } diff --git a/tests/worktable/uuid.rs b/tests/worktable/uuid.rs index 822d7e9..386e464 100644 --- a/tests/worktable/uuid.rs +++ b/tests/worktable/uuid.rs @@ -10,16 +10,16 @@ worktable! ( } ); -#[test] -fn insert() { +#[tokio::test] +async fn insert() { let table = TestWorkTable::default(); let row = TestRow { id: Uuid::new_v4(), another: 1, }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, row); - assert!(table.select(Uuid::new_v4()).is_none()) + assert!(table.select(Uuid::new_v4()).await.is_none()) } diff --git a/tests/worktable/with_enum.rs b/tests/worktable/with_enum.rs index 9ee9916..346b9d9 100644 --- a/tests/worktable/with_enum.rs +++ b/tests/worktable/with_enum.rs @@ -23,18 +23,18 @@ worktable! ( } ); -#[test] -fn insert() { +#[tokio::test] +async fn insert() { let table = TestWorkTable::default(); let row = TestRow { id: 1, test: SomeEnum::First, }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, row); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } #[tokio::test] @@ -50,8 +50,8 @@ async fn update() { test: SomeEnum::Second, }; table.update(updated.clone()).await.unwrap(); - let selected_row = table.select(pk).unwrap(); + let selected_row = table.select(pk).await.unwrap(); assert_eq!(selected_row, updated); - assert!(table.select(2).is_none()) + assert!(table.select(2).await.is_none()) } From 74f2f9040d481055a0bc8d3311a0b03d701c5b2e Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:51:54 +0300 Subject: [PATCH 17/32] make select async --- codegen/src/worktable/generator/table/index_fns.rs | 8 ++++++-- src/persistence/operation/batch.rs | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/codegen/src/worktable/generator/table/index_fns.rs b/codegen/src/worktable/generator/table/index_fns.rs index 52b88f3..c9c9a50 100644 --- a/codegen/src/worktable/generator/table/index_fns.rs +++ b/codegen/src/worktable/generator/table/index_fns.rs @@ -64,8 +64,12 @@ impl Generator { }; Ok(quote! { - pub fn #fn_name(&self, by: #type_) -> Option<#row_ident> { - let link = self.0.indexes.#field_ident.get(#by).map(|kv| kv.get().value.into())?; + pub async fn #fn_name(&self, by: #type_) -> Option<#row_ident> { + let mut link: Link = self.0.indexes.#field_ident.get(#by).map(|kv| kv.get().value.into())?; + if self.0.lock_manager.await_page_lock(link.page_id).await { + // We waited for vacuum to complete, need to re-lookup the link + link = self.0.indexes.#field_ident.get(#by).map(|kv| kv.get().value.into())?; + } self.0.data.select_non_ghosted(link).ok() } }) diff --git a/src/persistence/operation/batch.rs b/src/persistence/operation/batch.rs index 0ea195e..b52e70f 100644 --- a/src/persistence/operation/batch.rs +++ b/src/persistence/operation/batch.rs @@ -149,6 +149,7 @@ where let pk = self .info_wt .select_by_operation_id(op.operation_id()) + .await .expect("exists as all should be inserted on prepare step") .id; self.info_wt.delete_without_lock(pk.into()).await.unwrap(); From e717e17061fb518198249a6718c8d4095b3594e4 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 22 Jan 2026 23:04:04 +0300 Subject: [PATCH 18/32] fix selects in e2e --- tests/persistence/sync/string_re_read.rs | 20 ++++---- tests/persistence/sync/uuid_.rs | 2 +- tests/worktable/base.rs | 12 ++--- tests/worktable/index/insert.rs | 2 +- tests/worktable/index/update_by_pk.rs | 36 +++++++------- tests/worktable/index/update_full.rs | 60 ++++++++++++------------ tests/worktable/index/update_query.rs | 30 ++++++------ tests/worktable/unsized_.rs | 18 +++---- 8 files changed, 90 insertions(+), 90 deletions(-) diff --git a/tests/persistence/sync/string_re_read.rs b/tests/persistence/sync/string_re_read.rs index 57b6021..7d49271 100644 --- a/tests/persistence/sync/string_re_read.rs +++ b/tests/persistence/sync/string_re_read.rs @@ -154,7 +154,7 @@ fn test_key_delete_scenario() { .len(), 1 ); - assert!(table.select_by_second("second_again".to_string()).is_none()); + assert!(table.select_by_second("second_again".to_string()).await.is_none()); table .insert(StringReReadRow { first: "first".to_string(), @@ -190,7 +190,7 @@ fn test_key_delete_scenario() { .len(), 1 ); - assert!(table.select_by_second("second".to_string()).is_none()); + assert!(table.select_by_second("second".to_string()).await.is_none()); } }) } @@ -265,7 +265,7 @@ fn test_key_delete() { .len(), 1 ); - assert!(table.select_by_second("second_again".to_string()).is_none()) + assert!(table.select_by_second("second_again".to_string()).await.is_none()) } }) } @@ -335,8 +335,8 @@ fn test_key_delete_all() { .len(), 0 ); - assert!(table.select_by_second("second_again".to_string()).is_none()); - assert!(table.select_by_second("second".to_string()).is_none()) + assert!(table.select_by_second("second_again".to_string()).await.is_none()); + assert!(table.select_by_second("second".to_string()).await.is_none()) } }) } @@ -428,7 +428,7 @@ fn test_key_delete_all_and_insert() { .len(), 1 ); - assert!(table.select_by_second("second".to_string()).is_some()) + assert!(table.select_by_second("second".to_string()).await.is_some()) } }) } @@ -502,7 +502,7 @@ fn test_key_delete_by_unique() { .len(), 1 ); - assert!(table.select_by_second("second_again".to_string()).is_none()) + assert!(table.select_by_second("second_again".to_string()).await.is_none()) } }) } @@ -574,8 +574,8 @@ fn test_key_delete_by_non_unique() { .len(), 0 ); - assert!(table.select_by_second("second".to_string()).is_none()); - assert!(table.select_by_second("second_again".to_string()).is_none()) + assert!(table.select_by_second("second".to_string()).await.is_none()); + assert!(table.select_by_second("second_again".to_string()).await.is_none()) } }) } @@ -633,7 +633,7 @@ fn test_big_amount_reread() { .await .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1001); - assert!(table.select_by_second("second_last".to_string()).is_some()); + assert!(table.select_by_second("second_last".to_string()).await.is_some()); } }) } diff --git a/tests/persistence/sync/uuid_.rs b/tests/persistence/sync/uuid_.rs index a10274c..9dc0ed8 100644 --- a/tests/persistence/sync/uuid_.rs +++ b/tests/persistence/sync/uuid_.rs @@ -125,7 +125,7 @@ fn test_big_amount_reread() { .await .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1001); - assert!(table.select_by_second(second_last).is_some()); + assert!(table.select_by_second(second_last).await.is_some()); } }) } diff --git a/tests/worktable/base.rs b/tests/worktable/base.rs index 317f1bb..ef95353 100644 --- a/tests/worktable/base.rs +++ b/tests/worktable/base.rs @@ -256,7 +256,7 @@ async fn update_parallel() { h.await.unwrap(); for (test, val) in i_state.lock_arc().iter() { - let row = table.select_by_test(*test).unwrap(); + let row = table.select_by_test(*test).await.unwrap(); assert_eq!(row.another, *val) } } @@ -281,7 +281,7 @@ async fn delete() { table.delete(pk.clone()).await.unwrap(); let selected_row = table.select(pk).await; assert!(selected_row.is_none()); - let selected_row = table.select_by_test(1); + let selected_row = table.select_by_test(1).await; assert!(selected_row.is_none()); let selected_row = table.select_by_exchange("test".to_string()); assert!(selected_row.execute().expect("REASON").is_empty()); @@ -586,10 +586,10 @@ async fn select_by_test() { exchange: "test".to_string(), }; let _ = table.insert(row.clone()).unwrap(); - let selected_row = table.select_by_test(1).unwrap(); + let selected_row = table.select_by_test(1).await.unwrap(); assert_eq!(selected_row, row); - assert!(table.select_by_test(2).is_none()) + assert!(table.select_by_test(2).await.is_none()) } #[tokio::test] @@ -1191,7 +1191,7 @@ async fn test_update_by_unique() { let row = AnotherByTestQuery { another: 3 }; table.update_another_by_test(row, 1).await.unwrap(); - let row = table.select_by_test(1).unwrap(); + let row = table.select_by_test(1).await.unwrap(); assert_eq!( row, @@ -1218,7 +1218,7 @@ async fn test_update_by_pk() { let row = AnotherByIdQuery { another: 3 }; table.update_another_by_id(row, pk).await.unwrap(); - let row = table.select_by_test(1).unwrap(); + let row = table.select_by_test(1).await.unwrap(); assert_eq!( row, diff --git a/tests/worktable/index/insert.rs b/tests/worktable/index/insert.rs index 62b6ad6..12073bf 100644 --- a/tests/worktable/index/insert.rs +++ b/tests/worktable/index/insert.rs @@ -238,7 +238,7 @@ async fn insert_when_unique_violated() { assert_eq!(attr_1_rows.len(), 1); assert_eq!(attr_1_rows.first().unwrap(), &row); let row_new_attr_2_row = table.select_by_attr2(row_new_attr_2); - assert!(row_new_attr_2_row.is_none()); + assert!(row_new_attr_2_row.await.is_none()); } h.join().unwrap(); diff --git a/tests/worktable/index/update_by_pk.rs b/tests/worktable/index/update_by_pk.rs index bb9c86b..1405b57 100644 --- a/tests/worktable/index/update_by_pk.rs +++ b/tests/worktable/index/update_by_pk.rs @@ -39,19 +39,19 @@ async fn update_by_pk_unique_indexes() { .unwrap(); // Checks idx updated - let updated = test_table.select_by_attr1(attr1_new.clone()); + let updated = test_table.select_by_attr1(attr1_new.clone()).await; assert_eq!(updated.unwrap().attr1, attr1_new); - let updated = test_table.select_by_attr2(attr2_new); + let updated = test_table.select_by_attr2(attr2_new).await; assert_eq!(updated.unwrap().attr2, attr2_new); - let updated = test_table.select_by_attr3(attr3_new); + let updated = test_table.select_by_attr3(attr3_new).await; assert_eq!(updated.unwrap().attr3, attr3_new); // Check old idx removed - let updated = test_table.select_by_attr1(attr1_old.clone()); + let updated = test_table.select_by_attr1(attr1_old.clone()).await; assert_eq!(updated, None); - let updated = test_table.select_by_attr2(attr2_old); + let updated = test_table.select_by_attr2(attr2_old).await; assert_eq!(updated, None); - let updated = test_table.select_by_attr3(attr3_old); + let updated = test_table.select_by_attr3(attr3_old).await; assert_eq!(updated, None); } @@ -156,18 +156,18 @@ async fn update_by_pk_with_reinsert_and_secondary_unique_violation() { ); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); } #[tokio::test] @@ -203,16 +203,16 @@ async fn update_by_pk_with_secondary_unique_violation() { ); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); } diff --git a/tests/worktable/index/update_full.rs b/tests/worktable/index/update_full.rs index 1e3a67a..86c7c56 100644 --- a/tests/worktable/index/update_full.rs +++ b/tests/worktable/index/update_full.rs @@ -36,19 +36,19 @@ async fn update_by_full_row_unique_indexes() { .unwrap(); // Checks idx updated - let updated = test_table.select_by_attr1(attr1_new.clone()); + let updated = test_table.select_by_attr1(attr1_new.clone()).await; assert_eq!(updated.unwrap().attr1, attr1_new); - let updated = test_table.select_by_attr2(attr2_new); + let updated = test_table.select_by_attr2(attr2_new).await; assert_eq!(updated.unwrap().attr2, attr2_new); - let updated = test_table.select_by_attr3(attr3_new); + let updated = test_table.select_by_attr3(attr3_new).await; assert_eq!(updated.unwrap().attr3, attr3_new); // Check old idx removed - let updated = test_table.select_by_attr1(attr1_old.clone()); + let updated = test_table.select_by_attr1(attr1_old.clone()).await; assert_eq!(updated, None); - let updated = test_table.select_by_attr2(attr2_old); + let updated = test_table.select_by_attr2(attr2_old).await; assert_eq!(updated, None); - let updated = test_table.select_by_attr3(attr3_old); + let updated = test_table.select_by_attr3(attr3_old).await; assert_eq!(updated, None); } @@ -177,19 +177,19 @@ async fn update_by_full_row_unique_with_string_update() { .unwrap(); // Checks idx updated - let updated = test_table.select_by_attr1(attr1_new.clone()); + let updated = test_table.select_by_attr1(attr1_new.clone()).await; assert_eq!(updated.unwrap().attr1, attr1_new); - let updated = test_table.select_by_attr2(attr2_new); + let updated = test_table.select_by_attr2(attr2_new).await; assert_eq!(updated.unwrap().attr2, attr2_new); - let updated = test_table.select_by_attr3(attr3_new); + let updated = test_table.select_by_attr3(attr3_new).await; assert_eq!(updated.unwrap().attr3, attr3_new); // Check old idx removed - let updated = test_table.select_by_attr1(attr1_old.clone()); + let updated = test_table.select_by_attr1(attr1_old.clone()).await; assert_eq!(updated, None); - let updated = test_table.select_by_attr2(attr2_old); + let updated = test_table.select_by_attr2(attr2_old).await; assert_eq!(updated, None); - let updated = test_table.select_by_attr3(attr3_old); + let updated = test_table.select_by_attr3(attr3_old).await; assert_eq!(updated, None); } @@ -312,19 +312,19 @@ async fn update_by_full_row_with_reinsert_and_primary_key_violation() { assert_eq!(test_table.select(row1.id).await.unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); assert_eq!(test_table.select(row2.id).await.unwrap(), row2); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); } #[tokio::test] @@ -353,19 +353,19 @@ async fn update_by_full_row_with_reinsert_and_secondary_unique_violation() { assert_eq!(test_table.select(row1.id).await.unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); assert_eq!(test_table.select(row2.id).await.unwrap(), row2); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); } #[tokio::test] @@ -394,17 +394,17 @@ async fn update_by_full_row_with_secondary_unique_violation() { assert_eq!(test_table.select(row1.id).await.unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); assert_eq!(test_table.select(row2.id).await.unwrap(), row2); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); } diff --git a/tests/worktable/index/update_query.rs b/tests/worktable/index/update_query.rs index 43ca8e9..adf0f7f 100644 --- a/tests/worktable/index/update_query.rs +++ b/tests/worktable/index/update_query.rs @@ -40,11 +40,11 @@ async fn update_two_via_query_unique_indexes() { new_row.attr2 = attr2_new; // Check old idx removed - let updated = test_table.select_by_attr1(attr1_old.clone()); + let updated = test_table.select_by_attr1(attr1_old.clone()).await; assert_eq!(updated, None); - let updated = test_table.select_by_attr2(attr2_old); + let updated = test_table.select_by_attr2(attr2_old).await; assert_eq!(updated, None); - let updated = test_table.select_by_attr3(attr3_old); + let updated = test_table.select_by_attr3(attr3_old).await; assert!(updated.is_some()); assert_eq!(updated, Some(new_row)) } @@ -81,18 +81,18 @@ async fn update_with_reinsert_and_secondary_unique_violation() { ); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); } #[tokio::test] @@ -127,18 +127,18 @@ async fn update_with_secondary_unique_violation() { ); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); } #[tokio::test] diff --git a/tests/worktable/unsized_.rs b/tests/worktable/unsized_.rs index c9edf1f..4eb5267 100644 --- a/tests/worktable/unsized_.rs +++ b/tests/worktable/unsized_.rs @@ -50,7 +50,7 @@ async fn test_update_string_full_row() { .await .unwrap(); - let row = table.select_by_test(1).unwrap(); + let row = table.select_by_test(1).await.unwrap(); assert_eq!( row, @@ -81,7 +81,7 @@ async fn test_update_string_by_unique() { }; table.update_exchange_by_test(row, 1).await.unwrap(); - let row = table.select_by_test(1).unwrap(); + let row = table.select_by_test(1).await.unwrap(); assert_eq!( row, @@ -112,7 +112,7 @@ async fn test_update_string_by_pk() { }; table.update_exchange_by_id(row, pk).await.unwrap(); - let row = table.select_by_test(1).unwrap(); + let row = table.select_by_test(1).await.unwrap(); assert_eq!( row, @@ -216,7 +216,7 @@ async fn update_many_times() { } for (test, val) in i_state { - let row = table.select_by_test(test).unwrap(); + let row = table.select_by_test(test).await.unwrap(); assert_eq!(row.exchange, val) } } @@ -284,7 +284,7 @@ async fn update_parallel() { h.await.unwrap(); for (test, val) in i_state.lock_arc().iter() { - let row = table.select_by_test(*test).unwrap(); + let row = table.select_by_test(*test).await.unwrap(); assert_eq!(&row.exchange, val) } } @@ -340,7 +340,7 @@ async fn test_update_many_strings_by_unique() { .await .unwrap(); - let row = table.select_by_test(1).unwrap(); + let row = table.select_by_test(1).await.unwrap(); assert_eq!( row, @@ -376,7 +376,7 @@ async fn test_update_many_strings_by_pk() { }; table.update_exchange_and_some_by_id(row, pk).await.unwrap(); - let row = table.select_by_test(1).unwrap(); + let row = table.select_by_test(1).await.unwrap(); assert_eq!( row, @@ -908,7 +908,7 @@ async fn update_parallel_more_strings_with_select_unique() { }); for _ in 0..20_000 { let val = fastrand::i64(0..1000); - let res = table.select_by_test(val); + let res = table.select_by_test(val).await; assert!(res.is_some()) } h1.await.unwrap(); @@ -963,7 +963,7 @@ async fn upsert_parallel() { }); for _ in 0..20_000 { let val = fastrand::i64(0..1000); - let res = table.select_by_test(val); + let res = table.select_by_test(val).await; assert!(res.is_some()) } h1.await.unwrap(); From 8554bf09733572454eb0e0534e1696d0dbc50b64 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 22 Jan 2026 23:58:40 +0300 Subject: [PATCH 19/32] add tests for parallel selects --- Cargo.toml | 1 + .../src/worktable/generator/table/impls.rs | 8 +- src/table/vacuum/fragmentation_info.rs | 3 +- src/table/vacuum/manager.rs | 85 +++++++++++++++++-- src/table/vacuum/mod.rs | 1 + tests/worktable/mod.rs | 1 + tests/worktable/vacuum.rs | 63 ++++++++++++++ 7 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 tests/worktable/vacuum.rs diff --git a/Cargo.toml b/Cargo.toml index d5e5754..016c9c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ ordered-float = "5.0.0" parking_lot = "0.12.3" prettytable-rs = "^0.10" smart-default = "0.7.1" +log = "0.4.29" [dev-dependencies] rand = "0.9.1" diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index 0364714..9822a52 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -302,8 +302,8 @@ impl Generator { if self.is_persist { quote! { - pub fn vacuum(&self) -> Box { - Box::new(EmptyDataVacuum::< + pub fn vacuum(&self) -> std::sync::Arc { + std::sync::Arc::new(EmptyDataVacuum::< _, _, _, @@ -324,8 +324,8 @@ impl Generator { } } else { quote! { - pub fn vacuum(&self) -> Box { - Box::new(EmptyDataVacuum::new( + pub fn vacuum(&self) -> std::sync::Arc { + std::sync::Arc::new(EmptyDataVacuum::new( #table_name, std::sync::Arc::clone(&self.0.data), std::sync::Arc::clone(&self.0.lock_manager), diff --git a/src/table/vacuum/fragmentation_info.rs b/src/table/vacuum/fragmentation_info.rs index dd6d3b2..4f317bb 100644 --- a/src/table/vacuum/fragmentation_info.rs +++ b/src/table/vacuum/fragmentation_info.rs @@ -45,12 +45,13 @@ impl FragmentationInfo { .map(|i| i.page_size) .unwrap_or(INNER_PAGE_SIZE); let total_empty_bytes: u64 = per_page_info.iter().map(|i| i.empty_bytes as u64).sum(); + let filled_bytes = total_pages as u64 * page_size as u64; Self { page_size, table_name, total_pages, per_page_info, - overall_fragmentation_ratio: total_empty_bytes as f64 / page_size as f64, + overall_fragmentation_ratio: filled_bytes as f64 / total_empty_bytes as f64, total_empty_bytes, } } diff --git a/src/table/vacuum/manager.rs b/src/table/vacuum/manager.rs index 09388bc..1bf44c2 100644 --- a/src/table/vacuum/manager.rs +++ b/src/table/vacuum/manager.rs @@ -1,10 +1,13 @@ -use crate::vacuum::WorkTableVacuum; -use parking_lot::RwLock; -use smart_default::SmartDefault; use std::collections::HashMap; use std::sync::Arc; -use std::sync::atomic::AtomicU64; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; +use tokio::task::AbortHandle; + +use parking_lot::RwLock; +use smart_default::SmartDefault; + +use crate::vacuum::WorkTableVacuum; /// Configuration for [`VacuumManager`]. #[derive(Debug, Clone, SmartDefault)] @@ -26,7 +29,7 @@ pub struct VacuumManager { pub config: VacuumManagerConfig, pub id_gen: AtomicU64, #[debug(ignore)] - pub vacuums: Arc>>>, + pub vacuums: Arc>>>, } impl VacuumManager { @@ -43,4 +46,76 @@ impl VacuumManager { vacuums: Arc::default(), } } + + /// Registers a new vacuum with the manager and returns its unique ID. + pub fn register(&self, table: Arc) -> u64 { + let id = self.id_gen.fetch_add(1, Ordering::AcqRel); + let mut vacuums = self.vacuums.write(); + vacuums.insert(id, table); + id + } + + /// Starts a background task that periodically checks fragmentation and runs + /// vacuum. + /// + /// Returns an `AbortHandle` that can be used to cancel the task. + pub fn run_vacuum_task(self: Arc) -> AbortHandle { + let manager = self.clone(); + + let handle = tokio::spawn(async move { + loop { + tokio::time::sleep(self.config.check_interval).await; + + let vacuums_to_check: Vec<_> = { + let vacuums_read = self.vacuums.read(); + vacuums_read + .iter() + .map(|(id, v)| (*id, v.table_name().to_string())) + .collect() + }; + + for (id, table_name) in vacuums_to_check { + let vacuum_opt = { + let vacuums_read = self.vacuums.read(); + vacuums_read.get(&id).map(|v| v.clone()) + }; + + if let Some(vacuum) = vacuum_opt { + let info = vacuum.analyze_fragmentation(); + + // println!("vacuum info: {:?}", info); + if info.overall_fragmentation_ratio + < self.config.low_fragmentation_threshold + && info.overall_fragmentation_ratio != 0.0 + { + match vacuum.vacuum().await { + Ok(stats) => { + // println!( + // "Vacuum completed for table '{}': {} pages processed, {} bytes freed in {:.2}ms", + // table_name, + // stats.pages_processed, + // stats.bytes_freed, + // stats.duration_ns as f64 / 1_000_000.0 + // ); + log::info!( + "Vacuum completed for table '{}': {} pages processed, {} bytes freed in {:.2}ms", + table_name, + stats.pages_processed, + stats.bytes_freed, + stats.duration_ns as f64 / 1_000_000.0 + ); + } + Err(e) => { + // println!("Vacuum failed for table '{}': {}", table_name, e); + log::error!("Vacuum failed for table '{}': {}", table_name, e); + } + } + } + } + } + } + }); + + handle.abort_handle() + } } diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 42d728f..6f1a365 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -6,6 +6,7 @@ mod fragmentation_info; mod manager; mod vacuum; +pub use manager::{VacuumManager, VacuumManagerConfig}; pub use vacuum::EmptyDataVacuum; /// Trait for unifying different [`WorkTable`] related [`EmptyDataVacuum`]'s. diff --git a/tests/worktable/mod.rs b/tests/worktable/mod.rs index 253569d..ee48e0a 100644 --- a/tests/worktable/mod.rs +++ b/tests/worktable/mod.rs @@ -12,4 +12,5 @@ mod option; mod tuple_primary_key; mod unsized_; mod uuid; +mod vacuum; mod with_enum; diff --git a/tests/worktable/vacuum.rs b/tests/worktable/vacuum.rs new file mode 100644 index 0000000..1e0c3bd --- /dev/null +++ b/tests/worktable/vacuum.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; +use std::time::Duration; +use worktable::prelude::*; +use worktable::vacuum::{VacuumManager, VacuumManagerConfig}; +use worktable_codegen::worktable; + +worktable!( + name: VacuumTest, + columns: { + id: u64 primary_key autoincrement, + value: i64, + data: String + }, + indexes: { + value_idx: value unique, + data_idx: data, + } +); + +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] +async fn vacuum_parallel_with_selects() { + let mut config = VacuumManagerConfig::default(); + config.check_interval = Duration::from_millis(5); + let vacuum_manager = Arc::new(VacuumManager::with_config(config)); + let table = Arc::new(VacuumTestWorkTable::default()); + + // Insert 2000 rows + let mut rows = Vec::new(); + for i in 0..2000 { + let row = VacuumTestRow { + id: table.get_next_pk().into(), + value: i, + data: format!("test_data_{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + rows.push((id, row)); + } + let rows = Arc::new(rows); + + let vacuum = table.vacuum(); + vacuum_manager.register(vacuum); + let _h = vacuum_manager.run_vacuum_task(); + + let delete_table = table.clone(); + let ids_to_delete: Arc> = Arc::new(rows.iter().step_by(2).map(|p| p.0).collect()); + let task_ids = ids_to_delete.clone(); + let delete_task = tokio::spawn(async move { + for id in task_ids.iter() { + delete_table.delete((*id).into()).await.unwrap(); + } + }); + + for _ in 0..10 { + // Verify all remaining rows are still accessible multiple times while vacuuming + for (id, expected) in rows.iter().filter(|(i, _)| !ids_to_delete.contains(i)) { + let row = table.select(*id).await; + assert_eq!(row, Some(expected.clone())); + } + } + + delete_task.await.unwrap(); +} From a8fe5fa21d28e86fe217e6881ad0563bf48e1a52 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:17:46 +0300 Subject: [PATCH 20/32] add tests --- src/table/vacuum/manager.rs | 2 -- src/table/vacuum/vacuum.rs | 21 +++++++---- tests/worktable/vacuum.rs | 69 +++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/table/vacuum/manager.rs b/src/table/vacuum/manager.rs index 1bf44c2..dbee788 100644 --- a/src/table/vacuum/manager.rs +++ b/src/table/vacuum/manager.rs @@ -60,8 +60,6 @@ impl VacuumManager { /// /// Returns an `AbortHandle` that can be used to cancel the task. pub fn run_vacuum_task(self: Arc) -> AbortHandle { - let manager = self.clone(); - let handle = tokio::spawn(async move { loop { tokio::time::sleep(self.config.check_interval).await; diff --git a/src/table/vacuum/vacuum.rs b/src/table/vacuum/vacuum.rs index cf8a77a..e77add9 100644 --- a/src/table/vacuum/vacuum.rs +++ b/src/table/vacuum/vacuum.rs @@ -14,17 +14,17 @@ use rkyv::ser::Serializer; use rkyv::ser::allocator::ArenaHandle; use rkyv::ser::sharing::Share; use rkyv::util::AlignedVec; -use rkyv::{Archive, Serialize}; +use rkyv::{Archive, Deserialize, Serialize}; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; use crate::lock::WorkTableLock; use crate::prelude::{OffsetEqLink, TablePrimaryKey}; -use async_trait::async_trait; - use crate::vacuum::VacuumStats; use crate::vacuum::WorkTableVacuum; use crate::vacuum::fragmentation_info::{FragmentationInfo, PageFragmentationInfo}; use crate::{AvailableIndex, PrimaryIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; +use async_trait::async_trait; +use rkyv::api::high::HighDeserializer; #[derive(Debug)] pub struct EmptyDataVacuum< @@ -89,7 +89,8 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, - <::WrappedRow as Archive>::Archived: GhostWrapper, + <::WrappedRow as Archive>::Archived: GhostWrapper + + Deserialize<::WrappedRow, HighDeserializer>, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc, AvailableIndexes: Debug + AvailableIndex, @@ -387,6 +388,11 @@ where let old_offset_link = OffsetEqLink(old_link); let new_offset_link = OffsetEqLink(new_link); + let row = self + .data_pages + .select(new_link) + .expect("should exist as link was moved correctly"); + self.primary_index .pk_map .insert(pk.clone(), new_offset_link); @@ -394,7 +400,9 @@ where self.primary_index .reverse_pk_map .insert(new_offset_link, pk); - // TODO: update secondary indexes + self.secondary_indexes + .reinsert_row(row.clone(), old_link, row, new_link) + .expect("should be ok as index were no violated"); } /// Creates a new [`EmptyDataVacuum`] from the given [`WorkTable`] components. @@ -454,7 +462,8 @@ where Strategy, Share>, rkyv::rancor::Error>, > + Send + Sync, - <::WrappedRow as Archive>::Archived: GhostWrapper, + <::WrappedRow as Archive>::Archived: GhostWrapper + + Deserialize<::WrappedRow, HighDeserializer>, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc + Send diff --git a/tests/worktable/vacuum.rs b/tests/worktable/vacuum.rs index 1e0c3bd..5046dfc 100644 --- a/tests/worktable/vacuum.rs +++ b/tests/worktable/vacuum.rs @@ -56,8 +56,77 @@ async fn vacuum_parallel_with_selects() { for (id, expected) in rows.iter().filter(|(i, _)| !ids_to_delete.contains(i)) { let row = table.select(*id).await; assert_eq!(row, Some(expected.clone())); + let row = row.unwrap(); + let by_value = table.select_by_value(row.value).await; + assert_eq!(by_value, Some(expected.clone())); } } delete_task.await.unwrap(); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] +async fn vacuum_parallel_with_inserts() { + let mut config = VacuumManagerConfig::default(); + config.check_interval = Duration::from_millis(5); + let vacuum_manager = Arc::new(VacuumManager::with_config(config)); + let table = Arc::new(VacuumTestWorkTable::default()); + + // Insert 2000 rows + let mut rows = Vec::new(); + for i in 0..2000 { + let row = VacuumTestRow { + id: table.get_next_pk().into(), + value: i, + data: format!("test_data_{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + rows.push((id, row)); + } + let rows = Arc::new(rows); + + let vacuum = table.vacuum(); + vacuum_manager.register(vacuum); + let _h = vacuum_manager.run_vacuum_task(); + + let delete_table = table.clone(); + let ids_to_delete: Arc> = Arc::new(rows.iter().step_by(2).map(|p| p.0).collect()); + let task_ids = ids_to_delete.clone(); + let delete_task = tokio::spawn(async move { + for id in task_ids.iter() { + delete_table.delete((*id).into()).await.unwrap(); + } + }); + + let mut inserted_rows = Vec::new(); + for i in 2001..3000 { + let row = VacuumTestRow { + id: table.get_next_pk().into(), + value: i, + data: format!("test_data_{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + inserted_rows.push((id, row)); + } + + // Verify all remaining rows are still accessible + for (id, expected) in rows.iter().filter(|(i, _)| !ids_to_delete.contains(i)) { + let row = table.select(*id).await; + assert_eq!(row, Some(expected.clone())); + let row = row.unwrap(); + let by_value = table.select_by_value(row.value).await; + assert_eq!(by_value, Some(expected.clone())); + } + // Verify all inserted rows are accessible + for (id, expected) in inserted_rows.iter() { + let row = table.select(*id).await; + assert_eq!(row, Some(expected.clone())); + let row = row.unwrap(); + let by_value = table.select_by_value(row.value).await; + assert_eq!(by_value, Some(expected.clone())); + } + + delete_task.await.unwrap(); +} From 9526343277a4eff66c81a286394b5a6680532c81 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:41:20 +0300 Subject: [PATCH 21/32] WIP --- .../src/worktable/generator/queries/update.rs | 46 ++++++++++++-- .../src/worktable/generator/table/impls.rs | 3 +- src/table/select/query.rs | 4 +- src/table/vacuum/manager.rs | 1 + tests/worktable/vacuum.rs | 61 +++++++++++++++++++ 5 files changed, 105 insertions(+), 10 deletions(-) diff --git a/codegen/src/worktable/generator/queries/update.rs b/codegen/src/worktable/generator/queries/update.rs index 9b9d4ae..c1f1516 100644 --- a/codegen/src/worktable/generator/queries/update.rs +++ b/codegen/src/worktable/generator/queries/update.rs @@ -92,11 +92,13 @@ impl Generator { #full_row_lock }; - let link = match self.0 - .primary_index.pk_map + let mut link: Link = match self.0 + .primary_index + .pk_map .get(&pk) .map(|v| v.get().value.into()) - .ok_or(WorkTableError::NotFound) { + .ok_or(WorkTableError::NotFound) + { Ok(l) => l, Err(e) => { lock.unlock(); @@ -106,6 +108,16 @@ impl Generator { } }; + if self.0.lock_manager.await_page_lock(link.page_id).await { + // We waited for vacuum to complete, need to re-lookup the link + link = self.0 + .primary_index + .pk_map + .get(&pk) + .map(|v| v.get().value.into()) + .expect("should be available as was found before vacuum"); + } + let row_old = self.0.data.select_non_ghosted(link)?; self.0.update_state.insert(pk.clone(), row_old); @@ -477,8 +489,9 @@ impl Generator { #custom_lock }; - let link = match self.0 - .primary_index.pk_map + let mut link: Link = match self.0 + .primary_index + .pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound) { @@ -491,6 +504,16 @@ impl Generator { } }; + if self.0.lock_manager.await_page_lock(link.page_id).await { + // We waited for vacuum to complete, need to re-lookup the link + link = self.0 + .primary_index + .pk_map + .get(&pk) + .map(|v| v.get().value.into()) + .expect("should be available as was found before vacuum"); + } + let mut bytes = rkyv::to_bytes::(&row).map_err(|_| WorkTableError::SerializeError)?; let mut archived_row = unsafe { rkyv::access_unchecked_mut::<<#query_ident as rkyv::Archive>::Archived>(&mut bytes[..]).unseal_unchecked() }; @@ -708,10 +731,21 @@ impl Generator { .unseal_unchecked() }; - let link = self.0.indexes.#index + let mut link: Link = self.0.indexes + .#index .get(#by) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; + + if self.0.lock_manager.await_page_lock(link.page_id).await { + // We waited for vacuum to complete, need to re-lookup the link + link = self.0.indexes + .#index + .get(#by) + .map(|v| v.get().value.into()) + .expect("should be available as was found before vacuum"); + } + let pk = self.0.data.select_non_ghosted(link)?.get_primary_key().clone(); let lock = { diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index 9822a52..5ce1e59 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -176,8 +176,7 @@ impl Generator { pub async fn upsert(&self, row: #row_type) -> core::result::Result<(), WorkTableError> { let pk = row.get_primary_key(); let need_to_update = { - if let Some(_) = self.0.primary_index.pk_map.get(&pk) - { + if let Some(link) = self.0.primary_index.pk_map.get(&pk) { true } else { false diff --git a/src/table/select/query.rs b/src/table/select/query.rs index 491c08a..ca7bf40 100644 --- a/src/table/select/query.rs +++ b/src/table/select/query.rs @@ -1,8 +1,8 @@ -use std::collections::VecDeque; - use crate::WorkTableError; use crate::select::{Order, QueryParams}; +use std::collections::VecDeque; + pub struct SelectQueryBuilder where I: DoubleEndedIterator + Sized, diff --git a/src/table/vacuum/manager.rs b/src/table/vacuum/manager.rs index dbee788..b7c2d9d 100644 --- a/src/table/vacuum/manager.rs +++ b/src/table/vacuum/manager.rs @@ -81,6 +81,7 @@ impl VacuumManager { if let Some(vacuum) = vacuum_opt { let info = vacuum.analyze_fragmentation(); + log::info!("vacuum info: {:?}", info); // println!("vacuum info: {:?}", info); if info.overall_fragmentation_ratio < self.config.low_fragmentation_threshold diff --git a/tests/worktable/vacuum.rs b/tests/worktable/vacuum.rs index 5046dfc..a0008d2 100644 --- a/tests/worktable/vacuum.rs +++ b/tests/worktable/vacuum.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use worktable::prelude::*; @@ -130,3 +131,63 @@ async fn vacuum_parallel_with_inserts() { delete_task.await.unwrap(); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] +async fn vacuum_parallel_with_upserts() { + let mut config = VacuumManagerConfig::default(); + config.check_interval = Duration::from_millis(5); + let vacuum_manager = Arc::new(VacuumManager::with_config(config)); + let table = Arc::new(VacuumTestWorkTable::default()); + + // Insert 3000 rows + let mut rows = Vec::new(); + for i in 0..3000 { + let row = VacuumTestRow { + id: table.get_next_pk().into(), + value: i, + data: format!("test_data_{}", i), + }; + let id = row.id; + table.insert(row.clone()).unwrap(); + rows.push((id, row)); + } + let rows = Arc::new(rows); + + let vacuum = table.vacuum(); + vacuum_manager.register(vacuum); + let _h = vacuum_manager.run_vacuum_task(); + + let delete_table = table.clone(); + let ids_to_delete: Arc> = Arc::new(rows.iter().step_by(2).map(|p| p.0).collect()); + let task_ids = ids_to_delete.clone(); + let delete_task = tokio::spawn(async move { + for id in task_ids.iter() { + delete_table.delete((*id).into()).await.unwrap(); + } + }); + + let mut row_state = rows.iter().cloned().collect::>(); + for _ in 0..3000 { + let id = fastrand::u64(0..3000); + let i = fastrand::i64(0..3000); + let row = VacuumTestRow { + id, + value: id as i64, + data: format!("test_data_{}", i), + }; + let id = row.id; + table.upsert(row.clone()).await.unwrap(); + row_state.entry(id).and_modify(|r| *r = row); + } + + // Verify all inserted rows are accessible + for (id, expected) in row_state.iter() { + let row = table.select(*id).await; + assert_eq!(row, Some(expected.clone())); + let row = row.unwrap(); + let by_value = table.select_by_value(row.value).await; + assert_eq!(by_value, Some(expected.clone())); + } + + delete_task.await.unwrap(); +} From 8942db0ebf9c911d0a8371275cb01d78f720f138 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:28:44 +0300 Subject: [PATCH 22/32] WIP --- codegen/src/worktable/generator/wrapper.rs | 21 ++ idea.md | 64 ----- old_README.md | 304 --------------------- other_db.md | 37 --- src/in_memory/data.rs | 37 ++- src/in_memory/mod.rs | 2 +- src/in_memory/pages.rs | 156 ++++++++++- src/in_memory/row.rs | 5 + src/lib.rs | 4 +- src/lock/mod.rs | 2 - src/lock/table_lock.rs | 222 --------------- src/table/mod.rs | 2 +- src/table/vacuum/fragmentation_info.rs | 15 +- src/table/vacuum/vacuum.rs | 231 +++------------- x.md | 16 -- 15 files changed, 265 insertions(+), 853 deletions(-) delete mode 100644 idea.md delete mode 100644 old_README.md delete mode 100644 other_db.md delete mode 100644 src/lock/table_lock.rs delete mode 100644 x.md diff --git a/codegen/src/worktable/generator/wrapper.rs b/codegen/src/worktable/generator/wrapper.rs index b77f0ad..2a54b72 100644 --- a/codegen/src/worktable/generator/wrapper.rs +++ b/codegen/src/worktable/generator/wrapper.rs @@ -9,12 +9,14 @@ impl Generator { let impl_ = self.gen_wrapper_impl(); let storable_impl = self.get_wrapper_storable_impl(); let ghost_wrapper_impl = self.get_wrapper_ghost_impl(); + let vacuum_wrapper_impl = self.get_wrapper_vacuum_impl(); quote! { #type_ #impl_ #storable_impl #ghost_wrapper_impl + #vacuum_wrapper_impl } } @@ -30,6 +32,7 @@ impl Generator { inner: #row_ident, is_ghosted: bool, is_deleted: bool, + is_in_vacuum_process: bool, } } } @@ -50,11 +53,16 @@ impl Generator { self.is_ghosted } + fn is_vacuumed(&self) -> bool { + self.is_in_vacuum_process + } + fn from_inner(inner: #row_ident) -> Self { Self { inner, is_ghosted: true, is_deleted: false, + is_in_vacuum_process: false, } } } @@ -85,4 +93,17 @@ impl Generator { } } } + + fn get_wrapper_vacuum_impl(&self) -> TokenStream { + let name_generator = WorktableNameGenerator::from_table_name(self.name.to_string()); + let row_ident = name_generator.get_archived_wrapper_type_ident(); + + quote! { + impl VacuumWrapper for #row_ident { + fn set_in_vacuum_process(&mut self) { + self.is_in_vacuum_process = true; + } + } + } + } } diff --git a/idea.md b/idea.md deleted file mode 100644 index 2f3ce03..0000000 --- a/idea.md +++ /dev/null @@ -1,64 +0,0 @@ -I think we can use some logic, that will give us differences between current row state and state we need and we will -use this data to update indexes. - -We can have some object like - -```rust -pub struct Difference { - // Any.... will have some notes about it below, I think it's some open question for now. - old_value: Box, - new_value: Box, -} -``` - -And we will have some trait `Comparable` - -```rust -pub trait Comparable { - fn compare(&self, with: With) -> HashMap<&'static str, Difference>; -} -``` - -It will be used to compare `Row` with `Row` and some query values with row. It will return `Difference`s that can be -used -in secondary index object. - -Main issue about this is that `Difference` will have different types for different row columns. So we need some way to -unify this, because we can't just have some `Difference`. So first option is `Any`. We can just `downcast_ref` for -type we need because we will be fully sure that types will be correct. - -Second option is enum. We can generate private enum like `AvailableType` for every table like: - -```rust -enum AvailableType { - U64(u64), - I64(i64) -} -``` - -And our Difference will become: - -```rust -pub struct Difference { - old_value: AvailableType, - new_value: AvailableType, -} -``` - -You can choose between two of this. - -Next step is `TableSecondaryIndex` trait update. It will become something like: - -```rust -pub trait TableSecondaryIndex { - fn save_row(&self, row: Row, link: Link) -> Result<(), WorkTableError>; - - fn delete_row(&self, row: Row, link: Link) -> Result<(), WorkTableError>; - - fn process_differences(&self, differences: HashMap) -> Result<(), WorkTableError>; -} -``` - -This method will be easily generated via codegen as I think. We can easily generate map from index name to field with -index -to update. \ No newline at end of file diff --git a/old_README.md b/old_README.md deleted file mode 100644 index 56f181d..0000000 --- a/old_README.md +++ /dev/null @@ -1,304 +0,0 @@ -# Absolutely not a Database (WorkTable) - -## What we have for now - -WorkTable macro for generating type alias and row type. - -```rust - worktable! ( - name: Test, - columns: { - id: u64 primary_key, - test: i64 - } - ); -``` - -Expanded as: - -```rust -#[derive(Debug, Clone)] - pub struct TestRow { - id: u64, - test: i64, - } - -impl worktable::TableRow for TestRow { fn get_primary_key(&self) -> &u64 { &self.id } } - -type TestWorkTable = worktable::WorkTable; -``` - -* Underlying structure as `Vec` with no sync algorithms, `BTreeMap` for primary key. - -## TODO parts: - -1. [Underlying structure refactor.](#underlying-structure-refactor) -2. [Query macros support.](#query-macros-support) -3. [Persistence support.](#persistence-support) - -## Underlying structure refactor. - -We have big amount off nearly small objects, so we need to control allocations to optimize theirs creation. For -achieving this we can store table's data on `Pages`. - -### Pages - -Pages are byte arrays of some size (4Kb as minimal value, it's better to choose this value from disk storage page size). -Data stored in these pages in some deserialized format (some kind of binary format. **rkyv** can be used for -serialization and deserialization). - -```rust -struct Page { - data: [u8; PAGE_SIZE], - - info: PageInfo, // Some info about `Page` like it's index etc. -} -``` - -To navigate on pages _link_'s can be used. - -```rust -struct Link { - /// Id of the page where data is located. - page_id: u64, - - /// Offset ona page (< PAGE_SIZE, so u16 will be enough up to 64Kb page) - offset: u16, - - /// Length of the data. For rows this will be same, so maybe not used. - len: u16, -} -``` - -### Empty link storage - -When we added some data and then deleted we will have a gap in bytes, and we need to control this gaps and fill them -with data. If we have rows-based storage, all rows will have same length, so when old is deleted, we can easily -replace it with new one. So we need some storage (stack) for this empty links. - -Lock-free stack (using atomics) can be used here, because we don't want to lock on new row addition. - -### Defragmentation (?) - -When count of empty link will massively grow, we will have empty pages or big gaps in pages data. So, if we need, we can -have defragmentation algorithm that will tighten data and delete gaps. - -### Locks on operations - -As was said before we don't want to lock on addition, so we can use atomic for page's tail, and lock-free stack for -empty links. On row addition we wil first check empty link registry, and use link popped from it if there is some. If -there is no links, we will use current tails index (which will be stored in atomic) and use it for new data storage. -We possibly can have locks only on new page allocation. - -Updates and deletes will be always lock actions. - -So, for lock control we can use map that will map row's primary key to it's `RwLock`. On new data addition. For indexes -lock-free map can be used. - -Upd. Maybe atomic pointers can be used here, but I'm no sure. - -So, modifying algorithm will look like: we get `RwLock` by row's primary key (or index key), then modify row, and\ -releasing lock. - -Upd. No locks for updating/deleting. Delete as flag for row, update as delete + insert. - -### Filtering data - -I think for filtering we will need to copy table, which is bad. I need to think about filtering more to make it more -effective. - -### Foreign keys - -Foreign keys can be implemented as map from key-id to other's table row link. So join operation on row can be done by -O(1). - -## Query macros support. - -We need to extend macro usage to minimize client boilerplate code. Example: -```rust -{ -worktable!( - name: Price - columns: { - id: u64 primary key autoincrement, - exchange: u8, - level: u8, - asks_price: f64, - bids_price: f64, - asks_qty: f64, - bids_qty: f64, - timestamp: u64, - } - queries: { - select: { - // similar to SELECT bids_price, bids_qty FROM price where exchange=$1 - BidsPriceQty("bids_price", "bids_qty") by "exchange" as bids_price_by_exchange, // name override - // similar to SELECT bids_price, bids_qty FROM price where bids_price>$1 - BidsPriceQty("bids_price", "bids_qty") by "bids_price" > as bids_price_above, - // similar to SELECT bids_price, bids_qty FROM price where timestamp>$1 and timestamp<$2 - BidsPriceQty("bids_price", "bids_qty") by "timestamp" > and "timestamp" < as bids_price_by_date, - } - } -); - -let price_table = PriceWorkTable::new(); - -// Result is multiple rows. -// without override price_table.bids_price_qty_by_exchange() -let binance_orders: BidsPriceByExchange = price_table.bids_price_by_exchange(Exchange::BinanceSpot as u8); - -// Result is multiple rows. -// without override price_table.bids_price_qty_by_bids_price_more() -let binance_orders: BidsPriceAbove = price_table.bids_price_above(1000.0); - -// Result is still multiple rows. -// without override price_table.bids_price_qty_by_timestamp_more_and_timestamp_less() -let binance_orders: BidsPriceByDate = price_table.bids_price_by_date(123312341, 1234128345); -} -``` - -As inspiration for macro interfaces/design [this crate](https://github.com/helsing-ai/atmosphere) can be used. -It contains derive macro implementation, but it's still usable for our case. - - - -## Persistence support. - -Next step after in-memory storage we need to add persistence support. - -For starting point we can use [mmap-sync](https://github.com/cloudflare/mmap-sync/tree/main) which has mapped files -implementation and read/write interface. We will need pages reader/writer for our storage engine. - -### Data container format - -As starting point innodb format was chosen. We can use it for storing tables data -([leaf pages](https://github.com/Codetector1374/InnoDB_rs/blob/master/src/innodb/page/mod.rs) of b-tree must have nearly -same layout). - -#### Page format - -Original innodb [format](https://blog.jcole.us/2013/01/03/the-basics-of-innodb-space-file-layout/) - -General `Page` layout: - -```text -+----------------------+---------+ -| Offset (Page number) | 4 bytes | -+----------------------+---------+ -| Previous page ID | 4 bytes | -+----------------------+---------+ -| Next page ID | 4 bytes | -+----------------------+---------+ -| Page type | 2 bytes | -+----------------------+---------+ -| Space ID | 4 bytes | -+----------------------+---------+ -| Page data | n bytes | -+----------------------+---------+ -``` - -Total header length is `18 bytes`. - -* Offset is current `Page`'s ID, in code will be represented as `u32` (4,294,967,295 available pages). -* Previous page ID is ID of previous _logical_ `Page`. -* Next page ID is ID of next _logical_ `Page`. These IDs are used to form doubly-linked list from pages in one file. -* Page type describes type of this page (TODO: Describe pages types) -* Space ID is ID of file (Space) to which this `Page` is related to. -* Page data is just pages internal data. - -Comparison with original InnoDB: -* No checksum part in header (we don't care about this). -* No LSN page modification header -* No flush LSN header -* No `Page` trailer - -#### File layout - -Original InnoDB [format](https://blog.jcole.us/2013/01/04/page-management-in-innodb-space-files/) - -For each table separate file will be used. This files will be named as `Space`'s. Each space will have some general -structure. - -General `Space` layout: - -```text -+-------------------------------+---------+ -| Space internals page | 1 Page | -+-------------------------------+---------+ -| Space Pages | n Pages | -+-------------------------------+---------+ -``` - -`Space` internal page: - -```text -+-----------------------+-----------+ -| General page header | 18 bytes | -+-----------------------+-----------+ -| Space header | 12 bytes | -+-----------------------+-----------+ -| Table schema | n bytes | -+-----------------------+-----------+ -``` - -As each `Space` is related to separate `Table`, it must contain this `Table`'s schema to validate data and row structure. - -`Space` header: - -```text -+-------------------------------+---------+ -| Space ID | 4 bytes | -+-------------------------------+---------+ -| Highest used Page number | 4 bytes | -+-------------------------------+---------+ -| Highest allocated Page number | 4 bytes | -+-------------------------------+---------+ -``` - -#### Page types - -Original InnoDB page [types](https://github.com/Codetector1374/InnoDB_rs/blob/6a153a7185feb31e8a31369c9671c4497f56e1c7/src/innodb/page/mod.rs#L99C3-L99C4) - -```rust -#[repr(u16)] -enum PageType { - /// Newly allocated pages. - Free = 0, - /// Space header `Page` type. - SpaceHeader = 1, - /// Table data `Page` type. - Data = 2, - /// Index `Page` type. - Index = 3, -} -``` - -### Not sized types (strings) - -Strings must be stored as varchars, we don't need to have empty padding in rows to have same width. We can have -different row width, because we will hae links for the rows which contain its length. So we don't need this empty tail. - -Same logic can be used for not sized array data types. - -### Files read/write logic - -For [fastest way possible to read bytes](https://users.rust-lang.org/t/fastest-way-possible-to-read-bytes/86177) we need -multiple opened read/write threads for one file. Using this we can update different parts of file at one time. To use -this io_uring can be used. For this approach we have multiple solutions: - -* https://github.com/ringbahn/iou - Interface to Linux's io_uring interface -* https://github.com/bytedance/monoio - A thread-per-core Rust runtime with io_uring/epoll/kqueue. It's goal is to - replace Tokio. They have benchmarks that proves that they are _blazingly_ fast [wow](https://github.com/bytedance/monoio/blob/master/docs/en/benchmark.md). -* https://github.com/tokio-rs/io-uring - tokios io-uring raw interface. Is unsafe. -* https://github.com/tokio-rs/tokio-uring - io-uring in Tokio runtime. Safe. -* https://github.com/compio-rs/compio - inspired by MonoIO crate. Don't think we rally need this because it's feature is - that this crates supports Windows. Do we need Windows? - -[Discussion](https://users.rust-lang.org/t/file-reading-async-sync-performance-differences-hyper-tokio/34696) about -difference between sync and async i/o (5 years ago before io_uring was added). - -### Bunch of links that must be sorted................... - -https://crates.io/crates/faster-hex - where do we need to use hex? I don't know............ - - diff --git a/other_db.md b/other_db.md deleted file mode 100644 index 79ef682..0000000 --- a/other_db.md +++ /dev/null @@ -1,37 +0,0 @@ -* https://github.com/rust-lib-project/calibur - file system code https://github.com/rust-lib-project/calibur/tree/main/src/common/file_system. -Strange, only read/write logic. https://github.com/rust-lib-project/calibur/blob/main/src/table/block_based/table_builder.rs -write logic usage for table. - -* https://gitlab.com/persy/persy has only sync file interface, no async. It's bad.... - -* https://www.sqlite.org/fileformat.html useful file format. Pretty simple and good. - -* https://github.com/cloudflare/mmap-sync/tree/main uses memory mapped files. Is used to save some abstract data, can -be refactored for our use case. Also memory mapped files usage is good example. - -* https://github.com/naoto0822/mysql-parser-rs MySql dialect query parser. not usable at all. - - .--------------------------------------------------------------------------------------------------------------------- - -* https://github.com/zombodb/zombodb/ not usable, just a wrapper around postgres + elastic. - -* https://github.com/zeedb/ZeeDB/?tab=readme-ov-file not usable, no disk write logic, sync lockable pages. - -* https://github.com/erikgrinaker/toydb/blob/master/src/storage/bitcask.rs key-value storage. append-only, don't think -it's really useful - -* https://github.com/oxigraph/oxigraph/tree/main nothing useful - -* https://github.com/vincent-herlemont/native_db/tree/main key-value, nothing useful. - -* https://github.com/helsing-ai/atmosphere useful macro parts and interface. - -* https://github.com/tontinton/dbeel/tree/main uses glommio with io_uring, can be useful - -* https://github.com/cutsea110/simpledb/tree/master sync filesystem using mutexes. not think can be usable - -* https://github.com/influxdata/influxdb/tree/main/influxdb3_write/src some useful parts can be found, but it's sync. - -* https://github.com/tikv/tikv key-value, nothing useful. - -* https://github.com/PoloDB/PoloDB/tree/master rocksdb wrapper. \ No newline at end of file diff --git a/src/in_memory/data.rs b/src/in_memory/data.rs index 50c6edc..8b131b8 100644 --- a/src/in_memory/data.rs +++ b/src/in_memory/data.rs @@ -52,7 +52,7 @@ pub struct Data { /// [`Id]: PageId /// [`General`]: page::General #[rkyv(with = Skip)] - id: PageId, + pub id: PageId, /// Offset to the first free byte on this [`Data`] page. #[rkyv(with = AtomicLoad)] @@ -233,10 +233,6 @@ impl Data { Ok(unsafe { rkyv::access_unchecked::<::Archived>(bytes) }) } - //#[cfg_attr( - // feature = "perf_measurements", - // performance_measurement(prefix_name = "DataRow") - //)] pub fn get_row(&self, link: Link) -> Result where Row: Archive, @@ -325,6 +321,10 @@ impl Data { pub fn free_space(&self) -> usize { DATA_LENGTH.saturating_sub(self.free_offset.load(Ordering::Acquire) as usize) } + + pub fn reset(&self) { + self.free_offset.store(0, Ordering::Release); + } } /// Error that can appear on [`Data`] page operations. @@ -356,6 +356,7 @@ mod tests { use rkyv::{Archive, Deserialize, Serialize}; + use crate::in_memory::DATA_INNER_LENGTH; use crate::in_memory::data::{Data, ExecutionError, INNER_PAGE_SIZE}; use crate::prelude::Link; @@ -695,4 +696,30 @@ mod tests { assert_eq!(retrieved, row); } } + + #[test] + fn reset_clears_free_offset() { + let page = Data::::new(1.into()); + + let row1 = TestRow { a: 10, b: 20 }; + let row2 = TestRow { a: 30, b: 40 }; + let link1 = page.save_row(&row1).unwrap(); + let link2 = page.save_row(&row2).unwrap(); + + assert!(page.free_offset.load(Ordering::Relaxed) > 0); + assert_eq!(link1.offset, 0); + assert_eq!(link2.offset, 16); + + page.reset(); + + assert_eq!(page.free_offset.load(Ordering::Relaxed), 0); + assert_eq!(page.free_space(), DATA_INNER_LENGTH); + + let row3 = TestRow { a: 99, b: 88 }; + let link3 = page.save_row(&row3).unwrap(); + assert_eq!(link3.offset, 0); + + let retrieved = page.get_row(link3).unwrap(); + assert_eq!(retrieved, row3); + } } diff --git a/src/in_memory/mod.rs b/src/in_memory/mod.rs index c032ea1..04ad8a8 100644 --- a/src/in_memory/mod.rs +++ b/src/in_memory/mod.rs @@ -6,4 +6,4 @@ mod row; pub use data::{DATA_INNER_LENGTH, Data, ExecutionError as DataExecutionError}; pub use empty_link_registry::EmptyLinkRegistry; pub use pages::{DataPages, ExecutionError as PagesExecutionError}; -pub use row::{GhostWrapper, Query, RowWrapper, StorableRow}; +pub use row::{GhostWrapper, Query, RowWrapper, StorableRow, VacuumWrapper}; diff --git a/src/in_memory/pages.rs b/src/in_memory/pages.rs index 081ce9d..1085ad6 100644 --- a/src/in_memory/pages.rs +++ b/src/in_memory/pages.rs @@ -200,6 +200,15 @@ where } } + /// Allocates new page but **NOT** sets it as `current`. + pub fn allocate_new_page(&self) -> Arc::WrappedRow, DATA_LENGTH>> { + let mut pages = self.pages.write(); + let index = self.last_page_id.fetch_add(1, Ordering::AcqRel) + 1; + let page = Arc::new(Data::new(index.into())); + pages.push(page.clone()); + page + } + #[cfg_attr( feature = "perf_measurements", performance_measurement(prefix_name = "DataPages") @@ -215,7 +224,6 @@ where { let pages = self.pages.read(); let page = pages - // - 1 is used because page ids are starting from 1. .get(page_id_mapper(link.page_id.into())) .ok_or(ExecutionError::PageNotFound(link.page_id))?; let gen_row = page.get_row(link).map_err(ExecutionError::DataPageError)?; @@ -233,7 +241,6 @@ where { let pages = self.pages.read(); let page = pages - // - 1 is used because page ids are starting from 1. .get(page_id_mapper(link.page_id.into())) .ok_or(ExecutionError::PageNotFound(link.page_id))?; let gen_row = page.get_row(link).map_err(ExecutionError::DataPageError)?; @@ -327,9 +334,7 @@ where } pub fn delete(&self, link: Link) -> Result<(), ExecutionError> { - //println!("Pushing empty link"); self.empty_links.push(link); - //println!("Pushed empty link"); Ok(()) } @@ -343,8 +348,10 @@ where } pub fn mark_page_empty(&self, page_id: PageId) { - let mut g = self.empty_pages.write(); - g.push_back(page_id); + if u32::from(page_id) != self.current_page_id.load(Ordering::Acquire) { + let mut g = self.empty_pages.write(); + g.push_back(page_id); + } } pub fn get_empty_pages(&self) -> Vec { @@ -416,7 +423,7 @@ mod tests { use rkyv::{Archive, Deserialize, Serialize}; use crate::in_memory::pages::DataPages; - use crate::in_memory::{PagesExecutionError, RowWrapper, StorableRow}; + use crate::in_memory::{DATA_INNER_LENGTH, PagesExecutionError, RowWrapper, StorableRow}; #[derive( Archive, Copy, Clone, Deserialize, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, @@ -437,6 +444,10 @@ mod tests { #[rkyv(with = AtomicLoad)] pub is_ghosted: AtomicBool, + /// Indicator for vacuumed rows. + #[rkyv(with = AtomicLoad)] + pub is_vacuumed: AtomicBool, + /// Indicator for deleted rows. #[rkyv(with = AtomicLoad)] pub deleted: AtomicBool, @@ -451,11 +462,16 @@ mod tests { self.is_ghosted.load(Ordering::Relaxed) } + fn is_vacuumed(&self) -> bool { + self.is_vacuumed.load(Ordering::Relaxed) + } + /// Creates new [`GeneralRow`] from `Inner`. fn from_inner(inner: Inner) -> Self { Self { inner, is_ghosted: AtomicBool::new(true), + is_vacuumed: AtomicBool::new(false), deleted: AtomicBool::new(false), } } @@ -645,4 +661,130 @@ mod tests { println!("vec {elapsed:?}") } + + #[test] + fn allocate_new_page_creates_page_correctly() { + let pages = DataPages::::new(); + + let initial_last_id = pages.last_page_id.load(Ordering::Relaxed); + let initial_current = pages.current_page_id.load(Ordering::Relaxed); + let initial_count = pages.get_page_count(); + + let _allocated_page = pages.allocate_new_page(); + + assert_eq!( + pages.last_page_id.load(Ordering::Relaxed), + initial_last_id + 1 + ); + + assert_eq!( + pages.current_page_id.load(Ordering::Relaxed), + initial_current, + "current_page_id should NOT change after allocate_new_page" + ); + + assert_eq!(pages.get_page_count(), initial_count + 1); + + let retrieved_page = pages.get_page((initial_last_id + 1).into()); + assert!(retrieved_page.is_some()); + } + + #[test] + fn allocate_multiple_new_pages() { + let pages = DataPages::::new(); + + let initial_last_id = pages.last_page_id.load(Ordering::Relaxed); + let initial_current = pages.current_page_id.load(Ordering::Relaxed); + + let _page2 = pages.allocate_new_page(); + let _page3 = pages.allocate_new_page(); + let _page4 = pages.allocate_new_page(); + + assert_eq!( + pages.last_page_id.load(Ordering::Relaxed), + initial_last_id + 3 + ); + assert_eq!( + pages.current_page_id.load(Ordering::Relaxed), + initial_current + ); + assert_eq!(pages.get_page_count(), 4); + } + + #[test] + fn insert_continues_on_current_page_after_allocation() { + let pages = DataPages::::new(); + + pages.allocate_new_page(); + + let row = TestRow { a: 42, b: 99 }; + let link = pages.insert(row).unwrap(); + + assert_eq!(link.page_id, 1.into()); + } + + #[test] + fn allocate_new_page_concurrent() { + let pages = Arc::new(DataPages::::new()); + let mut handles = Vec::new(); + + for _ in 0..10 { + let pages_clone = pages.clone(); + let handle = thread::spawn(move || { + for _ in 0..10 { + pages_clone.allocate_new_page(); + } + }); + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + + assert_eq!(pages.get_page_count(), 101); + assert_eq!(pages.last_page_id.load(Ordering::Relaxed), 101); + } + + #[test] + fn allocated_page_has_correct_initial_state() { + let pages = DataPages::::new(); + + let allocated = pages.allocate_new_page(); + + assert_eq!(allocated.free_offset.load(Ordering::Relaxed), 0); + assert_eq!(allocated.free_space(), DATA_INNER_LENGTH); + } + + #[test] + fn skips_explicitly_allocated_page() { + let pages = DataPages::::new(); + + // Allocate page explicitly + pages.allocate_new_page(); + assert_eq!(pages.last_page_id.load(Ordering::Relaxed), 2); + assert_eq!(pages.current_page_id.load(Ordering::Relaxed), 1); + + loop { + let row = TestRow { + a: 42, + b: pages.row_count.load(Ordering::Relaxed), + }; + let link = pages.insert(row).unwrap(); + if link.page_id != 1.into() { + break; + } + } + + let row = TestRow { a: 999, b: 888 }; + let new_link = pages.insert(row).unwrap(); + + assert_eq!( + new_link.page_id, + 3.into(), + "New insert should go to page 3, not page 2" + ); + assert_eq!(pages.current_page_id.load(Ordering::Relaxed), 3); + assert_eq!(pages.get_page_count(), 3); + } } diff --git a/src/in_memory/row.rs b/src/in_memory/row.rs index 2ef9bf0..c332415 100644 --- a/src/in_memory/row.rs +++ b/src/in_memory/row.rs @@ -12,6 +12,7 @@ pub trait StorableRow { pub trait RowWrapper { fn get_inner(self) -> Inner; fn is_ghosted(&self) -> bool; + fn is_vacuumed(&self) -> bool; fn from_inner(inner: Inner) -> Self; } @@ -19,6 +20,10 @@ pub trait GhostWrapper { fn unghost(&mut self); } +pub trait VacuumWrapper { + fn set_in_vacuum_process(&mut self); +} + pub trait Query { fn merge(self, row: Row) -> Row; } diff --git a/src/lib.rs b/src/lib.rs index be174b4..7c9c0f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,9 @@ pub use table::*; pub use worktable_codegen::worktable; pub mod prelude { - pub use crate::in_memory::{Data, DataPages, GhostWrapper, Query, RowWrapper, StorableRow}; + pub use crate::in_memory::{ + Data, DataPages, GhostWrapper, Query, RowWrapper, StorableRow, VacuumWrapper, + }; pub use crate::lock::LockMap; pub use crate::lock::{Lock, RowLock}; pub use crate::mem_stat::MemStat; diff --git a/src/lock/mod.rs b/src/lock/mod.rs index a12c66f..7dbe0d0 100644 --- a/src/lock/mod.rs +++ b/src/lock/mod.rs @@ -1,6 +1,5 @@ mod map; mod row_lock; -mod table_lock; use std::future::Future; use std::hash::{Hash, Hasher}; @@ -14,7 +13,6 @@ use parking_lot::Mutex; pub use map::LockMap; pub use row_lock::{FullRowLock, RowLock}; -pub use table_lock::WorkTableLock; #[derive(Debug)] pub struct Lock { diff --git a/src/lock/table_lock.rs b/src/lock/table_lock.rs deleted file mode 100644 index 0c28f5d..0000000 --- a/src/lock/table_lock.rs +++ /dev/null @@ -1,222 +0,0 @@ -use std::fmt::Debug; -use std::hash::Hash; -use std::sync::Arc; - -use data_bucket::page::PageId; - -use crate::lock::map::LockMap; -use crate::lock::row_lock::{FullRowLock, RowLock}; - -/// Unified lock manager for WorkTable operations. -/// -/// Combines row-level locking with -/// page-level locking (for defragmentation operations). -#[derive(Debug)] -pub struct WorkTableLock { - pub row_locks: LockMap, - pub vacuum_lock: Arc>, -} - -impl Default for WorkTableLock { - fn default() -> Self { - Self { - row_locks: LockMap::default(), - vacuum_lock: Arc::new(LockMap::default()), - } - } -} - -impl WorkTableLock -where - PrimaryKey: Hash + Eq + Debug + Clone, -{ - /// Locks a page for vacuum operations, returning the [`FullRowLock`]. - /// - /// If a lock already exists for the page, it returns the existing lock. - /// Otherwise, creates a new lock. - pub fn lock_page(&self, page_id: PageId) -> Arc> { - if let Some(lock) = self.vacuum_lock.get(&page_id) { - return lock; - } - - let (row_lock, _) = FullRowLock::with_lock(self.vacuum_lock.next_id()); - let lock = Arc::new(tokio::sync::RwLock::new(row_lock)); - self.vacuum_lock.insert(page_id, lock.clone()); - lock - } - - pub fn get_page_lock(&self, page_id: PageId) -> Option>> { - self.vacuum_lock.get(&page_id) - } - - pub fn remove_page_lock(&self, page_id: &PageId) { - self.vacuum_lock.remove_with_lock_check(page_id); - } - - /// Checks if a page is locked by vacuum operations and awaits the lock if it is. - /// - /// This should be called before any operation that accesses data on a specific page. - /// - /// Returns `false` if not waited at all, `true` if waited. - pub async fn await_page_lock(&self, page_id: PageId) -> bool { - if let Some(lock) = self.get_page_lock(page_id) { - let guard = lock.read().await; - let wait = guard.wait(); - drop(guard); - wait.await; - - true - } else { - false - } - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - use std::time::Duration; - - use tokio::time::timeout; - - use super::*; - - #[tokio::test] - async fn test_default_creates_empty_locks() { - let locks: WorkTableLock<(), String> = WorkTableLock::default(); - - assert!(locks.get_page_lock(1.into()).is_none()); - assert!(locks.get_page_lock(2.into()).is_none()); - } - - #[tokio::test] - async fn test_lock_page_returns_lock() { - let locks: WorkTableLock<(), String> = WorkTableLock::default(); - - let lock = locks.lock_page(5.into()); - - let guard = lock.read().await; - assert!(guard.is_locked()); - } - - #[tokio::test] - async fn test_lock_page_same_id_returns_same_lock() { - let locks: WorkTableLock<(), String> = WorkTableLock::default(); - - let lock1 = locks.lock_page(10.into()); - let lock2 = locks.lock_page(10.into()); - - let ptr1 = Arc::as_ptr(&lock1); - let ptr2 = Arc::as_ptr(&lock2); - assert_eq!(ptr1, ptr2); - } - - #[tokio::test] - async fn test_lock_page_different_ids_returns_different_locks() { - let locks: WorkTableLock<(), String> = WorkTableLock::default(); - - let lock1 = locks.lock_page(1.into()); - let lock2 = locks.lock_page(2.into()); - - let ptr1 = Arc::as_ptr(&lock1); - let ptr2 = Arc::as_ptr(&lock2); - assert_ne!(ptr1, ptr2); - } - - #[tokio::test] - async fn test_get_page_lock_returns_none_when_no_lock() { - let locks: WorkTableLock<(), String> = WorkTableLock::default(); - - assert!(locks.get_page_lock(999.into()).is_none()); - } - - #[tokio::test] - async fn test_get_page_lock_returns_some_when_locked() { - let locks: WorkTableLock<(), String> = WorkTableLock::default(); - - locks.lock_page(42.into()); - let retrieved = locks.get_page_lock(42.into()); - - assert!(retrieved.is_some()); - } - - #[tokio::test] - async fn test_remove_page_lock_removes_unlocked_lock() { - let locks: WorkTableLock<(), String> = WorkTableLock::default(); - - let lock = locks.lock_page(7.into()); - - { - let guard = lock.read().await; - guard.unlock(); - } - - locks.remove_page_lock(&7.into()); - - assert!(locks.get_page_lock(7.into()).is_none()); - } - - #[tokio::test] - async fn test_remove_page_lock_does_not_remove_locked_lock() { - let locks: WorkTableLock<(), String> = WorkTableLock::default(); - - let lock = locks.lock_page(8.into()); - - let _guard = lock.write().await; - - locks.remove_page_lock(&8.into()); - - assert!(locks.get_page_lock(8.into()).is_some()); - } - - #[tokio::test] - async fn test_await_page_lock_returns_immediately_when_no_lock() { - let locks: WorkTableLock<(), String> = WorkTableLock::default(); - - let result = timeout(Duration::from_millis(10), locks.await_page_lock(100.into())).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_await_page_lock_blocks_when_locked() { - let locks = Arc::new(WorkTableLock::<(), String>::default()); - - let lock = locks.lock_page(99.into()); - - let guard = lock.write().await; - - let locks_clone = locks.clone(); - let await_task = tokio::spawn(async move { - locks_clone.await_page_lock(99.into()).await; - }); - - let result = timeout(Duration::from_millis(50), await_task).await; - assert!(result.is_err()); - - guard.unlock(); - drop(guard); - - let result = timeout(Duration::from_millis(100), async move { - locks.await_page_lock(99.into()).await; - }) - .await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_multiple_pages_can_be_locked_independently() { - let locks: WorkTableLock<(), String> = WorkTableLock::default(); - - let page1 = locks.lock_page(1.into()); - let page2 = locks.lock_page(2.into()); - - let lock1 = page1.write().await; - let lock2 = page2.write().await; - - assert!(lock1.is_locked()); - assert!(lock2.is_locked()); - - drop(lock2); - drop(lock1); - } -} diff --git a/src/table/mod.rs b/src/table/mod.rs index 2def132..51abc10 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -54,7 +54,7 @@ pub struct WorkTable< pub pk_gen: PkGen, - pub lock_manager: Arc>, + pub lock_manager: Arc>, pub update_state: IndexMap, diff --git a/src/table/vacuum/fragmentation_info.rs b/src/table/vacuum/fragmentation_info.rs index 4f317bb..02b87fa 100644 --- a/src/table/vacuum/fragmentation_info.rs +++ b/src/table/vacuum/fragmentation_info.rs @@ -60,9 +60,10 @@ impl FragmentationInfo { /// Fragmentation information for a single data [`Page`]. /// /// [`Page`]: crate::in_memory::Data -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub struct PageFragmentationInfo { pub page_id: PageId, + pub links: Vec, pub page_size: usize, pub empty_bytes: u32, pub filled_empty_ratio: f64, @@ -80,16 +81,17 @@ impl EmptyLinkRegistry { /// Calculates [`PageFragmentationInfo`] information for all pages with /// empty [`Link`]s. pub fn get_per_page_info(&self) -> Vec { - let mut page_empty_bytes: HashMap = HashMap::new(); + let mut page_empty_data: HashMap)> = HashMap::new(); for (page_id, link) in self.page_links_map.iter() { - let entry = page_empty_bytes.entry(*page_id).or_insert(0); - *entry += link.length; + let entry = page_empty_data.entry(*page_id).or_default(); + entry.0 += link.length; + entry.1.push(link.clone()); } - let mut per_page_data: Vec = page_empty_bytes + let mut per_page_data: Vec = page_empty_data .into_iter() - .map(|(page_id, empty_bytes)| { + .map(|(page_id, (empty_bytes, links))| { let filled_empty_ratio = if empty_bytes > 0 { let filled_bytes = DATA_LENGTH.saturating_sub(empty_bytes as usize); filled_bytes as f64 / empty_bytes as f64 @@ -99,6 +101,7 @@ impl EmptyLinkRegistry { PageFragmentationInfo { page_size: DATA_LENGTH, + links, page_id, empty_bytes, filled_empty_ratio, diff --git a/src/table/vacuum/vacuum.rs b/src/table/vacuum/vacuum.rs index e77add9..b5ca3eb 100644 --- a/src/table/vacuum/vacuum.rs +++ b/src/table/vacuum/vacuum.rs @@ -18,12 +18,13 @@ use rkyv::{Archive, Deserialize, Serialize}; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; use crate::lock::WorkTableLock; -use crate::prelude::{OffsetEqLink, TablePrimaryKey}; +use crate::prelude::{OffsetEqLink, TablePrimaryKey, VacuumWrapper}; use crate::vacuum::VacuumStats; use crate::vacuum::WorkTableVacuum; use crate::vacuum::fragmentation_info::{FragmentationInfo, PageFragmentationInfo}; use crate::{AvailableIndex, PrimaryIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; use async_trait::async_trait; +use ordered_float::OrderedFloat; use rkyv::api::high::HighDeserializer; #[derive(Debug)] @@ -45,7 +46,6 @@ pub struct EmptyDataVacuum< table_name: &'static str, data_pages: Arc>, - lock_manager: Arc>, primary_index: Arc>, secondary_indexes: Arc, @@ -90,6 +90,7 @@ where Strategy, Share>, rkyv::rancor::Error>, >, <::WrappedRow as Archive>::Archived: GhostWrapper + + VacuumWrapper + Deserialize<::WrappedRow, HighDeserializer>, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc, @@ -98,72 +99,56 @@ where async fn defragment(&self) -> VacuumStats { let now = Instant::now(); - let per_page_info = self.data_pages.empty_links_registry().get_per_page_info(); + let mut per_page_info = self.data_pages.empty_links_registry().get_per_page_info(); + per_page_info.sort_by(|l, r| { + OrderedFloat(l.filled_empty_ratio).cmp(&OrderedFloat(r.filled_empty_ratio)) + }); let initial_bytes_freed: u64 = per_page_info.iter().map(|i| i.empty_bytes as u64).sum(); + let additional_allocated_page = self.data_pages.allocate_new_page(); - let mut in_migration_pages = VecDeque::new(); - let mut free_pages = vec![]; + let mut free_pages = VecDeque::new(); let mut defragmented_pages = VecDeque::new(); + free_pages.push_back(additional_allocated_page.id); let pages_processed = per_page_info.len(); let mut info_iter = per_page_info.into_iter(); while let Some(info) = info_iter.next() { - let page_id = info.page_id; - if let Some(id) = defragmented_pages.pop_front() { - match self.move_data_from(page_id, id).await { + let page_from = info.page_id; + loop { + let page_to = if let Some(id) = defragmented_pages.pop_front() { + id + } else if let Some(id) = free_pages.pop_front() { + id + } else { + unreachable!("I hope so") + }; + match self.move_data_from(page_from, page_to).await { (true, true) => { // from moved fully and on to no more space - free_pages.push(page_id); + free_pages.push_back(page_from); + self.free_page(page_from); + break; } (true, false) => { // from moved fully but to has space - free_pages.push(id); - defragmented_pages.push_back(id); + free_pages.push_back(page_from); + self.free_page(page_from); + defragmented_pages.push_back(page_to); + break; } (false, true) => { // from was not moved but to have NO space - in_migration_pages.push_back(page_id); + continue; } (false, false) => unreachable!( "at least one of two situations should appear to break from while cycle" ), } - } else { - let page_id = info.page_id; - self.defragment_page(info).await; - if let Some(id) = in_migration_pages.pop_front() { - match self.move_data_from(id, page_id).await { - (true, true) => { - // from moved fully and on to no more space - free_pages.push(id); - } - (true, false) => { - // from moved fully but to has space - free_pages.push(id); - defragmented_pages.push_back(page_id); - } - (false, true) => { - // from was not moved but to have NO space - in_migration_pages.push_back(id); - } - (false, false) => unreachable!( - "at least one of two situations should appear to break from while cycle" - ), - } - } else { - defragmented_pages.push_back(page_id); - } } - } - - for in_migration_pages in in_migration_pages { - let page_start = Link { - page_id: in_migration_pages, - offset: 0, - length: 0, - }; - self.shift_data_in_range(page_start, None); + for l in info.links { + self.data_pages.empty_links_registry().remove_link(l) + } } let pages_freed = free_pages.len(); @@ -179,10 +164,15 @@ where } } - async fn move_data_from(&self, from: PageId, to: PageId) -> (bool, bool) { - let from_lock = self.lock_manager.lock_page(from); - let to_lock = self.lock_manager.lock_page(to); + fn free_page(&self, page_id: PageId) { + let p = self + .data_pages + .get_page(page_id) + .expect("should exist as called"); + p.reset() + } + async fn move_data_from(&self, from: PageId, to: PageId) -> (bool, bool) { let to_page = self .data_pages .get_page(to) @@ -234,6 +224,11 @@ where drop(range); for (from_link, pk) in links { + unsafe { + self.data_pages + .with_mut_ref(from_link.0, |r| r.set_in_vacuum_process()) + .expect("link should be valid") + } let raw_data = from_page .get_raw_row(from_link.0) .expect("link is not bigger than free offset"); @@ -243,147 +238,9 @@ where self.update_index_after_move(pk, from_link.0, new_link); } - { - let g = from_lock.read().await; - g.unlock(); - self.lock_manager.remove_page_lock(&from) - } - { - let g = to_lock.read().await; - g.unlock(); - self.lock_manager.remove_page_lock(&to) - } - (from_page_will_be_moved, to_page_will_be_filled) } - async fn defragment_page(&self, info: PageFragmentationInfo) { - let registry = self.data_pages.empty_links_registry(); - let mut page_empty_links = registry - .page_links_map - .get(&info.page_id) - .map(|(_, l)| *l) - .collect::>(); - page_empty_links.sort_by(|l1, l2| l1.offset.cmp(&l2.offset)); - - let lock = self.lock_manager.lock_page(info.page_id); - let mut empty_links_iter = page_empty_links.into_iter(); - - let Some(mut current_empty) = empty_links_iter.next() else { - // TODO: create some kind of guard that will Drop himself - { - let l = lock.read().await; - l.unlock(); - self.lock_manager.remove_page_lock(&info.page_id) - } - return; - }; - registry.remove_link(current_empty); - - let Some(mut next_empty) = empty_links_iter.next() else { - self.shift_data_in_range(current_empty, None); - { - let l = lock.read().await; - l.unlock(); - self.lock_manager.remove_page_lock(&info.page_id) - } - return; - }; - registry.remove_link(next_empty); - - loop { - let offset = self.shift_data_in_range(current_empty, Some(next_empty.offset)); - - let new_next = empty_links_iter.next(); - match new_next { - Some(link) => { - registry.remove_link(link); - current_empty = Link { - page_id: next_empty.page_id, - offset, - length: next_empty.length + (next_empty.offset - offset), - }; - next_empty = link; - } - None => { - let from = Link { - page_id: next_empty.page_id, - offset, - length: next_empty.length + (next_empty.offset - offset), - }; - self.shift_data_in_range(from, None); - break; - } - } - } - - { - let l = lock.read().await; - l.unlock(); - self.lock_manager.remove_page_lock(&info.page_id) - } - } - - fn shift_data_in_range(&self, start_link: Link, end_offset: Option) -> u32 { - let page_id = start_link.page_id; - let page = self - .data_pages - .get_page(page_id) - .expect("should exist as link exists"); - let start_link = OffsetEqLink::<_>(start_link); - let range = if let Some(offset) = end_offset { - let end = OffsetEqLink::<_>(Link { - page_id, - offset, - length: 0, - }); - self.primary_index.reverse_pk_map.range(start_link..end) - } else { - let end = OffsetEqLink::<_>(Link { - page_id: page_id.next(), - offset: 0, - length: 0, - }); - self.primary_index.reverse_pk_map.range(start_link..end) - } - .map(|(l, pk)| (*l, pk.clone())) - .collect::>(); - let mut range_iter = range.into_iter(); - - let mut entry_offset = start_link.0.offset; - while let Some((link, pk)) = range_iter.next() { - let link_value = link.0; - - if let Some(end) = end_offset { - if entry_offset + link_value.length >= end { - return entry_offset; - } - } - - let new_link = Link { - page_id, - offset: entry_offset, - length: link_value.length, - }; - - // TODO: Safety comment - unsafe { - page.move_from_to(link_value, new_link) - .expect("should use valid links") - } - entry_offset += link_value.length; - self.update_index_after_move(pk.clone(), link_value, new_link); - } - - if end_offset.is_none() { - // Is safe as page is locked now and we can get here only if end_offset - // is not set so we are shifting till page end. - page.free_offset.store(entry_offset, Ordering::Release); - } - - entry_offset - } - fn update_index_after_move(&self, pk: PrimaryKey, old_link: Link, new_link: Link) { let old_offset_link = OffsetEqLink(old_link); let new_offset_link = OffsetEqLink(new_link); diff --git a/x.md b/x.md deleted file mode 100644 index a41a4ea..0000000 --- a/x.md +++ /dev/null @@ -1,16 +0,0 @@ -- [x] Add methods for data page to update it's parts. It will have `Link` and byte array as args. -- [ ] Fix `PageId`s mismatch in `Space` and `DataPages`. `DataPages` always adds pages with incremental `PageId`'s first. But - in `Space` it can become different `PageId`. For example `SpaceInfo` is always 0, then primary index, so data is at - least 3rd. After read we will get page with 3 as id, but all `Link`'s in indexes will be wrong. (in progress by ATsibin) -- [ ] Check `PersistTable` macro and see `Space` type that is generated (`Space` describes file structure). - You need to add methods to add pages to the `Space` correctly. You must be careful with `Intervals` that describes data layout. -- [ ] Check `indexset` and see how to map internal nodes to disk representation. Also there must be possibility to set node size to -meet page size correctly. -- [ ] Add `PesristEngine` object to `WorkTable`. It will contain queue of write operations to sync in-memory with file. - - [x] Create operation as struct representation. As I think it can be enum of `Create`, `Update` and `Delete`. - `Create` ops contains primary + secondary keys data, `Link` on data page and data as bytes array. `Update` should just - contain `Link` on data page and data as bytes array. `Delete` should contain primary + secondary keys data to find and - remove them from index pages (index pages are not optimised now, empty links are also not optimised now). - - [ ] Create `PesristEngine` object that will contain queue of ops and will apply them to the file. (in progress by me) - - [ ] Add logic to generated tables code in methods to push operations into `PesristEngine`. - From a3da270b288ae81a3034cb83342c67657c5d0d06 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:37:48 +0300 Subject: [PATCH 23/32] rework locks back --- .../persist_table/generator/space_file/mod.rs | 3 +- .../src/worktable/generator/queries/delete.rs | 4 +- .../worktable/generator/queries/in_place.rs | 2 +- .../src/worktable/generator/queries/locks.rs | 12 ++--- .../src/worktable/generator/queries/update.rs | 49 ++++------------- .../src/worktable/generator/table/impls.rs | 16 ++++-- .../worktable/generator/table/index_fns.rs | 6 +-- src/in_memory/empty_link_registry.rs | 54 ++++++++++++++++++- src/table/mod.rs | 36 +++---------- src/table/vacuum/vacuum.rs | 14 +++-- 10 files changed, 100 insertions(+), 96 deletions(-) diff --git a/codegen/src/persist_table/generator/space_file/mod.rs b/codegen/src/persist_table/generator/space_file/mod.rs index b45c4b2..ed93b06 100644 --- a/codegen/src/persist_table/generator/space_file/mod.rs +++ b/codegen/src/persist_table/generator/space_file/mod.rs @@ -155,6 +155,7 @@ impl Generator { let dir_name = name_generator.get_dir_name(); let const_name = name_generator.get_page_inner_size_const_ident(); let pk_type = name_generator.get_primary_key_type_ident(); + let lock_type = name_generator.get_lock_type_ident(); let table_name = name_generator.get_work_table_literal_name(); let primary_index_init = if self.attributes.pk_unsized { @@ -229,7 +230,7 @@ impl Generator { primary_index: std::sync::Arc::new(primary_index), indexes: std::sync::Arc::new(indexes), pk_gen: PrimaryKeyGeneratorState::from_state(self.data_info.inner.pk_gen_state), - lock_manager: std::sync::Arc::new(worktable::lock::WorkTableLock::default()), + lock_manager: std::sync::Arc::new(LockMap::<#lock_type, #pk_type>::default()), update_state: IndexMap::default(), table_name: #table_name, pk_phantom: std::marker::PhantomData, diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index fb566e4..fce59cc 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -48,7 +48,7 @@ impl Generator { #delete_logic lock.unlock(); // Releases locks - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks + self.0.lock_manager.remove_with_lock_check(&pk); // Removes locks core::result::Result::Ok(()) } @@ -108,7 +108,7 @@ impl Generator { Ok(l) => l, Err(e) => { lock.unlock(); // Releases locks - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks + self.0.lock_manager.remove_with_lock_check(&pk); // Removes locks return Err(e); } }; diff --git a/codegen/src/worktable/generator/queries/in_place.rs b/codegen/src/worktable/generator/queries/in_place.rs index 8ce9a2c..efe4ca3 100644 --- a/codegen/src/worktable/generator/queries/in_place.rs +++ b/codegen/src/worktable/generator/queries/in_place.rs @@ -126,7 +126,7 @@ impl Generator { }; lock.unlock(); - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); + self.0.lock_manager.remove_with_lock_check(&pk); Ok(()) } diff --git a/codegen/src/worktable/generator/queries/locks.rs b/codegen/src/worktable/generator/queries/locks.rs index 33bfe16..b3974b2 100644 --- a/codegen/src/worktable/generator/queries/locks.rs +++ b/codegen/src/worktable/generator/queries/locks.rs @@ -126,8 +126,8 @@ impl Generator { let lock_ident = name_generator.get_lock_type_ident(); quote! { - let lock_id = self.0.lock_manager.row_locks.next_id(); - if let Some(lock) = self.0.lock_manager.row_locks.get(&pk) { + let lock_id = self.0.lock_manager.next_id(); + if let Some(lock) = self.0.lock_manager.get(&pk) { let mut lock_guard = lock.write().await; #[allow(clippy::mutable_key_type)] let (locks, op_lock) = lock_guard.lock(lock_id); @@ -140,7 +140,7 @@ impl Generator { let (lock, op_lock) = #lock_ident::with_lock(lock_id); let mut lock = std::sync::Arc::new(tokio::sync::RwLock::new(lock)); let mut guard = lock.write().await; - if let Some(old_lock) = self.0.lock_manager.row_locks.insert(pk.clone(), lock.clone()) { + if let Some(old_lock) = self.0.lock_manager.insert(pk.clone(), lock.clone()) { let mut old_lock_guard = old_lock.write().await; #[allow(clippy::mutable_key_type)] let locks = guard.merge(&mut *old_lock_guard); @@ -160,8 +160,8 @@ impl Generator { let lock_ident = name_generator.get_lock_type_ident(); quote! { - let lock_id = self.0.lock_manager.row_locks.next_id(); - if let Some(lock) = self.0.lock_manager.row_locks.get(&pk) { + let lock_id = self.0.lock_manager.next_id(); + if let Some(lock) = self.0.lock_manager.get(&pk) { let mut lock_guard = lock.write().await; #[allow(clippy::mutable_key_type)] let (locks, op_lock) = lock_guard.#ident(lock_id); @@ -174,7 +174,7 @@ impl Generator { let (_, op_lock) = lock.#ident(lock_id); let lock = std::sync::Arc::new(tokio::sync::RwLock::new(lock)); let mut guard = lock.write().await; - if let Some(old_lock) = self.0.lock_manager.row_locks.insert(pk.clone(), lock.clone()) { + if let Some(old_lock) = self.0.lock_manager.insert(pk.clone(), lock.clone()) { let mut old_lock_guard = old_lock.write().await; #[allow(clippy::mutable_key_type)] let locks = guard.merge(&mut *old_lock_guard); diff --git a/codegen/src/worktable/generator/queries/update.rs b/codegen/src/worktable/generator/queries/update.rs index c1f1516..b45ce2e 100644 --- a/codegen/src/worktable/generator/queries/update.rs +++ b/codegen/src/worktable/generator/queries/update.rs @@ -78,7 +78,7 @@ impl Generator { self.0.update_state.remove(&pk); lock.unlock(); - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks + self.0.lock_manager.remove_with_lock_check(&pk); // Removes locks return core::result::Result::Ok(()); } @@ -102,22 +102,12 @@ impl Generator { Ok(l) => l, Err(e) => { lock.unlock(); - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); + self.0.lock_manager.remove_with_lock_check(&pk); return Err(e); } }; - if self.0.lock_manager.await_page_lock(link.page_id).await { - // We waited for vacuum to complete, need to re-lookup the link - link = self.0 - .primary_index - .pk_map - .get(&pk) - .map(|v| v.get().value.into()) - .expect("should be available as was found before vacuum"); - } - let row_old = self.0.data.select_non_ghosted(link)?; self.0.update_state.insert(pk.clone(), row_old); @@ -139,7 +129,7 @@ impl Generator { self.0.update_state.remove(&pk); lock.unlock(); // Releases locks - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks + self.0.lock_manager.remove_with_lock_check(&pk); // Removes locks #persist_call @@ -293,7 +283,7 @@ impl Generator { } lock.unlock(); // Releases locks - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks + self.0.lock_manager.remove_with_lock_check(&pk); // Removes locks return core::result::Result::Ok(()); } @@ -498,22 +488,12 @@ impl Generator { Ok(l) => l, Err(e) => { lock.unlock(); - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); + self.0.lock_manager.remove_with_lock_check(&pk); return Err(e); } }; - if self.0.lock_manager.await_page_lock(link.page_id).await { - // We waited for vacuum to complete, need to re-lookup the link - link = self.0 - .primary_index - .pk_map - .get(&pk) - .map(|v| v.get().value.into()) - .expect("should be available as was found before vacuum"); - } - let mut bytes = rkyv::to_bytes::(&row).map_err(|_| WorkTableError::SerializeError)?; let mut archived_row = unsafe { rkyv::access_unchecked_mut::<<#query_ident as rkyv::Archive>::Archived>(&mut bytes[..]).unseal_unchecked() }; @@ -529,7 +509,7 @@ impl Generator { #diff_process_remove lock.unlock(); - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); + self.0.lock_manager.remove_with_lock_check(&pk); #persist_call @@ -605,7 +585,7 @@ impl Generator { } lock.unlock(); // Releases locks - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); // Removes locks + self.0.lock_manager.remove_with_lock_check(&pk); // Removes locks continue; } else { @@ -672,7 +652,7 @@ impl Generator { } for (pk, lock) in pk_to_unlock { lock.unlock(); - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); + self.0.lock_manager.remove_with_lock_check(&pk); } core::result::Result::Ok(()) } @@ -737,15 +717,6 @@ impl Generator { .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; - if self.0.lock_manager.await_page_lock(link.page_id).await { - // We waited for vacuum to complete, need to re-lookup the link - link = self.0.indexes - .#index - .get(#by) - .map(|v| v.get().value.into()) - .expect("should be available as was found before vacuum"); - } - let pk = self.0.data.select_non_ghosted(link)?.get_primary_key().clone(); let lock = { @@ -759,7 +730,7 @@ impl Generator { Ok(l) => l, Err(e) => { lock.unlock(); - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); + self.0.lock_manager.remove_with_lock_check(&pk); return Err(e); } @@ -779,7 +750,7 @@ impl Generator { #diff_process_remove lock.unlock(); - self.0.lock_manager.row_locks.remove_with_lock_check(&pk); + self.0.lock_manager.remove_with_lock_check(&pk); #persist_call diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index 5ce1e59..6b3c1ca 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -298,6 +298,7 @@ impl Generator { let name_generator = WorktableNameGenerator::from_table_name(self.name.to_string()); let table_name = name_generator.get_work_table_literal_name(); let secondary_index_events = name_generator.get_space_secondary_index_events_ident(); + let lock_type = name_generator.get_lock_type_ident(); if self.is_persist { quote! { @@ -309,13 +310,12 @@ impl Generator { _, _, _, - _, + #lock_type, _, #secondary_index_events >::new( #table_name, std::sync::Arc::clone(&self.0.data), - std::sync::Arc::clone(&self.0.lock_manager), std::sync::Arc::clone(&self.0.primary_index), std::sync::Arc::clone(&self.0.indexes), )) @@ -324,10 +324,18 @@ impl Generator { } else { quote! { pub fn vacuum(&self) -> std::sync::Arc { - std::sync::Arc::new(EmptyDataVacuum::new( + std::sync::Arc::new(EmptyDataVacuum::< + _, + _, + _, + _, + _, + _, + #lock_type, + _ + >::new( #table_name, std::sync::Arc::clone(&self.0.data), - std::sync::Arc::clone(&self.0.lock_manager), std::sync::Arc::clone(&self.0.primary_index), std::sync::Arc::clone(&self.0.indexes), )) diff --git a/codegen/src/worktable/generator/table/index_fns.rs b/codegen/src/worktable/generator/table/index_fns.rs index c9c9a50..f2c1dab 100644 --- a/codegen/src/worktable/generator/table/index_fns.rs +++ b/codegen/src/worktable/generator/table/index_fns.rs @@ -65,11 +65,7 @@ impl Generator { Ok(quote! { pub async fn #fn_name(&self, by: #type_) -> Option<#row_ident> { - let mut link: Link = self.0.indexes.#field_ident.get(#by).map(|kv| kv.get().value.into())?; - if self.0.lock_manager.await_page_lock(link.page_id).await { - // We waited for vacuum to complete, need to re-lookup the link - link = self.0.indexes.#field_ident.get(#by).map(|kv| kv.get().value.into())?; - } + let link: Link = self.0.indexes.#field_ident.get(#by).map(|kv| kv.get().value.into())?; self.0.data.select_non_ghosted(link).ok() } }) diff --git a/src/in_memory/empty_link_registry.rs b/src/in_memory/empty_link_registry.rs index a4a7e26..9e139ac 100644 --- a/src/in_memory/empty_link_registry.rs +++ b/src/in_memory/empty_link_registry.rs @@ -1,11 +1,13 @@ -use crate::in_memory::DATA_INNER_LENGTH; +use std::sync::atomic::{AtomicU32, Ordering}; + use data_bucket::Link; use data_bucket::page::PageId; use derive_more::Into; use indexset::concurrent::multimap::BTreeMultiMap; use indexset::concurrent::set::BTreeSet; use parking_lot::FairMutex; -use std::sync::atomic::{AtomicU32, Ordering}; + +use crate::in_memory::DATA_INNER_LENGTH; /// A link wrapper that implements `Ord` based on absolute index calculation. #[derive(Copy, Clone, Debug, Eq, PartialEq, Into)] @@ -82,6 +84,7 @@ pub struct EmptyLinkRegistry { sum_links_len: AtomicU32, op_lock: FairMutex<()>, + vacuum_lock: tokio::sync::Mutex<()>, } impl Default for EmptyLinkRegistry { @@ -92,6 +95,7 @@ impl Default for EmptyLinkRegistry { page_links_map: BTreeMultiMap::new(), sum_links_len: Default::default(), op_lock: Default::default(), + vacuum_lock: Default::default(), } } } @@ -157,6 +161,10 @@ impl EmptyLinkRegistry { } pub fn pop_max(&self) -> Option { + if self.vacuum_lock.try_lock().is_err() { + return None; + } + let _g = self.op_lock.lock(); let mut iter = self.length_ord_links.iter().rev(); @@ -175,6 +183,10 @@ impl EmptyLinkRegistry { pub fn get_empty_links_size_bytes(&self) -> u32 { self.sum_links_len.load(Ordering::Acquire) } + + pub async fn lock_vacuum(&self) -> tokio::sync::MutexGuard<'_, ()> { + self.vacuum_lock.lock().await + } } #[cfg(test)] @@ -435,4 +447,42 @@ mod tests { registry.pop_max(); assert_eq!(registry.sum_links_len.load(Ordering::Acquire), 0); } + + #[tokio::test] + async fn test_lock_vacuum_prevents_pop() { + let registry = EmptyLinkRegistry::::default(); + + let link = Link { + page_id: 1.into(), + offset: 0, + length: 100, + }; + + registry.push(link); + + let popped = registry.pop_max(); + assert!(popped.is_some()); + assert_eq!(popped.unwrap().length, 100); + + registry.push(Link { + page_id: 1.into(), + offset: 0, + length: 100, + }); + + let _lock = registry.lock_vacuum().await; + let popped_locked = registry.pop_max(); + assert!( + popped_locked.is_none(), + "pop_max should return None when vacuum lock is held" + ); + + drop(_lock); + let popped_after_unlock = registry.pop_max(); + assert!( + popped_after_unlock.is_some(), + "pop_max should return link after vacuum lock is released" + ); + assert_eq!(popped_after_unlock.unwrap().length, 100); + } } diff --git a/src/table/mod.rs b/src/table/mod.rs index 51abc10..afda0f8 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -3,9 +3,8 @@ pub mod system_info; pub mod vacuum; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; -use crate::lock::WorkTableLock; use crate::persistence::{InsertOperation, Operation}; -use crate::prelude::{Link, OperationId, PrimaryKeyGeneratorState}; +use crate::prelude::{Link, LockMap, OperationId, PrimaryKeyGeneratorState, VacuumWrapper}; use crate::primary_key::{PrimaryKeyGenerator, TablePrimaryKey}; use crate::util::OffsetEqLink; use crate::{ @@ -54,7 +53,7 @@ pub struct WorkTable< pub pk_gen: PkGen, - pub lock_manager: Arc>, + pub lock_manager: Arc>, pub update_state: IndexMap, @@ -159,21 +158,11 @@ where <::WrappedRow as Archive>::Archived: Deserialize<::WrappedRow, HighDeserializer>, { - let mut link: Option = self + let link: Option = self .primary_index .pk_map .get(&pk) .map(|v| v.get().value.into()); - if let Some(l) = link { - if self.lock_manager.await_page_lock(l.page_id).await { - // We waited for vacuum to complete, need to re-lookup the link - link = self - .primary_index - .pk_map - .get(&pk) - .map(|v| v.get().value.into()); - } - } if let Some(link) = link { self.data.select(link).ok() } else { @@ -196,7 +185,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, - <::WrappedRow as Archive>::Archived: GhostWrapper, + <::WrappedRow as Archive>::Archived: GhostWrapper + VacuumWrapper, PrimaryKey: Clone, AvailableTypes: 'static, AvailableIndexes: AvailableIndex, @@ -262,7 +251,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, - <::WrappedRow as Archive>::Archived: GhostWrapper, + <::WrappedRow as Archive>::Archived: GhostWrapper + VacuumWrapper, PrimaryKey: Clone, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc, @@ -340,7 +329,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, - <::WrappedRow as Archive>::Archived: GhostWrapper, + <::WrappedRow as Archive>::Archived: GhostWrapper + VacuumWrapper, PrimaryKey: Clone, AvailableTypes: 'static, AvailableIndexes: Debug + AvailableIndex, @@ -351,21 +340,12 @@ where if pk != row_old.get_primary_key() { return Err(WorkTableError::PrimaryUpdateTry); } - let mut old_link: Link = self + let old_link: Link = self .primary_index .pk_map .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; - if self.lock_manager.await_page_lock(old_link.page_id).await { - // We waited for vacuum to complete, need to re-lookup the link - old_link = self - .primary_index - .pk_map - .get(&pk) - .map(|v| v.get().value.into()) - .ok_or(WorkTableError::NotFound)?; - } let new_link = self .data .insert(row_new.clone()) @@ -426,7 +406,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, - <::WrappedRow as Archive>::Archived: GhostWrapper, + <::WrappedRow as Archive>::Archived: GhostWrapper + VacuumWrapper, PrimaryKey: Clone, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc, diff --git a/src/table/vacuum/vacuum.rs b/src/table/vacuum/vacuum.rs index b5ca3eb..c9d501a 100644 --- a/src/table/vacuum/vacuum.rs +++ b/src/table/vacuum/vacuum.rs @@ -2,7 +2,6 @@ use std::collections::VecDeque; use std::fmt::Debug; use std::marker::PhantomData; use std::sync::Arc; -use std::sync::atomic::Ordering; use std::time::Instant; use data_bucket::Link; @@ -17,11 +16,10 @@ use rkyv::util::AlignedVec; use rkyv::{Archive, Deserialize, Serialize}; use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; -use crate::lock::WorkTableLock; use crate::prelude::{OffsetEqLink, TablePrimaryKey, VacuumWrapper}; use crate::vacuum::VacuumStats; use crate::vacuum::WorkTableVacuum; -use crate::vacuum::fragmentation_info::{FragmentationInfo, PageFragmentationInfo}; +use crate::vacuum::fragmentation_info::FragmentationInfo; use crate::{AvailableIndex, PrimaryIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; use async_trait::async_trait; use ordered_float::OrderedFloat; @@ -50,7 +48,7 @@ pub struct EmptyDataVacuum< primary_index: Arc>, secondary_indexes: Arc, - phantom_data: PhantomData<(SecondaryEvents, AvailableTypes, AvailableIndexes)>, + phantom_data: PhantomData<(SecondaryEvents, AvailableTypes, AvailableIndexes, LockType)>, } impl< @@ -99,7 +97,9 @@ where async fn defragment(&self) -> VacuumStats { let now = Instant::now(); - let mut per_page_info = self.data_pages.empty_links_registry().get_per_page_info(); + let registry = self.data_pages.empty_links_registry(); + let mut per_page_info = registry.get_per_page_info(); + let _registry_lock = registry.lock_vacuum().await; per_page_info.sort_by(|l, r| { OrderedFloat(l.filled_empty_ratio).cmp(&OrderedFloat(r.filled_empty_ratio)) }); @@ -266,14 +266,12 @@ where pub fn new( table_name: &'static str, data_pages: Arc>, - lock_manager: Arc>, primary_index: Arc>, secondary_indexes: Arc, ) -> Self { Self { table_name, data_pages, - lock_manager, primary_index, secondary_indexes, phantom_data: PhantomData, @@ -320,6 +318,7 @@ where > + Send + Sync, <::WrappedRow as Archive>::Archived: GhostWrapper + + VacuumWrapper + Deserialize<::WrappedRow, HighDeserializer>, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc @@ -388,7 +387,6 @@ mod tests { EmptyDataVacuum::new( table.name(), Arc::clone(&table.0.data), - Arc::clone(&table.0.lock_manager), Arc::clone(&table.0.primary_index), Arc::clone(&table.0.indexes), ) From 72b537b915a0ed689d66c710eb3e04d90f257694 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:51:41 +0300 Subject: [PATCH 24/32] WIP --- .../src/worktable/generator/queries/delete.rs | 4 +- .../src/worktable/generator/queries/locks.rs | 2 +- .../src/worktable/generator/queries/update.rs | 29 +++- .../src/worktable/generator/table/impls.rs | 12 +- codegen/src/worktable/generator/wrapper.rs | 30 ++-- src/in_memory/data.rs | 13 +- src/in_memory/empty_link_registry.rs | 12 ++ src/in_memory/mod.rs | 2 +- src/in_memory/pages.rs | 159 +++++++++++++++++- src/in_memory/row.rs | 8 +- src/lib.rs | 2 +- src/table/mod.rs | 14 +- src/table/vacuum/vacuum.rs | 122 +++++++++----- tests/worktable/vacuum.rs | 22 ++- 14 files changed, 323 insertions(+), 108 deletions(-) diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index fce59cc..507127f 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -75,9 +75,9 @@ impl Generator { let process = if self.is_persist { quote! { + self.0.data.delete(link).map_err(WorkTableError::PagesError)?; let secondary_keys_events = self.0.indexes.delete_row_cdc(row, link)?; let (_, primary_key_events) = self.0.primary_index.remove_cdc(pk.clone(), link); - self.0.data.delete(link).map_err(WorkTableError::PagesError)?; let mut op: Operation< <<#pk_ident as TablePrimaryKey>::Generator as PrimaryKeyGeneratorState>::State, #pk_ident, @@ -92,9 +92,9 @@ impl Generator { } } else { quote! { + self.0.data.delete(link).map_err(WorkTableError::PagesError)?; self.0.indexes.delete_row(row, link)?; self.0.primary_index.remove(&pk, link); - self.0.data.delete(link).map_err(WorkTableError::PagesError)?; } }; if is_locked { diff --git a/codegen/src/worktable/generator/queries/locks.rs b/codegen/src/worktable/generator/queries/locks.rs index b3974b2..7e80f54 100644 --- a/codegen/src/worktable/generator/queries/locks.rs +++ b/codegen/src/worktable/generator/queries/locks.rs @@ -138,7 +138,7 @@ impl Generator { } else { #[allow(clippy::mutable_key_type)] let (lock, op_lock) = #lock_ident::with_lock(lock_id); - let mut lock = std::sync::Arc::new(tokio::sync::RwLock::new(lock)); + let lock = std::sync::Arc::new(tokio::sync::RwLock::new(lock)); let mut guard = lock.write().await; if let Some(old_lock) = self.0.lock_manager.insert(pk.clone(), lock.clone()) { let mut old_lock_guard = old_lock.write().await; diff --git a/codegen/src/worktable/generator/queries/update.rs b/codegen/src/worktable/generator/queries/update.rs index b45ce2e..b100eae 100644 --- a/codegen/src/worktable/generator/queries/update.rs +++ b/codegen/src/worktable/generator/queries/update.rs @@ -723,16 +723,27 @@ impl Generator { #custom_lock }; - let link = match self.0.indexes.#index - .get(#by) - .map(|v| v.get().value.into()) - .ok_or(WorkTableError::NotFound) { - Ok(l) => l, - Err(e) => { - lock.unlock(); - self.0.lock_manager.remove_with_lock_check(&pk); + let link = loop { + let link = match self.0.indexes.#index + .get(#by) + .map(|v| v.get().value.into()) + .ok_or(WorkTableError::NotFound) { + Ok(l) => l, + Err(e) => { + lock.unlock(); + self.0.lock_manager.remove_with_lock_check(&pk); - return Err(e); + return Err(e); + } + }; + + if let Err(e) = self.0.data.select_non_vacuumed(link) { + if e.is_vacuumed() { + continue; + } + return Err(e.into()); + } else { + break link; } }; diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index 6b3c1ca..44a92b7 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -314,11 +314,12 @@ impl Generator { _, #secondary_index_events >::new( - #table_name, - std::sync::Arc::clone(&self.0.data), - std::sync::Arc::clone(&self.0.primary_index), - std::sync::Arc::clone(&self.0.indexes), - )) + #table_name, + std::sync::Arc::clone(&self.0.data), + std::sync::Arc::clone(&self.0.lock_manager), + std::sync::Arc::clone(&self.0.primary_index), + std::sync::Arc::clone(&self.0.indexes), + )) } } } else { @@ -336,6 +337,7 @@ impl Generator { >::new( #table_name, std::sync::Arc::clone(&self.0.data), + std::sync::Arc::clone(&self.0.lock_manager), std::sync::Arc::clone(&self.0.primary_index), std::sync::Arc::clone(&self.0.indexes), )) diff --git a/codegen/src/worktable/generator/wrapper.rs b/codegen/src/worktable/generator/wrapper.rs index 2a54b72..7c82e9a 100644 --- a/codegen/src/worktable/generator/wrapper.rs +++ b/codegen/src/worktable/generator/wrapper.rs @@ -8,15 +8,13 @@ impl Generator { let type_ = self.gen_wrapper_type(); let impl_ = self.gen_wrapper_impl(); let storable_impl = self.get_wrapper_storable_impl(); - let ghost_wrapper_impl = self.get_wrapper_ghost_impl(); - let vacuum_wrapper_impl = self.get_wrapper_vacuum_impl(); + let archived_wrapper_impl = self.get_archived_wrapper_impl(); quote! { #type_ #impl_ #storable_impl - #ghost_wrapper_impl - #vacuum_wrapper_impl + #archived_wrapper_impl } } @@ -57,6 +55,10 @@ impl Generator { self.is_in_vacuum_process } + fn is_deleted(&self) -> bool { + self.is_deleted + } + fn from_inner(inner: #row_ident) -> Self { Self { inner, @@ -81,28 +83,24 @@ impl Generator { } } - fn get_wrapper_ghost_impl(&self) -> TokenStream { + fn get_archived_wrapper_impl(&self) -> TokenStream { let name_generator = WorktableNameGenerator::from_table_name(self.name.to_string()); let row_ident = name_generator.get_archived_wrapper_type_ident(); quote! { - impl GhostWrapper for #row_ident { + impl ArchivedRowWrapper for #row_ident { fn unghost(&mut self) { self.is_ghosted = false; } - } - } - } - - fn get_wrapper_vacuum_impl(&self) -> TokenStream { - let name_generator = WorktableNameGenerator::from_table_name(self.name.to_string()); - let row_ident = name_generator.get_archived_wrapper_type_ident(); - - quote! { - impl VacuumWrapper for #row_ident { fn set_in_vacuum_process(&mut self) { self.is_in_vacuum_process = true; } + fn delete(&mut self) { + self.is_deleted = true; + } + fn is_deleted(&self) -> bool { + self.is_deleted + } } } } diff --git a/src/in_memory/data.rs b/src/in_memory/data.rs index 8b131b8..33fea76 100644 --- a/src/in_memory/data.rs +++ b/src/in_memory/data.rs @@ -1,4 +1,5 @@ use std::cell::UnsafeCell; +use std::fmt::Debug; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicU32, Ordering}; @@ -207,10 +208,6 @@ impl Data { Row: Archive, ::Archived: Portable, { - if link.offset > self.free_offset.load(Ordering::Acquire) { - return Err(ExecutionError::DeserializeError); - } - let inner_data = unsafe { &mut *self.inner_data.get() }; let bytes = &mut inner_data[link.offset as usize..(link.offset + link.length) as usize]; Ok(unsafe { rkyv::access_unchecked_mut::<::Archived>(&mut bytes[..]) }) @@ -224,10 +221,6 @@ impl Data { where Row: Archive, { - if link.offset > self.free_offset.load(Ordering::Acquire) { - return Err(ExecutionError::DeserializeError); - } - let inner_data = unsafe { &*self.inner_data.get() }; let bytes = &inner_data[link.offset as usize..(link.offset + link.length) as usize]; Ok(unsafe { rkyv::access_unchecked::<::Archived>(bytes) }) @@ -244,10 +237,6 @@ impl Data { } pub fn get_raw_row(&self, link: Link) -> Result, ExecutionError> { - if link.offset > self.free_offset.load(Ordering::Acquire) { - return Err(ExecutionError::DeserializeError); - } - let inner_data = unsafe { &mut *self.inner_data.get() }; let bytes = &mut inner_data[link.offset as usize..(link.offset + link.length) as usize]; Ok(bytes.to_vec()) diff --git a/src/in_memory/empty_link_registry.rs b/src/in_memory/empty_link_registry.rs index 9e139ac..5aa3638 100644 --- a/src/in_memory/empty_link_registry.rs +++ b/src/in_memory/empty_link_registry.rs @@ -119,6 +119,18 @@ impl EmptyLinkRegistry { self.sum_links_len.fetch_add(link.length, Ordering::AcqRel); } + pub fn remove_link_for_page(&self, page_id: PageId) { + let _g = self.op_lock.lock(); + let links = self + .page_links_map + .get(&page_id) + .map(|(_, l)| *l) + .collect::>(); + for l in links { + self.remove_link(l); + } + } + pub fn push(&self, link: Link) { let mut index_ord_link = IndexOrdLink(link); let _g = self.op_lock.lock(); diff --git a/src/in_memory/mod.rs b/src/in_memory/mod.rs index 04ad8a8..2b02cf0 100644 --- a/src/in_memory/mod.rs +++ b/src/in_memory/mod.rs @@ -6,4 +6,4 @@ mod row; pub use data::{DATA_INNER_LENGTH, Data, ExecutionError as DataExecutionError}; pub use empty_link_registry::EmptyLinkRegistry; pub use pages::{DataPages, ExecutionError as PagesExecutionError}; -pub use row::{GhostWrapper, Query, RowWrapper, StorableRow, VacuumWrapper}; +pub use row::{ArchivedRowWrapper, Query, RowWrapper, StorableRow}; diff --git a/src/in_memory/pages.rs b/src/in_memory/pages.rs index 1085ad6..9109c00 100644 --- a/src/in_memory/pages.rs +++ b/src/in_memory/pages.rs @@ -18,6 +18,7 @@ use std::{ }; use crate::in_memory::empty_link_registry::EmptyLinkRegistry; +use crate::prelude::ArchivedRowWrapper; use crate::{ in_memory::{ DATA_INNER_LENGTH, Data, DataExecutionError, @@ -250,6 +251,29 @@ where Ok(gen_row.get_inner()) } + pub fn select_non_vacuumed(&self, link: Link) -> Result + where + Row: Archive + + for<'a> Serialize< + Strategy, Share>, rkyv::rancor::Error>, + >, + <::WrappedRow as Archive>::Archived: Portable + + Deserialize<::WrappedRow, HighDeserializer>, + { + let pages = self.pages.read(); + let page = pages + .get(page_id_mapper(link.page_id.into())) + .ok_or(ExecutionError::PageNotFound(link.page_id))?; + let gen_row = page.get_row(link).map_err(ExecutionError::DataPageError)?; + if gen_row.is_ghosted() { + return Err(ExecutionError::Ghosted); + } + if gen_row.is_vacuumed() { + return Err(ExecutionError::Vacuumed); + } + Ok(gen_row.get_inner()) + } + #[cfg_attr( feature = "perf_measurements", performance_measurement(prefix_name = "DataPages") @@ -333,7 +357,20 @@ where } } - pub fn delete(&self, link: Link) -> Result<(), ExecutionError> { + pub fn delete(&self, link: Link) -> Result<(), ExecutionError> + where + Row: Archive + + for<'a> Serialize< + Strategy, Share>, rkyv::rancor::Error>, + >, + ::WrappedRow: Archive + + for<'a> Serialize< + Strategy, Share>, rkyv::rancor::Error>, + >, + <::WrappedRow as Archive>::Archived: ArchivedRowWrapper, + { + unsafe { self.with_mut_ref(link, |r| r.delete())? } + self.empty_links.push(link); Ok(()) } @@ -408,6 +445,16 @@ pub enum ExecutionError { Locked, Ghosted, + + Vacuumed, + + Deleted, +} + +impl ExecutionError { + pub fn is_vacuumed(&self) -> bool { + matches!(self, Self::Vacuumed) + } } #[cfg(test)] @@ -422,8 +469,9 @@ mod tests { use rkyv::with::{AtomicLoad, Relaxed}; use rkyv::{Archive, Deserialize, Serialize}; - use crate::in_memory::pages::DataPages; + use crate::in_memory::pages::{DataPages, ExecutionError}; use crate::in_memory::{DATA_INNER_LENGTH, PagesExecutionError, RowWrapper, StorableRow}; + use crate::prelude::ArchivedRowWrapper; #[derive( Archive, Copy, Clone, Deserialize, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, @@ -466,6 +514,10 @@ mod tests { self.is_vacuumed.load(Ordering::Relaxed) } + fn is_deleted(&self) -> bool { + self.deleted.load(Ordering::Relaxed) + } + /// Creates new [`GeneralRow`] from `Inner`. fn from_inner(inner: Inner) -> Self { Self { @@ -481,6 +533,24 @@ mod tests { type WrappedRow = GeneralRow; } + impl ArchivedRowWrapper for ArchivedGeneralRow + where + T: Archive, + { + fn unghost(&mut self) { + self.is_ghosted = false + } + fn set_in_vacuum_process(&mut self) { + self.is_vacuumed = true + } + fn delete(&mut self) { + self.deleted = true + } + fn is_deleted(&self) -> bool { + self.deleted + } + } + #[test] fn insert() { let pages = DataPages::::new(); @@ -530,6 +600,91 @@ mod tests { assert_eq!(res.err(), Some(PagesExecutionError::Ghosted)) } + #[test] + fn select_non_vacuumed_returns_row_when_valid() { + let pages = DataPages::::new(); + + let row = TestRow { a: 10, b: 20 }; + let link = pages.insert(row).unwrap(); + + unsafe { + pages + .with_mut_ref(link, |archived| { + archived.unghost(); + }) + .unwrap(); + } + + let res = pages.select_non_vacuumed(link); + assert!( + res.is_ok(), + "select_non_vacuumed should return Ok for unghosted, non-vacuumed row" + ); + assert_eq!(res.unwrap(), TestRow { a: 10, b: 20 }); + } + + #[test] + fn select_non_vacuumed_returns_ghosted_error_for_ghosted_row() { + let pages = DataPages::::new(); + + let row = TestRow { a: 10, b: 20 }; + let link = pages.insert(row).unwrap(); + + let res = pages.select_non_vacuumed(link); + assert!(res.is_err()); + assert_eq!(res.err(), Some(ExecutionError::Ghosted)); + } + + #[test] + fn select_non_vacuumed_returns_vacuumed_error_for_vacuumed_row() { + let pages = DataPages::::new(); + + let row = TestRow { a: 10, b: 20 }; + let link = pages.insert(row).unwrap(); + + unsafe { + pages + .with_mut_ref(link, |archived| { + archived.unghost(); + }) + .unwrap(); + } + + unsafe { + pages + .with_mut_ref(link, |archived| archived.set_in_vacuum_process()) + .unwrap(); + } + + let res = pages.select_non_vacuumed(link); + assert!(res.is_err()); + assert_eq!(res.err(), Some(ExecutionError::Vacuumed)); + } + + #[test] + fn select_non_vacuumed_errors_on_vacuumed_even_if_unghosted() { + let pages = DataPages::::new(); + + let row = TestRow { a: 42, b: 99 }; + let link = pages.insert(row).unwrap(); + + unsafe { + pages + .with_mut_ref(link, |archived| { + archived.set_in_vacuum_process(); + }) + .unwrap(); + } + + let res = pages.select_non_vacuumed(link); + assert!(res.is_err()); + assert_eq!( + res.err(), + Some(ExecutionError::Ghosted), + "Should check ghosted before vacuumed" + ); + } + #[test] fn update() { let pages = DataPages::::new(); diff --git a/src/in_memory/row.rs b/src/in_memory/row.rs index c332415..2d75a8b 100644 --- a/src/in_memory/row.rs +++ b/src/in_memory/row.rs @@ -13,15 +13,15 @@ pub trait RowWrapper { fn get_inner(self) -> Inner; fn is_ghosted(&self) -> bool; fn is_vacuumed(&self) -> bool; + fn is_deleted(&self) -> bool; fn from_inner(inner: Inner) -> Self; } -pub trait GhostWrapper { +pub trait ArchivedRowWrapper { fn unghost(&mut self); -} - -pub trait VacuumWrapper { fn set_in_vacuum_process(&mut self); + fn delete(&mut self); + fn is_deleted(&self) -> bool; } pub trait Query { diff --git a/src/lib.rs b/src/lib.rs index 7c9c0f6..9e2648b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ pub use worktable_codegen::worktable; pub mod prelude { pub use crate::in_memory::{ - Data, DataPages, GhostWrapper, Query, RowWrapper, StorableRow, VacuumWrapper, + ArchivedRowWrapper, Data, DataPages, Query, RowWrapper, StorableRow, }; pub use crate::lock::LockMap; pub use crate::lock::{Lock, RowLock}; diff --git a/src/table/mod.rs b/src/table/mod.rs index afda0f8..ccab4c1 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -2,9 +2,9 @@ pub mod select; pub mod system_info; pub mod vacuum; -use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; +use crate::in_memory::{ArchivedRowWrapper, DataPages, RowWrapper, StorableRow}; use crate::persistence::{InsertOperation, Operation}; -use crate::prelude::{Link, LockMap, OperationId, PrimaryKeyGeneratorState, VacuumWrapper}; +use crate::prelude::{Link, LockMap, OperationId, PrimaryKeyGeneratorState}; use crate::primary_key::{PrimaryKeyGenerator, TablePrimaryKey}; use crate::util::OffsetEqLink; use crate::{ @@ -164,7 +164,7 @@ where .get(&pk) .map(|v| v.get().value.into()); if let Some(link) = link { - self.data.select(link).ok() + self.data.select_non_ghosted(link).ok() } else { None } @@ -185,7 +185,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, - <::WrappedRow as Archive>::Archived: GhostWrapper + VacuumWrapper, + <::WrappedRow as Archive>::Archived: ArchivedRowWrapper, PrimaryKey: Clone, AvailableTypes: 'static, AvailableIndexes: AvailableIndex, @@ -251,7 +251,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, - <::WrappedRow as Archive>::Archived: GhostWrapper + VacuumWrapper, + <::WrappedRow as Archive>::Archived: ArchivedRowWrapper, PrimaryKey: Clone, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc, @@ -329,7 +329,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, - <::WrappedRow as Archive>::Archived: GhostWrapper + VacuumWrapper, + <::WrappedRow as Archive>::Archived: ArchivedRowWrapper, PrimaryKey: Clone, AvailableTypes: 'static, AvailableIndexes: Debug + AvailableIndex, @@ -406,7 +406,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, - <::WrappedRow as Archive>::Archived: GhostWrapper + VacuumWrapper, + <::WrappedRow as Archive>::Archived: ArchivedRowWrapper, PrimaryKey: Clone, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc, diff --git a/src/table/vacuum/vacuum.rs b/src/table/vacuum/vacuum.rs index c9d501a..0ac7da0 100644 --- a/src/table/vacuum/vacuum.rs +++ b/src/table/vacuum/vacuum.rs @@ -15,12 +15,14 @@ use rkyv::ser::sharing::Share; use rkyv::util::AlignedVec; use rkyv::{Archive, Deserialize, Serialize}; -use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; -use crate::prelude::{OffsetEqLink, TablePrimaryKey, VacuumWrapper}; +use crate::in_memory::{ArchivedRowWrapper, DataPages, RowWrapper, StorableRow}; +use crate::prelude::{Lock, LockMap, OffsetEqLink, RowLock, TablePrimaryKey}; use crate::vacuum::VacuumStats; use crate::vacuum::WorkTableVacuum; use crate::vacuum::fragmentation_info::FragmentationInfo; -use crate::{AvailableIndex, PrimaryIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc}; +use crate::{ + AvailableIndex, PrimaryIndex, TableIndex, TableRow, TableSecondaryIndex, TableSecondaryIndexCdc, +}; use async_trait::async_trait; use ordered_float::OrderedFloat; use rkyv::api::high::HighDeserializer; @@ -45,10 +47,12 @@ pub struct EmptyDataVacuum< data_pages: Arc>, + lock_manager: Arc>, + primary_index: Arc>, secondary_indexes: Arc, - phantom_data: PhantomData<(SecondaryEvents, AvailableTypes, AvailableIndexes, LockType)>, + phantom_data: PhantomData<(SecondaryEvents, AvailableTypes, AvailableIndexes)>, } impl< @@ -87,13 +91,31 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, - <::WrappedRow as Archive>::Archived: GhostWrapper - + VacuumWrapper + <::WrappedRow as Archive>::Archived: ArchivedRowWrapper + Deserialize<::WrappedRow, HighDeserializer>, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc, AvailableIndexes: Debug + AvailableIndex, + LockType: RowLock, { + /// Creates a new [`EmptyDataVacuum`] from the given [`WorkTable`] components. + pub fn new( + table_name: &'static str, + data_pages: Arc>, + lock_manager: Arc>, + primary_index: Arc>, + secondary_indexes: Arc, + ) -> Self { + Self { + table_name, + data_pages, + lock_manager, + primary_index, + secondary_indexes, + phantom_data: PhantomData, + } + } + async fn defragment(&self) -> VacuumStats { let now = Instant::now(); @@ -146,9 +168,7 @@ where ), } } - for l in info.links { - self.data_pages.empty_links_registry().remove_link(l) - } + registry.remove_link_for_page(page_from); } let pages_freed = free_pages.len(); @@ -224,58 +244,74 @@ where drop(range); for (from_link, pk) in links { + let lock = self.full_row_lock(&pk).await; + if self + .data_pages + .with_ref(from_link.0, |r| r.is_deleted()) + .expect("link should be valid") + { + lock.unlock(); + self.lock_manager.remove_with_lock_check(&pk); + continue; + } + let raw_data = from_page + .get_raw_row(from_link.0) + .expect("link is not bigger than free offset"); unsafe { self.data_pages .with_mut_ref(from_link.0, |r| r.set_in_vacuum_process()) .expect("link should be valid") } - let raw_data = from_page - .get_raw_row(from_link.0) - .expect("link is not bigger than free offset"); let new_link = to_page .save_raw_row(&raw_data) .expect("page is not full as checked on links collection"); - self.update_index_after_move(pk, from_link.0, new_link); + self.update_index_after_move(pk.clone(), from_link.0, new_link); + self.lock_manager.remove_with_lock_check(&pk); + lock.unlock(); } (from_page_will_be_moved, to_page_will_be_filled) } - fn update_index_after_move(&self, pk: PrimaryKey, old_link: Link, new_link: Link) { - let old_offset_link = OffsetEqLink(old_link); - let new_offset_link = OffsetEqLink(new_link); + async fn full_row_lock(&self, pk: &PrimaryKey) -> Arc { + let lock_id = self.lock_manager.next_id(); + if let Some(lock) = self.lock_manager.get(pk) { + let mut lock_guard = lock.write().await; + #[allow(clippy::mutable_key_type)] + let (locks, op_lock) = lock_guard.lock(lock_id); + drop(lock_guard); + futures::future::join_all(locks.iter().map(|l| l.wait()).collect::>()).await; + + op_lock + } else { + #[allow(clippy::mutable_key_type)] + let (lock, op_lock) = LockType::with_lock(lock_id); + let lock = Arc::new(tokio::sync::RwLock::new(lock)); + let mut guard = lock.write().await; + if let Some(old_lock) = self.lock_manager.insert(pk.clone(), lock.clone()) { + let mut old_lock_guard = old_lock.write().await; + #[allow(clippy::mutable_key_type)] + let locks = guard.merge(&mut *old_lock_guard); + drop(old_lock_guard); + drop(guard); + + futures::future::join_all(locks.iter().map(|l| l.wait()).collect::>()).await; + } + + op_lock + } + } + fn update_index_after_move(&self, pk: PrimaryKey, old_link: Link, new_link: Link) { let row = self .data_pages .select(new_link) .expect("should exist as link was moved correctly"); - self.primary_index - .pk_map - .insert(pk.clone(), new_offset_link); - self.primary_index.reverse_pk_map.remove(&old_offset_link); - self.primary_index - .reverse_pk_map - .insert(new_offset_link, pk); self.secondary_indexes .reinsert_row(row.clone(), old_link, row, new_link) .expect("should be ok as index were no violated"); - } - - /// Creates a new [`EmptyDataVacuum`] from the given [`WorkTable`] components. - pub fn new( - table_name: &'static str, - data_pages: Arc>, - primary_index: Arc>, - secondary_indexes: Arc, - ) -> Self { - Self { - table_name, - data_pages, - primary_index, - secondary_indexes, - phantom_data: PhantomData, - } + self.primary_index.insert(pk.clone(), new_link); } } @@ -317,8 +353,7 @@ where Strategy, Share>, rkyv::rancor::Error>, > + Send + Sync, - <::WrappedRow as Archive>::Archived: GhostWrapper - + VacuumWrapper + <::WrappedRow as Archive>::Archived: ArchivedRowWrapper + Deserialize<::WrappedRow, HighDeserializer>, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc @@ -328,7 +363,7 @@ where SecondaryEvents: Send + Sync + 'static, AvailableTypes: Send + Sync + 'static, AvailableIndexes: Send + Sync + 'static, - LockType: Send + Sync, + LockType: RowLock + Send + Sync, { fn table_name(&self) -> &str { self.table_name @@ -352,7 +387,7 @@ mod tests { use indexset::core::pair::Pair; use worktable_codegen::{MemStat, worktable}; - use crate::in_memory::{GhostWrapper, RowWrapper, StorableRow}; + use crate::in_memory::{ArchivedRowWrapper, RowWrapper, StorableRow}; use crate::prelude::*; use crate::vacuum::vacuum::EmptyDataVacuum; @@ -387,6 +422,7 @@ mod tests { EmptyDataVacuum::new( table.name(), Arc::clone(&table.0.data), + Arc::clone(&table.0.lock_manager), Arc::clone(&table.0.primary_index), Arc::clone(&table.0.indexes), ) diff --git a/tests/worktable/vacuum.rs b/tests/worktable/vacuum.rs index a0008d2..5718e8d 100644 --- a/tests/worktable/vacuum.rs +++ b/tests/worktable/vacuum.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; + +use parking_lot::Mutex; use worktable::prelude::*; use worktable::vacuum::{VacuumManager, VacuumManagerConfig}; use worktable_codegen::worktable; @@ -159,14 +161,19 @@ async fn vacuum_parallel_with_upserts() { let delete_table = table.clone(); let ids_to_delete: Arc> = Arc::new(rows.iter().step_by(2).map(|p| p.0).collect()); + let row_state = Arc::new(Mutex::new(rows.iter().cloned().collect::>())); let task_ids = ids_to_delete.clone(); + let task_row_state = Arc::clone(&row_state); let delete_task = tokio::spawn(async move { for id in task_ids.iter() { delete_table.delete((*id).into()).await.unwrap(); + { + let mut g = task_row_state.lock(); + g.remove(id); + } } }); - let mut row_state = rows.iter().cloned().collect::>(); for _ in 0..3000 { let id = fastrand::u64(0..3000); let i = fastrand::i64(0..3000); @@ -177,17 +184,22 @@ async fn vacuum_parallel_with_upserts() { }; let id = row.id; table.upsert(row.clone()).await.unwrap(); - row_state.entry(id).and_modify(|r| *r = row); + { + let mut g = row_state.lock(); + g.entry(id).and_modify(|r| *r = row.clone()).or_insert(row); + } } + delete_task.await.unwrap(); + + let g = row_state.lock(); + // Verify all inserted rows are accessible - for (id, expected) in row_state.iter() { + for (id, expected) in g.iter() { let row = table.select(*id).await; assert_eq!(row, Some(expected.clone())); let row = row.unwrap(); let by_value = table.select_by_value(row.value).await; assert_eq!(by_value, Some(expected.clone())); } - - delete_task.await.unwrap(); } From 2a3e5bf3001ba2df64eb4dc7184f0a67dd8db181 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:51:28 +0300 Subject: [PATCH 25/32] correction --- codegen/src/worktable/generator/queries/delete.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index 507127f..fce59cc 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -75,9 +75,9 @@ impl Generator { let process = if self.is_persist { quote! { - self.0.data.delete(link).map_err(WorkTableError::PagesError)?; let secondary_keys_events = self.0.indexes.delete_row_cdc(row, link)?; let (_, primary_key_events) = self.0.primary_index.remove_cdc(pk.clone(), link); + self.0.data.delete(link).map_err(WorkTableError::PagesError)?; let mut op: Operation< <<#pk_ident as TablePrimaryKey>::Generator as PrimaryKeyGeneratorState>::State, #pk_ident, @@ -92,9 +92,9 @@ impl Generator { } } else { quote! { - self.0.data.delete(link).map_err(WorkTableError::PagesError)?; self.0.indexes.delete_row(row, link)?; self.0.primary_index.remove(&pk, link); + self.0.data.delete(link).map_err(WorkTableError::PagesError)?; } }; if is_locked { From 43182cdc1c15701fb60406c7409e1b6c0db2aefb Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:21:07 +0300 Subject: [PATCH 26/32] rework select --- .../src/worktable/generator/queries/delete.rs | 4 +-- .../src/worktable/generator/queries/update.rs | 4 +-- .../src/worktable/generator/table/impls.rs | 4 +-- src/persistence/task.rs | 1 - src/table/mod.rs | 2 +- src/table/vacuum/vacuum.rs | 26 ++++++++--------- tests/persistence/sync/many_strings.rs | 20 ++++++------- tests/persistence/sync/mod.rs | 24 ++++++++-------- .../persistence/sync/string_primary_index.rs | 24 ++++++++-------- tests/persistence/sync/string_re_read.rs | 18 ++++++------ .../sync/string_secondary_index.rs | 26 ++++++++--------- tests/worktable/array.rs | 28 +++++++++---------- tests/worktable/base.rs | 28 +++++++++---------- tests/worktable/in_place.rs | 22 +++++++-------- tests/worktable/index/insert.rs | 10 +++---- tests/worktable/index/update_full.rs | 12 ++++---- tests/worktable/option.rs | 6 ++-- tests/worktable/tuple_primary_key.rs | 4 +-- tests/worktable/unsized_.rs | 20 ++++++------- tests/worktable/uuid.rs | 4 +-- tests/worktable/vacuum.rs | 8 +++--- tests/worktable/with_enum.rs | 8 +++--- 22 files changed, 151 insertions(+), 152 deletions(-) diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index fce59cc..e526391 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -112,7 +112,7 @@ impl Generator { return Err(e); } }; - let row = self.select(pk.clone()).await.unwrap(); + let row = self.select(pk.clone()).unwrap(); #process } } else { @@ -123,7 +123,7 @@ impl Generator { .get(&pk) .map(|v| v.get().value.into()) .ok_or(WorkTableError::NotFound)?; - let row = self.select(pk.clone()).await.unwrap(); + let row = self.select(pk.clone()).unwrap(); #process } } diff --git a/codegen/src/worktable/generator/queries/update.rs b/codegen/src/worktable/generator/queries/update.rs index b100eae..952fd8f 100644 --- a/codegen/src/worktable/generator/queries/update.rs +++ b/codegen/src/worktable/generator/queries/update.rs @@ -271,7 +271,7 @@ impl Generator { #full_row_lock }; - let row_old = self.0.select(pk.clone()).await.expect("should not be deleted by other thread"); + let row_old = self.0.select(pk.clone()).expect("should not be deleted by other thread"); let mut row_new = row_old.clone(); let pk = row_old.get_primary_key().clone(); #(#row_updates)* @@ -574,7 +574,7 @@ impl Generator { let lock = { #full_row_lock }; - let row_old = self.0.select(pk.clone()).await.expect("should not be deleted by other thread"); + let row_old = self.0.select(pk.clone()).expect("should not be deleted by other thread"); let mut row_new = row_old.clone(); #(#row_updates)* if let Err(e) = self.reinsert(row_old, row_new).await { diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index 44a92b7..0ddae8b 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -113,9 +113,9 @@ impl Generator { let primary_key_type = name_generator.get_primary_key_type_ident(); quote! { - pub async fn select(&self, pk: Pk) -> Option<#row_type> + pub fn select(&self, pk: Pk) -> Option<#row_type> where #primary_key_type: From { - self.0.select(pk.into()).await + self.0.select(pk.into()) } } } diff --git a/src/persistence/task.rs b/src/persistence/task.rs index 3db8db8..1b16a16 100644 --- a/src/persistence/task.rs +++ b/src/persistence/task.rs @@ -243,7 +243,6 @@ where let mut row: BatchInnerRow = self .queue_inner_wt .select(id) - .await .expect("exists as Id exists") .into(); let op = self diff --git a/src/table/mod.rs b/src/table/mod.rs index ccab4c1..d718f01 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -148,7 +148,7 @@ where feature = "perf_measurements", performance_measurement(prefix_name = "WorkTable") )] - pub async fn select(&self, pk: PrimaryKey) -> Option + pub fn select(&self, pk: PrimaryKey) -> Option where LockType: 'static, Row: Archive diff --git a/src/table/vacuum/vacuum.rs b/src/table/vacuum/vacuum.rs index 0ac7da0..4563470 100644 --- a/src/table/vacuum/vacuum.rs +++ b/src/table/vacuum/vacuum.rs @@ -454,7 +454,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().skip(2) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } @@ -488,7 +488,7 @@ mod tests { .into_iter() .filter(|(i, _)| *i != ids_to_delete[0] && *i != ids_to_delete[1]) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } @@ -522,7 +522,7 @@ mod tests { .into_iter() .filter(|(i, _)| *i != last_two_ids[0] && *i != last_two_ids[1]) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } @@ -554,7 +554,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } @@ -585,7 +585,7 @@ mod tests { let vacuum = create_vacuum(&table); vacuum.defragment().await; - let row = table.select(remaining_id).await; + let row = table.select(remaining_id); assert_eq!(row, Some(ids[0].1.clone())); } @@ -612,7 +612,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().take(4) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } @@ -654,7 +654,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } @@ -701,12 +701,12 @@ mod tests { .into_iter() .filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } for (id, expected) in new_ids { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } @@ -738,7 +738,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } @@ -773,7 +773,7 @@ mod tests { .into_iter() .filter(|(id, _)| !ids_to_delete.contains(id)) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } @@ -802,7 +802,7 @@ mod tests { vacuum.defragment().await; for (id, expected) in ids.into_iter().take(499) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } @@ -841,7 +841,7 @@ mod tests { .into_iter() .filter(|(id, _)| !ids_to_delete.contains(id)) { - let row = table.select(id).await; + let row = table.select(id); assert_eq!(row, Some(expected)); } } diff --git a/tests/persistence/sync/many_strings.rs b/tests/persistence/sync/many_strings.rs index 96d97a0..3ae5e7c 100644 --- a/tests/persistence/sync/many_strings.rs +++ b/tests/persistence/sync/many_strings.rs @@ -61,8 +61,8 @@ fn test_space_update_query_pk_sync() { let table = TestSyncWorkTable::load_from_file(config.clone()) .await .unwrap(); - assert!(table.select(pk.clone()).await.is_some()); - assert_eq!(table.select(pk.clone()).await.unwrap().another, 43); + assert!(table.select(pk.clone()).is_some()); + assert_eq!(table.select(pk.clone()).unwrap().another, 43); let q = FieldAnotherByIdQuery { field: "Some field value".to_string(), another: 0, @@ -75,10 +75,10 @@ fn test_space_update_query_pk_sync() { } { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).await.is_some()); - assert_eq!(table.select(pk.clone()).await.unwrap().another, 0); + assert!(table.select(pk.clone()).is_some()); + assert_eq!(table.select(pk.clone()).unwrap().another, 0); assert_eq!( - table.select(pk).await.unwrap().field, + table.select(pk).unwrap().field, "Some field value".to_string() ); } @@ -128,8 +128,8 @@ fn test_space_update_query_pk_many_times_sync() { let table = TestSyncWorkTable::load_from_file(config.clone()) .await .unwrap(); - assert!(table.select(pk.clone()).await.is_some()); - assert_eq!(table.select(pk.clone()).await.unwrap().another, 43); + assert!(table.select(pk.clone()).is_some()); + assert_eq!(table.select(pk.clone()).unwrap().another, 43); for i in 0..512 { let q = FieldAnotherByIdQuery { field: "Some field value".to_string(), @@ -145,10 +145,10 @@ fn test_space_update_query_pk_many_times_sync() { } { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).await.is_some()); - assert_eq!(table.select(pk.clone()).await.unwrap().another, 511); + assert!(table.select(pk.clone()).is_some()); + assert_eq!(table.select(pk.clone()).unwrap().another, 511); assert_eq!( - table.select(pk).await.unwrap().field, + table.select(pk).unwrap().field, "Some field value".to_string() ); } diff --git a/tests/persistence/sync/mod.rs b/tests/persistence/sync/mod.rs index 8b3a780..ce3e97b 100644 --- a/tests/persistence/sync/mod.rs +++ b/tests/persistence/sync/mod.rs @@ -87,7 +87,7 @@ fn test_space_insert_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); + assert!(table.select(pk).is_some()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -133,7 +133,7 @@ fn test_space_insert_many_sync() { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); let last = *pks.last().unwrap(); for pk in pks { - assert!(table.select(pk).await.is_some()); + assert!(table.select(pk).is_some()); } assert_eq!(table.0.pk_gen.get_state(), last + 1) } @@ -180,8 +180,8 @@ fn test_space_update_full_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); - assert_eq!(table.select(pk).await.unwrap().another, 13); + assert!(table.select(pk).is_some()); + assert_eq!(table.select(pk).unwrap().another, 13); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -224,8 +224,8 @@ fn test_space_update_query_pk_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); - assert_eq!(table.select(pk).await.unwrap().another, 13); + assert!(table.select(pk).is_some()); + assert_eq!(table.select(pk).unwrap().another, 13); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -268,8 +268,8 @@ fn test_space_update_query_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); - assert_eq!(table.select(pk).await.unwrap().field, 1.0); + assert!(table.select(pk).is_some()); + assert_eq!(table.select(pk).unwrap().field, 1.0); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -312,8 +312,8 @@ fn test_space_update_query_non_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); - assert_eq!(table.select(pk).await.unwrap().another, 13); + assert!(table.select(pk).is_some()); + assert_eq!(table.select(pk).unwrap().another, 13); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -350,7 +350,7 @@ fn test_space_delete_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_none()); + assert!(table.select(pk).is_none()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -390,7 +390,7 @@ fn test_space_delete_query_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_none()); + assert!(table.select(pk).is_none()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); diff --git a/tests/persistence/sync/string_primary_index.rs b/tests/persistence/sync/string_primary_index.rs index 0b1fe59..4fe857c 100644 --- a/tests/persistence/sync/string_primary_index.rs +++ b/tests/persistence/sync/string_primary_index.rs @@ -61,7 +61,7 @@ fn test_space_insert_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); + assert!(table.select(pk).is_some()); } }); } @@ -107,7 +107,7 @@ fn test_space_insert_many_sync() { { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); for pk in pks { - assert!(table.select(pk).await.is_some()); + assert!(table.select(pk).is_some()); } } }); @@ -155,8 +155,8 @@ fn test_space_update_full_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).await.is_some()); - assert_eq!(table.select(pk).await.unwrap().another, 13); + assert!(table.select(pk.clone()).is_some()); + assert_eq!(table.select(pk).unwrap().another, 13); } }); } @@ -198,8 +198,8 @@ fn test_space_update_query_pk_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).await.is_some()); - assert_eq!(table.select(pk).await.unwrap().another, 13); + assert!(table.select(pk.clone()).is_some()); + assert_eq!(table.select(pk).unwrap().another, 13); } }); } @@ -242,8 +242,8 @@ fn test_space_update_query_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).await.is_some()); - assert_eq!(table.select(pk).await.unwrap().field, 1.0); + assert!(table.select(pk.clone()).is_some()); + assert_eq!(table.select(pk).unwrap().field, 1.0); } }); } @@ -286,8 +286,8 @@ fn test_space_update_query_non_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk.clone()).await.is_some()); - assert_eq!(table.select(pk).await.unwrap().another, 13); + assert!(table.select(pk.clone()).is_some()); + assert_eq!(table.select(pk).unwrap().another, 13); } }); } @@ -333,7 +333,7 @@ fn test_space_delete_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_none()); + assert!(table.select(pk).is_none()); } }); } @@ -372,7 +372,7 @@ fn test_space_delete_query_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_none()); + assert!(table.select(pk).is_none()); } }); } diff --git a/tests/persistence/sync/string_re_read.rs b/tests/persistence/sync/string_re_read.rs index 7d49271..e946a80 100644 --- a/tests/persistence/sync/string_re_read.rs +++ b/tests/persistence/sync/string_re_read.rs @@ -145,7 +145,7 @@ fn test_key_delete_scenario() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1); - assert!(table.select(pk).await.is_none()); + assert!(table.select(pk).is_none()); assert_eq!( table .select_by_first("first".to_string()) @@ -181,7 +181,7 @@ fn test_key_delete_scenario() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1); - assert!(table.select(pk0).await.is_none()); + assert!(table.select(pk0).is_none()); assert_eq!( table .select_by_first("first".to_string()) @@ -256,7 +256,7 @@ fn test_key_delete() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1); - assert!(table.select(pk).await.is_none()); + assert!(table.select(pk).is_none()); assert_eq!( table .select_by_first("first".to_string()) @@ -325,8 +325,8 @@ fn test_key_delete_all() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 0); - assert!(table.select(pk0).await.is_none()); - assert!(table.select(pk1).await.is_none()); + assert!(table.select(pk0).is_none()); + assert!(table.select(pk1).is_none()); assert_eq!( table .select_by_first("first".to_string()) @@ -419,7 +419,7 @@ fn test_key_delete_all_and_insert() { assert_eq!(table.select_all().execute().unwrap().len(), 1); - assert!(table.select(pk).await.is_some()); + assert!(table.select(pk).is_some()); assert_eq!( table .select_by_first("first".to_string()) @@ -493,7 +493,7 @@ fn test_key_delete_by_unique() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1); - assert!(table.select(pk).await.is_none()); + assert!(table.select(pk).is_none()); assert_eq!( table .select_by_first("first".to_string()) @@ -564,8 +564,8 @@ fn test_key_delete_by_non_unique() { .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 0); - assert!(table.select(pk0).await.is_none()); - assert!(table.select(pk1).await.is_none()); + assert!(table.select(pk0).is_none()); + assert!(table.select(pk1).is_none()); assert_eq!( table .select_by_first("first".to_string()) diff --git a/tests/persistence/sync/string_secondary_index.rs b/tests/persistence/sync/string_secondary_index.rs index 34cb6aa..60d6816 100644 --- a/tests/persistence/sync/string_secondary_index.rs +++ b/tests/persistence/sync/string_secondary_index.rs @@ -61,7 +61,7 @@ fn test_space_insert_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); + assert!(table.select(pk).is_some()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -109,7 +109,7 @@ fn test_space_insert_many_sync() { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); let last = *pks.last().unwrap(); for pk in pks { - assert!(table.select(pk).await.is_some()); + assert!(table.select(pk).is_some()); } assert_eq!(table.0.pk_gen.get_state(), last + 1) } @@ -155,16 +155,16 @@ fn test_space_update_full_sync() { .unwrap(); table.wait_for_ops().await; assert_eq!( - table.select(row.id).await.unwrap().another, + table.select(row.id).unwrap().another, "Some string to test updated".to_string() ); row.id }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); + assert!(table.select(pk).is_some()); assert_eq!( - table.select(pk).await.unwrap().another, + table.select(pk).unwrap().another, "Some string to test updated".to_string() ); assert_eq!(table.0.pk_gen.get_state(), pk + 1) @@ -214,9 +214,9 @@ fn test_space_update_query_pk_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); + assert!(table.select(pk).is_some()); assert_eq!( - table.select(pk).await.unwrap().another, + table.select(pk).unwrap().another, "Some string to test updated".to_string() ); assert_eq!(table.0.pk_gen.get_state(), pk + 1) @@ -265,8 +265,8 @@ fn test_space_update_query_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); - assert_eq!(table.select(pk).await.unwrap().field, 1.0); + assert!(table.select(pk).is_some()); + assert_eq!(table.select(pk).unwrap().field, 1.0); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -317,9 +317,9 @@ fn test_space_update_query_non_unique_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_some()); + assert!(table.select(pk).is_some()); assert_eq!( - table.select(pk).await.unwrap().another, + table.select(pk).unwrap().another, "Some string to test updated".to_string() ); assert_eq!(table.0.pk_gen.get_state(), pk + 1) @@ -361,7 +361,7 @@ fn test_space_delete_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_none()); + assert!(table.select(pk).is_none()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); @@ -401,7 +401,7 @@ fn test_space_delete_query_sync() { }; { let table = TestSyncWorkTable::load_from_file(config).await.unwrap(); - assert!(table.select(pk).await.is_none()); + assert!(table.select(pk).is_none()); assert_eq!(table.0.pk_gen.get_state(), pk + 1) } }); diff --git a/tests/worktable/array.rs b/tests/worktable/array.rs index 2be037b..dd40eab 100644 --- a/tests/worktable/array.rs +++ b/tests/worktable/array.rs @@ -24,10 +24,10 @@ async fn insert() { test: [1; 20], }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, row); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[tokio::test] @@ -43,10 +43,10 @@ async fn update() { test: [2; 20], }; table.update(new_row.clone()).await.unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, new_row); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[tokio::test] @@ -64,7 +64,7 @@ async fn update_in_a_middle() { test: [1; 20], }; table.update(new_row.clone()).await.unwrap(); - let selected_row = table.select(3).await.unwrap(); + let selected_row = table.select(3).unwrap(); assert_eq!(selected_row, new_row); } @@ -82,10 +82,10 @@ async fn update_query() { .update_test_by_id(q.clone(), pk.clone()) .await .unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row.test, q.test); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } type ArrI = [i64; 20]; @@ -111,10 +111,10 @@ async fn insert_i() { test: [1; 20], }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, row); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[tokio::test] @@ -130,10 +130,10 @@ async fn update_i() { test: [2; 20], }; table.update(new_row.clone()).await.unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, new_row); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[tokio::test] @@ -151,7 +151,7 @@ async fn update_in_a_middle_i() { test: [1; 20], }; table.update(new_row.clone()).await.unwrap(); - let selected_row = table.select(3).await.unwrap(); + let selected_row = table.select(3).unwrap(); assert_eq!(selected_row, new_row); } @@ -169,8 +169,8 @@ async fn update_query_i() { .update_test_i_by_id(q.clone(), pk.clone()) .await .unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row.test, q.test); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } diff --git a/tests/worktable/base.rs b/tests/worktable/base.rs index ef95353..6ba2fa0 100644 --- a/tests/worktable/base.rs +++ b/tests/worktable/base.rs @@ -121,10 +121,10 @@ async fn update_spawn() { .await .unwrap() .unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, updated); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[tokio::test] @@ -149,10 +149,10 @@ async fn upsert_spawn() { .await .unwrap() .unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, updated); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[tokio::test] @@ -172,10 +172,10 @@ async fn update() { exchange: "test".to_string(), }; table.update(updated.clone()).await.unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, updated); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[tokio::test] @@ -196,11 +196,11 @@ async fn update_string() { exchange: "much bigger test to make size of new row bigger than previous one".to_string(), }; table.update(updated.clone()).await.unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, updated); assert_eq!(table.0.data.get_empty_links().first().unwrap(), &first_link); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -279,7 +279,7 @@ async fn delete() { .map(|kv| kv.get().value) .unwrap(); table.delete(pk.clone()).await.unwrap(); - let selected_row = table.select(pk).await; + let selected_row = table.select(pk); assert!(selected_row.is_none()); let selected_row = table.select_by_test(1).await; assert!(selected_row.is_none()); @@ -392,7 +392,7 @@ async fn delete_and_insert_less() { .map(|kv| kv.get().value) .unwrap(); table.delete(pk.clone()).await.unwrap(); - let selected_row = table.select(pk).await; + let selected_row = table.select(pk); assert!(selected_row.is_none()); let updated = TestRow { @@ -438,7 +438,7 @@ async fn delete_and_replace() { .map(|kv| kv.get().value) .unwrap(); table.delete(pk.clone()).await.unwrap(); - let selected_row = table.select(pk).await; + let selected_row = table.select(pk); assert!(selected_row.is_none()); let updated = TestRow { @@ -476,10 +476,10 @@ async fn upsert() { exchange: "test".to_string(), }; table.upsert(updated.clone()).await.unwrap(); - let selected_row = table.select(row.id).await.unwrap(); + let selected_row = table.select(row.id).unwrap(); assert_eq!(selected_row, updated); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[test] @@ -1250,6 +1250,6 @@ async fn _bench() { } for a in v { - let _ = table.select(a).await.expect("TODO: panic message"); + let _ = table.select(a).expect("TODO: panic message"); } } diff --git a/tests/worktable/in_place.rs b/tests/worktable/in_place.rs index c81042d..28f536f 100644 --- a/tests/worktable/in_place.rs +++ b/tests/worktable/in_place.rs @@ -45,7 +45,7 @@ async fn test_update_val_by_id() -> eyre::Result<()> { .update_val_by_id_in_place(|val| *val += 1, pk.0) .await? } - let row = table.select(pk).await.unwrap(); + let row = table.select(pk).unwrap(); assert_eq!(row.val, 10000); Ok(()) } @@ -67,7 +67,7 @@ async fn test_update_val2_by_id() -> eyre::Result<()> { .update_val_2_by_id_in_place(|val| *val += 1, pk.0) .await? } - let row = table.select(pk).await.unwrap(); + let row = table.select(pk).unwrap(); assert_eq!(row.val2, 100); Ok(()) } @@ -99,7 +99,7 @@ async fn test_update_val_by_id_two_thread() -> eyre::Result<()> { .await? } h.await?; - let row = table.select(pk).await.unwrap(); + let row = table.select(pk).unwrap(); assert_eq!(row.val, 20_000); Ok(()) } @@ -151,7 +151,7 @@ async fn test_update_val_and_val2_by_id_four_thread() -> eyre::Result<()> { h1.await?; h2.await?; h3.await?; - let row = table.select(pk).await.unwrap(); + let row = table.select(pk).unwrap(); assert_eq!(row.val, 20_000); assert_eq!(row.val2, 20_000); Ok(()) @@ -204,7 +204,7 @@ async fn test_update_val_by_id_four_thread() -> eyre::Result<()> { h1.await?; h2.await?; h3.await?; - let row = table.select(pk).await.unwrap(); + let row = table.select(pk).unwrap(); assert_eq!(row.val, 40_000); Ok(()) } @@ -283,15 +283,15 @@ async fn test_update_in_place_and_update_sized_multithread() -> eyre::Result<()> h2.await?; for (id, smth) in i_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.something, smth); } for (id, val) in val2_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.val2, val); } for (id, val) in val_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.val, val); } Ok(()) @@ -376,12 +376,12 @@ async fn test_update_in_place_and_update_unsized_multithread() -> eyre::Result<( h2.await?; for (id, smth) in i_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.another, smth); } let mut errors = 0; for (id, val) in val2_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); if &row.val2 != val { errors += 1; } @@ -389,7 +389,7 @@ async fn test_update_in_place_and_update_unsized_multithread() -> eyre::Result<( assert_eq!(errors, 0); let mut errors = 0; for (id, val) in val_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); if &row.val != val { errors += 1; } diff --git a/tests/worktable/index/insert.rs b/tests/worktable/index/insert.rs index 12073bf..237c659 100644 --- a/tests/worktable/index/insert.rs +++ b/tests/worktable/index/insert.rs @@ -32,10 +32,10 @@ async fn insert() { attr4: "Attribute4".to_string(), }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, row); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[tokio::test] @@ -60,7 +60,7 @@ async fn insert_when_pk_exists() { attr4: "Attributee".to_string(), }; assert!(table.insert(next_row.clone()).is_err()); - assert_eq!(table.select(pk.clone()).await.unwrap(), row); + assert_eq!(table.select(pk.clone()).unwrap(), row); assert!( table .0 @@ -232,7 +232,7 @@ async fn insert_when_unique_violated() { }); for _ in 0..5000 { - let sel_row = table.select(row.id).await; + let sel_row = table.select(row.id); assert_eq!(sel_row, Some(row.clone())); let attr_1_rows = table.select_by_attr1(row.attr1.clone()).execute().unwrap(); assert_eq!(attr_1_rows.len(), 1); @@ -323,7 +323,7 @@ async fn insert_when_pk_violated() { }); for _ in 0..5000 { - let sel_row = table.select(id).await; + let sel_row = table.select(id); assert!(sel_row.is_some()); assert_eq!(sel_row, Some(row.clone())) } diff --git a/tests/worktable/index/update_full.rs b/tests/worktable/index/update_full.rs index 86c7c56..3a8cfe9 100644 --- a/tests/worktable/index/update_full.rs +++ b/tests/worktable/index/update_full.rs @@ -310,7 +310,7 @@ async fn update_by_full_row_with_reinsert_and_primary_key_violation() { update.attr1 = "TEST_______________________1".to_string(); assert!(test_table.update(update).await.is_err()); - assert_eq!(test_table.select(row1.id).await.unwrap(), row1); + assert_eq!(test_table.select(row1.id).unwrap(), row1); assert_eq!( test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), row1 @@ -318,7 +318,7 @@ async fn update_by_full_row_with_reinsert_and_primary_key_violation() { assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); - assert_eq!(test_table.select(row2.id).await.unwrap(), row2); + assert_eq!(test_table.select(row2.id).unwrap(), row2); assert_eq!( test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), row2 @@ -351,7 +351,7 @@ async fn update_by_full_row_with_reinsert_and_secondary_unique_violation() { update.attr1 = row2.attr1.clone(); assert!(test_table.update(update).await.is_err()); - assert_eq!(test_table.select(row1.id).await.unwrap(), row1); + assert_eq!(test_table.select(row1.id).unwrap(), row1); assert_eq!( test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), row1 @@ -359,7 +359,7 @@ async fn update_by_full_row_with_reinsert_and_secondary_unique_violation() { assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); - assert_eq!(test_table.select(row2.id).await.unwrap(), row2); + assert_eq!(test_table.select(row2.id).unwrap(), row2); assert_eq!( test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), row2 @@ -392,7 +392,7 @@ async fn update_by_full_row_with_secondary_unique_violation() { update.attr2 = row2.attr2; assert!(test_table.update(update).await.is_err()); - assert_eq!(test_table.select(row1.id).await.unwrap(), row1); + assert_eq!(test_table.select(row1.id).unwrap(), row1); assert_eq!( test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), row1 @@ -400,7 +400,7 @@ async fn update_by_full_row_with_secondary_unique_violation() { assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); - assert_eq!(test_table.select(row2.id).await.unwrap(), row2); + assert_eq!(test_table.select(row2.id).unwrap(), row2); assert_eq!( test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), row2 diff --git a/tests/worktable/option.rs b/tests/worktable/option.rs index 86480dc..378240b 100644 --- a/tests/worktable/option.rs +++ b/tests/worktable/option.rs @@ -39,7 +39,7 @@ async fn update() { exchange: 1, }; table.update(new_row.clone()).await.unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, new_row); } @@ -57,7 +57,7 @@ async fn update_by_another() { .update_test_by_another(TestByAnotherQuery { test: Some(1) }, 1) .await .unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row.test, Some(1)); } @@ -75,6 +75,6 @@ async fn update_by_exchange() { .update_test_by_exchange(TestByExchangeQuery { test: Some(1) }, 1) .await .unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row.test, Some(1)); } diff --git a/tests/worktable/tuple_primary_key.rs b/tests/worktable/tuple_primary_key.rs index 57ba76a..36150c1 100644 --- a/tests/worktable/tuple_primary_key.rs +++ b/tests/worktable/tuple_primary_key.rs @@ -19,8 +19,8 @@ async fn insert() { another: 1, }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, row); - assert!(table.select((1, 0)).await.is_none()) + assert!(table.select((1, 0)).is_none()) } diff --git a/tests/worktable/unsized_.rs b/tests/worktable/unsized_.rs index 4eb5267..7e07c7b 100644 --- a/tests/worktable/unsized_.rs +++ b/tests/worktable/unsized_.rs @@ -591,11 +591,11 @@ async fn update_parallel_more_strings() { h.await.unwrap(); for (id, e) in e_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.exchange, e) } for (id, s) in s_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.some_string, s) } } @@ -684,15 +684,15 @@ async fn update_parallel_more_strings_more_threads() { h2.await.unwrap(); for (id, e) in e_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.exchange, e) } for (id, s) in s_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.some_string, s) } for (id, a) in a_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.another, a) } } @@ -772,11 +772,11 @@ async fn update_parallel_more_strings_with_select_non_unique() { h2.await.unwrap(); for (id, e) in e_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.exchange, e) } for (id, a) in a_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.another, a) } } @@ -840,7 +840,7 @@ async fn delete_parallel() { h2.await.unwrap(); for id in deleted_state.lock_arc().iter() { - let row = table.select(*id).await; + let row = table.select(*id); assert!(row.is_none()) } } @@ -915,7 +915,7 @@ async fn update_parallel_more_strings_with_select_unique() { h2.await.unwrap(); for (id, e) in e_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.exchange, e) } } @@ -969,7 +969,7 @@ async fn upsert_parallel() { h1.await.unwrap(); for (id, e) in e_state.lock_arc().iter() { - let row = table.select(*id).await.unwrap(); + let row = table.select(*id).unwrap(); assert_eq!(&row.exchange, e) } } diff --git a/tests/worktable/uuid.rs b/tests/worktable/uuid.rs index 386e464..1e8ab42 100644 --- a/tests/worktable/uuid.rs +++ b/tests/worktable/uuid.rs @@ -18,8 +18,8 @@ async fn insert() { another: 1, }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, row); - assert!(table.select(Uuid::new_v4()).await.is_none()) + assert!(table.select(Uuid::new_v4()).is_none()) } diff --git a/tests/worktable/vacuum.rs b/tests/worktable/vacuum.rs index 5718e8d..914b2e2 100644 --- a/tests/worktable/vacuum.rs +++ b/tests/worktable/vacuum.rs @@ -57,7 +57,7 @@ async fn vacuum_parallel_with_selects() { for _ in 0..10 { // Verify all remaining rows are still accessible multiple times while vacuuming for (id, expected) in rows.iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(*id).await; + let row = table.select(*id); assert_eq!(row, Some(expected.clone())); let row = row.unwrap(); let by_value = table.select_by_value(row.value).await; @@ -116,7 +116,7 @@ async fn vacuum_parallel_with_inserts() { // Verify all remaining rows are still accessible for (id, expected) in rows.iter().filter(|(i, _)| !ids_to_delete.contains(i)) { - let row = table.select(*id).await; + let row = table.select(*id); assert_eq!(row, Some(expected.clone())); let row = row.unwrap(); let by_value = table.select_by_value(row.value).await; @@ -124,7 +124,7 @@ async fn vacuum_parallel_with_inserts() { } // Verify all inserted rows are accessible for (id, expected) in inserted_rows.iter() { - let row = table.select(*id).await; + let row = table.select(*id); assert_eq!(row, Some(expected.clone())); let row = row.unwrap(); let by_value = table.select_by_value(row.value).await; @@ -196,7 +196,7 @@ async fn vacuum_parallel_with_upserts() { // Verify all inserted rows are accessible for (id, expected) in g.iter() { - let row = table.select(*id).await; + let row = table.select(*id); assert_eq!(row, Some(expected.clone())); let row = row.unwrap(); let by_value = table.select_by_value(row.value).await; diff --git a/tests/worktable/with_enum.rs b/tests/worktable/with_enum.rs index 346b9d9..afdad03 100644 --- a/tests/worktable/with_enum.rs +++ b/tests/worktable/with_enum.rs @@ -31,10 +31,10 @@ async fn insert() { test: SomeEnum::First, }; let pk = table.insert(row.clone()).unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, row); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } #[tokio::test] @@ -50,8 +50,8 @@ async fn update() { test: SomeEnum::Second, }; table.update(updated.clone()).await.unwrap(); - let selected_row = table.select(pk).await.unwrap(); + let selected_row = table.select(pk).unwrap(); assert_eq!(selected_row, updated); - assert!(table.select(2).await.is_none()) + assert!(table.select(2).is_none()) } From 0b42714f8483bc316f13ac1e4f7f19fb16b79053 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:40:58 +0300 Subject: [PATCH 27/32] rework select indxed --- .../worktable/generator/table/index_fns.rs | 2 +- src/persistence/operation/batch.rs | 1 - tests/persistence/sync/string_re_read.rs | 20 +++---- tests/persistence/sync/uuid_.rs | 2 +- tests/worktable/base.rs | 12 ++-- tests/worktable/index/insert.rs | 2 +- tests/worktable/index/update_by_pk.rs | 36 +++++------ tests/worktable/index/update_full.rs | 60 +++++++++---------- tests/worktable/index/update_query.rs | 30 +++++----- tests/worktable/unsized_.rs | 18 +++--- tests/worktable/vacuum.rs | 8 +-- 11 files changed, 95 insertions(+), 96 deletions(-) diff --git a/codegen/src/worktable/generator/table/index_fns.rs b/codegen/src/worktable/generator/table/index_fns.rs index f2c1dab..d864fc2 100644 --- a/codegen/src/worktable/generator/table/index_fns.rs +++ b/codegen/src/worktable/generator/table/index_fns.rs @@ -64,7 +64,7 @@ impl Generator { }; Ok(quote! { - pub async fn #fn_name(&self, by: #type_) -> Option<#row_ident> { + pub fn #fn_name(&self, by: #type_) -> Option<#row_ident> { let link: Link = self.0.indexes.#field_ident.get(#by).map(|kv| kv.get().value.into())?; self.0.data.select_non_ghosted(link).ok() } diff --git a/src/persistence/operation/batch.rs b/src/persistence/operation/batch.rs index b52e70f..0ea195e 100644 --- a/src/persistence/operation/batch.rs +++ b/src/persistence/operation/batch.rs @@ -149,7 +149,6 @@ where let pk = self .info_wt .select_by_operation_id(op.operation_id()) - .await .expect("exists as all should be inserted on prepare step") .id; self.info_wt.delete_without_lock(pk.into()).await.unwrap(); diff --git a/tests/persistence/sync/string_re_read.rs b/tests/persistence/sync/string_re_read.rs index e946a80..5414af3 100644 --- a/tests/persistence/sync/string_re_read.rs +++ b/tests/persistence/sync/string_re_read.rs @@ -154,7 +154,7 @@ fn test_key_delete_scenario() { .len(), 1 ); - assert!(table.select_by_second("second_again".to_string()).await.is_none()); + assert!(table.select_by_second("second_again".to_string()).is_none()); table .insert(StringReReadRow { first: "first".to_string(), @@ -190,7 +190,7 @@ fn test_key_delete_scenario() { .len(), 1 ); - assert!(table.select_by_second("second".to_string()).await.is_none()); + assert!(table.select_by_second("second".to_string()).is_none()); } }) } @@ -265,7 +265,7 @@ fn test_key_delete() { .len(), 1 ); - assert!(table.select_by_second("second_again".to_string()).await.is_none()) + assert!(table.select_by_second("second_again".to_string()).is_none()) } }) } @@ -335,8 +335,8 @@ fn test_key_delete_all() { .len(), 0 ); - assert!(table.select_by_second("second_again".to_string()).await.is_none()); - assert!(table.select_by_second("second".to_string()).await.is_none()) + assert!(table.select_by_second("second_again".to_string()).is_none()); + assert!(table.select_by_second("second".to_string()).is_none()) } }) } @@ -428,7 +428,7 @@ fn test_key_delete_all_and_insert() { .len(), 1 ); - assert!(table.select_by_second("second".to_string()).await.is_some()) + assert!(table.select_by_second("second".to_string()).is_some()) } }) } @@ -502,7 +502,7 @@ fn test_key_delete_by_unique() { .len(), 1 ); - assert!(table.select_by_second("second_again".to_string()).await.is_none()) + assert!(table.select_by_second("second_again".to_string()).is_none()) } }) } @@ -574,8 +574,8 @@ fn test_key_delete_by_non_unique() { .len(), 0 ); - assert!(table.select_by_second("second".to_string()).await.is_none()); - assert!(table.select_by_second("second_again".to_string()).await.is_none()) + assert!(table.select_by_second("second".to_string()).is_none()); + assert!(table.select_by_second("second_again".to_string()).is_none()) } }) } @@ -633,7 +633,7 @@ fn test_big_amount_reread() { .await .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1001); - assert!(table.select_by_second("second_last".to_string()).await.is_some()); + assert!(table.select_by_second("second_last".to_string()).is_some()); } }) } diff --git a/tests/persistence/sync/uuid_.rs b/tests/persistence/sync/uuid_.rs index 9dc0ed8..a10274c 100644 --- a/tests/persistence/sync/uuid_.rs +++ b/tests/persistence/sync/uuid_.rs @@ -125,7 +125,7 @@ fn test_big_amount_reread() { .await .unwrap(); assert_eq!(table.select_all().execute().unwrap().len(), 1001); - assert!(table.select_by_second(second_last).await.is_some()); + assert!(table.select_by_second(second_last).is_some()); } }) } diff --git a/tests/worktable/base.rs b/tests/worktable/base.rs index 6ba2fa0..92f2a3f 100644 --- a/tests/worktable/base.rs +++ b/tests/worktable/base.rs @@ -256,7 +256,7 @@ async fn update_parallel() { h.await.unwrap(); for (test, val) in i_state.lock_arc().iter() { - let row = table.select_by_test(*test).await.unwrap(); + let row = table.select_by_test(*test).unwrap(); assert_eq!(row.another, *val) } } @@ -281,7 +281,7 @@ async fn delete() { table.delete(pk.clone()).await.unwrap(); let selected_row = table.select(pk); assert!(selected_row.is_none()); - let selected_row = table.select_by_test(1).await; + let selected_row = table.select_by_test(1); assert!(selected_row.is_none()); let selected_row = table.select_by_exchange("test".to_string()); assert!(selected_row.execute().expect("REASON").is_empty()); @@ -586,10 +586,10 @@ async fn select_by_test() { exchange: "test".to_string(), }; let _ = table.insert(row.clone()).unwrap(); - let selected_row = table.select_by_test(1).await.unwrap(); + let selected_row = table.select_by_test(1).unwrap(); assert_eq!(selected_row, row); - assert!(table.select_by_test(2).await.is_none()) + assert!(table.select_by_test(2).is_none()) } #[tokio::test] @@ -1191,7 +1191,7 @@ async fn test_update_by_unique() { let row = AnotherByTestQuery { another: 3 }; table.update_another_by_test(row, 1).await.unwrap(); - let row = table.select_by_test(1).await.unwrap(); + let row = table.select_by_test(1).unwrap(); assert_eq!( row, @@ -1218,7 +1218,7 @@ async fn test_update_by_pk() { let row = AnotherByIdQuery { another: 3 }; table.update_another_by_id(row, pk).await.unwrap(); - let row = table.select_by_test(1).await.unwrap(); + let row = table.select_by_test(1).unwrap(); assert_eq!( row, diff --git a/tests/worktable/index/insert.rs b/tests/worktable/index/insert.rs index 237c659..dd6667e 100644 --- a/tests/worktable/index/insert.rs +++ b/tests/worktable/index/insert.rs @@ -238,7 +238,7 @@ async fn insert_when_unique_violated() { assert_eq!(attr_1_rows.len(), 1); assert_eq!(attr_1_rows.first().unwrap(), &row); let row_new_attr_2_row = table.select_by_attr2(row_new_attr_2); - assert!(row_new_attr_2_row.await.is_none()); + assert!(row_new_attr_2_row.is_none()); } h.join().unwrap(); diff --git a/tests/worktable/index/update_by_pk.rs b/tests/worktable/index/update_by_pk.rs index 1405b57..bb9c86b 100644 --- a/tests/worktable/index/update_by_pk.rs +++ b/tests/worktable/index/update_by_pk.rs @@ -39,19 +39,19 @@ async fn update_by_pk_unique_indexes() { .unwrap(); // Checks idx updated - let updated = test_table.select_by_attr1(attr1_new.clone()).await; + let updated = test_table.select_by_attr1(attr1_new.clone()); assert_eq!(updated.unwrap().attr1, attr1_new); - let updated = test_table.select_by_attr2(attr2_new).await; + let updated = test_table.select_by_attr2(attr2_new); assert_eq!(updated.unwrap().attr2, attr2_new); - let updated = test_table.select_by_attr3(attr3_new).await; + let updated = test_table.select_by_attr3(attr3_new); assert_eq!(updated.unwrap().attr3, attr3_new); // Check old idx removed - let updated = test_table.select_by_attr1(attr1_old.clone()).await; + let updated = test_table.select_by_attr1(attr1_old.clone()); assert_eq!(updated, None); - let updated = test_table.select_by_attr2(attr2_old).await; + let updated = test_table.select_by_attr2(attr2_old); assert_eq!(updated, None); - let updated = test_table.select_by_attr3(attr3_old).await; + let updated = test_table.select_by_attr3(attr3_old); assert_eq!(updated, None); } @@ -156,18 +156,18 @@ async fn update_by_pk_with_reinsert_and_secondary_unique_violation() { ); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); } #[tokio::test] @@ -203,16 +203,16 @@ async fn update_by_pk_with_secondary_unique_violation() { ); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); } diff --git a/tests/worktable/index/update_full.rs b/tests/worktable/index/update_full.rs index 3a8cfe9..fa57ca1 100644 --- a/tests/worktable/index/update_full.rs +++ b/tests/worktable/index/update_full.rs @@ -36,19 +36,19 @@ async fn update_by_full_row_unique_indexes() { .unwrap(); // Checks idx updated - let updated = test_table.select_by_attr1(attr1_new.clone()).await; + let updated = test_table.select_by_attr1(attr1_new.clone()); assert_eq!(updated.unwrap().attr1, attr1_new); - let updated = test_table.select_by_attr2(attr2_new).await; + let updated = test_table.select_by_attr2(attr2_new); assert_eq!(updated.unwrap().attr2, attr2_new); - let updated = test_table.select_by_attr3(attr3_new).await; + let updated = test_table.select_by_attr3(attr3_new); assert_eq!(updated.unwrap().attr3, attr3_new); // Check old idx removed - let updated = test_table.select_by_attr1(attr1_old.clone()).await; + let updated = test_table.select_by_attr1(attr1_old.clone()); assert_eq!(updated, None); - let updated = test_table.select_by_attr2(attr2_old).await; + let updated = test_table.select_by_attr2(attr2_old); assert_eq!(updated, None); - let updated = test_table.select_by_attr3(attr3_old).await; + let updated = test_table.select_by_attr3(attr3_old); assert_eq!(updated, None); } @@ -177,19 +177,19 @@ async fn update_by_full_row_unique_with_string_update() { .unwrap(); // Checks idx updated - let updated = test_table.select_by_attr1(attr1_new.clone()).await; + let updated = test_table.select_by_attr1(attr1_new.clone()); assert_eq!(updated.unwrap().attr1, attr1_new); - let updated = test_table.select_by_attr2(attr2_new).await; + let updated = test_table.select_by_attr2(attr2_new); assert_eq!(updated.unwrap().attr2, attr2_new); - let updated = test_table.select_by_attr3(attr3_new).await; + let updated = test_table.select_by_attr3(attr3_new); assert_eq!(updated.unwrap().attr3, attr3_new); // Check old idx removed - let updated = test_table.select_by_attr1(attr1_old.clone()).await; + let updated = test_table.select_by_attr1(attr1_old.clone()); assert_eq!(updated, None); - let updated = test_table.select_by_attr2(attr2_old).await; + let updated = test_table.select_by_attr2(attr2_old); assert_eq!(updated, None); - let updated = test_table.select_by_attr3(attr3_old).await; + let updated = test_table.select_by_attr3(attr3_old); assert_eq!(updated, None); } @@ -312,19 +312,19 @@ async fn update_by_full_row_with_reinsert_and_primary_key_violation() { assert_eq!(test_table.select(row1.id).unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); assert_eq!(test_table.select(row2.id).unwrap(), row2); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); } #[tokio::test] @@ -353,19 +353,19 @@ async fn update_by_full_row_with_reinsert_and_secondary_unique_violation() { assert_eq!(test_table.select(row1.id).unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); assert_eq!(test_table.select(row2.id).unwrap(), row2); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); } #[tokio::test] @@ -394,17 +394,17 @@ async fn update_by_full_row_with_secondary_unique_violation() { assert_eq!(test_table.select(row1.id).unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); assert_eq!(test_table.select(row2.id).unwrap(), row2); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); } diff --git a/tests/worktable/index/update_query.rs b/tests/worktable/index/update_query.rs index adf0f7f..43ca8e9 100644 --- a/tests/worktable/index/update_query.rs +++ b/tests/worktable/index/update_query.rs @@ -40,11 +40,11 @@ async fn update_two_via_query_unique_indexes() { new_row.attr2 = attr2_new; // Check old idx removed - let updated = test_table.select_by_attr1(attr1_old.clone()).await; + let updated = test_table.select_by_attr1(attr1_old.clone()); assert_eq!(updated, None); - let updated = test_table.select_by_attr2(attr2_old).await; + let updated = test_table.select_by_attr2(attr2_old); assert_eq!(updated, None); - let updated = test_table.select_by_attr3(attr3_old).await; + let updated = test_table.select_by_attr3(attr3_old); assert!(updated.is_some()); assert_eq!(updated, Some(new_row)) } @@ -81,18 +81,18 @@ async fn update_with_reinsert_and_secondary_unique_violation() { ); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); } #[tokio::test] @@ -127,18 +127,18 @@ async fn update_with_secondary_unique_violation() { ); assert_eq!( - test_table.select_by_attr1(row1.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row1.attr1.clone()).unwrap(), row1 ); - assert_eq!(test_table.select_by_attr2(row1.attr2).await.unwrap(), row1); - assert_eq!(test_table.select_by_attr3(row1.attr3).await.unwrap(), row1); + assert_eq!(test_table.select_by_attr2(row1.attr2).unwrap(), row1); + assert_eq!(test_table.select_by_attr3(row1.attr3).unwrap(), row1); assert_eq!( - test_table.select_by_attr1(row2.attr1.clone()).await.unwrap(), + test_table.select_by_attr1(row2.attr1.clone()).unwrap(), row2 ); - assert_eq!(test_table.select_by_attr2(row2.attr2).await.unwrap(), row2); - assert_eq!(test_table.select_by_attr3(row2.attr3).await.unwrap(), row2); + assert_eq!(test_table.select_by_attr2(row2.attr2).unwrap(), row2); + assert_eq!(test_table.select_by_attr3(row2.attr3).unwrap(), row2); } #[tokio::test] diff --git a/tests/worktable/unsized_.rs b/tests/worktable/unsized_.rs index 7e07c7b..c47ea61 100644 --- a/tests/worktable/unsized_.rs +++ b/tests/worktable/unsized_.rs @@ -50,7 +50,7 @@ async fn test_update_string_full_row() { .await .unwrap(); - let row = table.select_by_test(1).await.unwrap(); + let row = table.select_by_test(1).unwrap(); assert_eq!( row, @@ -81,7 +81,7 @@ async fn test_update_string_by_unique() { }; table.update_exchange_by_test(row, 1).await.unwrap(); - let row = table.select_by_test(1).await.unwrap(); + let row = table.select_by_test(1).unwrap(); assert_eq!( row, @@ -112,7 +112,7 @@ async fn test_update_string_by_pk() { }; table.update_exchange_by_id(row, pk).await.unwrap(); - let row = table.select_by_test(1).await.unwrap(); + let row = table.select_by_test(1).unwrap(); assert_eq!( row, @@ -216,7 +216,7 @@ async fn update_many_times() { } for (test, val) in i_state { - let row = table.select_by_test(test).await.unwrap(); + let row = table.select_by_test(test).unwrap(); assert_eq!(row.exchange, val) } } @@ -284,7 +284,7 @@ async fn update_parallel() { h.await.unwrap(); for (test, val) in i_state.lock_arc().iter() { - let row = table.select_by_test(*test).await.unwrap(); + let row = table.select_by_test(*test).unwrap(); assert_eq!(&row.exchange, val) } } @@ -340,7 +340,7 @@ async fn test_update_many_strings_by_unique() { .await .unwrap(); - let row = table.select_by_test(1).await.unwrap(); + let row = table.select_by_test(1).unwrap(); assert_eq!( row, @@ -376,7 +376,7 @@ async fn test_update_many_strings_by_pk() { }; table.update_exchange_and_some_by_id(row, pk).await.unwrap(); - let row = table.select_by_test(1).await.unwrap(); + let row = table.select_by_test(1).unwrap(); assert_eq!( row, @@ -908,7 +908,7 @@ async fn update_parallel_more_strings_with_select_unique() { }); for _ in 0..20_000 { let val = fastrand::i64(0..1000); - let res = table.select_by_test(val).await; + let res = table.select_by_test(val); assert!(res.is_some()) } h1.await.unwrap(); @@ -963,7 +963,7 @@ async fn upsert_parallel() { }); for _ in 0..20_000 { let val = fastrand::i64(0..1000); - let res = table.select_by_test(val).await; + let res = table.select_by_test(val); assert!(res.is_some()) } h1.await.unwrap(); diff --git a/tests/worktable/vacuum.rs b/tests/worktable/vacuum.rs index 914b2e2..f02055c 100644 --- a/tests/worktable/vacuum.rs +++ b/tests/worktable/vacuum.rs @@ -60,7 +60,7 @@ async fn vacuum_parallel_with_selects() { let row = table.select(*id); assert_eq!(row, Some(expected.clone())); let row = row.unwrap(); - let by_value = table.select_by_value(row.value).await; + let by_value = table.select_by_value(row.value); assert_eq!(by_value, Some(expected.clone())); } } @@ -119,7 +119,7 @@ async fn vacuum_parallel_with_inserts() { let row = table.select(*id); assert_eq!(row, Some(expected.clone())); let row = row.unwrap(); - let by_value = table.select_by_value(row.value).await; + let by_value = table.select_by_value(row.value); assert_eq!(by_value, Some(expected.clone())); } // Verify all inserted rows are accessible @@ -127,7 +127,7 @@ async fn vacuum_parallel_with_inserts() { let row = table.select(*id); assert_eq!(row, Some(expected.clone())); let row = row.unwrap(); - let by_value = table.select_by_value(row.value).await; + let by_value = table.select_by_value(row.value); assert_eq!(by_value, Some(expected.clone())); } @@ -199,7 +199,7 @@ async fn vacuum_parallel_with_upserts() { let row = table.select(*id); assert_eq!(row, Some(expected.clone())); let row = row.unwrap(); - let by_value = table.select_by_value(row.value).await; + let by_value = table.select_by_value(row.value); assert_eq!(by_value, Some(expected.clone())); } } From ae95ed9f80504603fcb6f3b936c90663aa9dcb31 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:55:13 +0300 Subject: [PATCH 28/32] corrections --- Cargo.toml | 1 + src/in_memory/empty_link_registry.rs | 4 +- src/table/vacuum/fragmentation_info.rs | 12 +++-- src/table/vacuum/manager.rs | 9 ++-- tests/worktable/unsized_.rs | 10 ++-- tests/worktable/vacuum.rs | 63 +++++++++++++++++++++++++- 6 files changed, 83 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 016c9c8..58c68af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,3 +45,4 @@ log = "0.4.29" [dev-dependencies] rand = "0.9.1" +chrono = "0.4.43" diff --git a/src/in_memory/empty_link_registry.rs b/src/in_memory/empty_link_registry.rs index 5aa3638..4cf49af 100644 --- a/src/in_memory/empty_link_registry.rs +++ b/src/in_memory/empty_link_registry.rs @@ -79,11 +79,11 @@ pub struct EmptyLinkRegistry { index_ord_links: BTreeSet>, length_ord_links: BTreeMultiMap, - pub page_links_map: BTreeMultiMap, + pub(crate) page_links_map: BTreeMultiMap, sum_links_len: AtomicU32, - op_lock: FairMutex<()>, + pub(crate) op_lock: FairMutex<()>, vacuum_lock: tokio::sync::Mutex<()>, } diff --git a/src/table/vacuum/fragmentation_info.rs b/src/table/vacuum/fragmentation_info.rs index 02b87fa..b3eab0e 100644 --- a/src/table/vacuum/fragmentation_info.rs +++ b/src/table/vacuum/fragmentation_info.rs @@ -83,10 +83,14 @@ impl EmptyLinkRegistry { pub fn get_per_page_info(&self) -> Vec { let mut page_empty_data: HashMap)> = HashMap::new(); - for (page_id, link) in self.page_links_map.iter() { - let entry = page_empty_data.entry(*page_id).or_default(); - entry.0 += link.length; - entry.1.push(link.clone()); + { + let _op_lock = self.op_lock.lock(); + let iter = self.page_links_map.iter(); + for (page_id, link) in iter { + let entry = page_empty_data.entry(*page_id).or_default(); + entry.0 += link.length; + entry.1.push(link.clone()); + } } let mut per_page_data: Vec = page_empty_data diff --git a/src/table/vacuum/manager.rs b/src/table/vacuum/manager.rs index b7c2d9d..61022a2 100644 --- a/src/table/vacuum/manager.rs +++ b/src/table/vacuum/manager.rs @@ -81,12 +81,13 @@ impl VacuumManager { if let Some(vacuum) = vacuum_opt { let info = vacuum.analyze_fragmentation(); - log::info!("vacuum info: {:?}", info); - // println!("vacuum info: {:?}", info); + log::debug!("vacuum info: {:?}", info); + //println!("vacuum info: {:?}", info); if info.overall_fragmentation_ratio < self.config.low_fragmentation_threshold && info.overall_fragmentation_ratio != 0.0 { + log::debug!("Vacuuming {}", info.table_name); match vacuum.vacuum().await { Ok(stats) => { // println!( @@ -96,7 +97,7 @@ impl VacuumManager { // stats.bytes_freed, // stats.duration_ns as f64 / 1_000_000.0 // ); - log::info!( + log::debug!( "Vacuum completed for table '{}': {} pages processed, {} bytes freed in {:.2}ms", table_name, stats.pages_processed, @@ -106,7 +107,7 @@ impl VacuumManager { } Err(e) => { // println!("Vacuum failed for table '{}': {}", table_name, e); - log::error!("Vacuum failed for table '{}': {}", table_name, e); + log::debug!("Vacuum failed for table '{}': {}", table_name, e); } } } diff --git a/tests/worktable/unsized_.rs b/tests/worktable/unsized_.rs index c47ea61..cf4c0b2 100644 --- a/tests/worktable/unsized_.rs +++ b/tests/worktable/unsized_.rs @@ -961,10 +961,12 @@ async fn upsert_parallel() { } } }); - for _ in 0..20_000 { - let val = fastrand::i64(0..1000); - let res = table.select_by_test(val); - assert!(res.is_some()) + for _ in 0..2_000 { + let all = table.select_all().execute().unwrap(); + assert_eq!(all.len(), 1000); + // let val = fastrand::i64(0..1000); + // let res = table.select_by_test(val); + // assert!(res.is_some()) } h1.await.unwrap(); diff --git a/tests/worktable/vacuum.rs b/tests/worktable/vacuum.rs index f02055c..30a0a5a 100644 --- a/tests/worktable/vacuum.rs +++ b/tests/worktable/vacuum.rs @@ -1,8 +1,8 @@ +use chrono::TimeDelta; +use parking_lot::Mutex; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; - -use parking_lot::Mutex; use worktable::prelude::*; use worktable::vacuum::{VacuumManager, VacuumManagerConfig}; use worktable_codegen::worktable; @@ -203,3 +203,62 @@ async fn vacuum_parallel_with_upserts() { assert_eq!(by_value, Some(expected.clone())); } } + +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] +async fn vacuum_loop_test() { + let mut config = VacuumManagerConfig::default(); + config.check_interval = Duration::from_millis(1_000); + let vacuum_manager = Arc::new(VacuumManager::with_config(config)); + let table = Arc::new(VacuumTestWorkTable::default()); + + // Insert 3000 rows + for i in 0..3000 { + let row = VacuumTestRow { + id: table.get_next_pk().into(), + value: chrono::Utc::now().timestamp_nanos_opt().unwrap(), + data: format!("test_data_{}", i), + }; + table.insert(row.clone()).unwrap(); + } + + let vacuum = table.vacuum(); + vacuum_manager.register(vacuum); + let _h = vacuum_manager.run_vacuum_task(); + + let insert_table = table.clone(); + let _task = tokio::spawn(async move { + let mut i = 3000; + loop { + let row = VacuumTestRow { + id: insert_table.get_next_pk().into(), + value: chrono::Utc::now().timestamp_nanos_opt().unwrap(), + data: format!("test_data_{}", i), + }; + insert_table.insert(row.clone()).unwrap(); + tokio::time::sleep(Duration::from_micros(500)).await; + i += 1; + } + }); + + tokio::time::sleep(Duration::from_millis(1_000)).await; + + loop { + tokio::time::sleep(Duration::from_millis(500)).await; + + let outdated_ts = chrono::Utc::now() + .checked_sub_signed(TimeDelta::new(0, 500 * 1_000_000).unwrap()) + .unwrap() + .timestamp_nanos_opt() + .unwrap(); + let ids_to_remove = table + .0 + .indexes + .value_idx + .range(..outdated_ts) + .map(|(_, l)| table.0.data.select(**l).unwrap()) + .collect::>(); + for row in ids_to_remove { + table.delete(row.id.into()).await.unwrap() + } + } +} From 18a37a6ce8dc4320a8fda27ff427574bb936f6f2 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:43:34 +0300 Subject: [PATCH 29/32] clippy corrections --- src/in_memory/pages.rs | 2 +- src/lock/row_lock.rs | 1 + src/table/vacuum/fragmentation_info.rs | 2 +- src/table/vacuum/manager.rs | 6 +++--- src/table/vacuum/mod.rs | 1 + src/table/vacuum/vacuum.rs | 9 +++++---- tests/worktable/vacuum.rs | 25 +++++++++++++++++-------- 7 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/in_memory/pages.rs b/src/in_memory/pages.rs index 9109c00..eaab1bd 100644 --- a/src/in_memory/pages.rs +++ b/src/in_memory/pages.rs @@ -393,7 +393,7 @@ where pub fn get_empty_pages(&self) -> Vec { let g = self.empty_pages.read(); - g.iter().map(|p| *p).collect() + g.iter().copied().collect() } pub fn get_page( diff --git a/src/lock/row_lock.rs b/src/lock/row_lock.rs index 05107e4..7b75ba4 100644 --- a/src/lock/row_lock.rs +++ b/src/lock/row_lock.rs @@ -37,6 +37,7 @@ impl FullRowLock { } } +#[allow(clippy::mutable_key_type)] impl RowLock for FullRowLock { fn is_locked(&self) -> bool { self.l.is_locked() diff --git a/src/table/vacuum/fragmentation_info.rs b/src/table/vacuum/fragmentation_info.rs index b3eab0e..e9caf41 100644 --- a/src/table/vacuum/fragmentation_info.rs +++ b/src/table/vacuum/fragmentation_info.rs @@ -89,7 +89,7 @@ impl EmptyLinkRegistry { for (page_id, link) in iter { let entry = page_empty_data.entry(*page_id).or_default(); entry.0 += link.length; - entry.1.push(link.clone()); + entry.1.push(*link); } } diff --git a/src/table/vacuum/manager.rs b/src/table/vacuum/manager.rs index 61022a2..0f972f3 100644 --- a/src/table/vacuum/manager.rs +++ b/src/table/vacuum/manager.rs @@ -24,7 +24,7 @@ pub struct VacuumManagerConfig { pub critical_fragmentation_threshold: f64, } -#[derive(derive_more::Debug)] +#[derive(derive_more::Debug, Default)] pub struct VacuumManager { pub config: VacuumManagerConfig, pub id_gen: AtomicU64, @@ -35,7 +35,7 @@ pub struct VacuumManager { impl VacuumManager { /// Creates a new vacuum manager with default configuration. pub fn new() -> Self { - Self::with_config(VacuumManagerConfig::default()) + Self::default() } /// Creates a new vacuum manager with the given configuration. @@ -75,7 +75,7 @@ impl VacuumManager { for (id, table_name) in vacuums_to_check { let vacuum_opt = { let vacuums_read = self.vacuums.read(); - vacuums_read.get(&id).map(|v| v.clone()) + vacuums_read.get(&id).cloned() }; if let Some(vacuum) = vacuum_opt { diff --git a/src/table/vacuum/mod.rs b/src/table/vacuum/mod.rs index 6f1a365..5396c2e 100644 --- a/src/table/vacuum/mod.rs +++ b/src/table/vacuum/mod.rs @@ -4,6 +4,7 @@ use crate::vacuum::fragmentation_info::FragmentationInfo; mod fragmentation_info; mod manager; +#[allow(clippy::module_inception)] mod vacuum; pub use manager::{VacuumManager, VacuumManagerConfig}; diff --git a/src/table/vacuum/vacuum.rs b/src/table/vacuum/vacuum.rs index 4563470..a60e70c 100644 --- a/src/table/vacuum/vacuum.rs +++ b/src/table/vacuum/vacuum.rs @@ -134,8 +134,8 @@ where let pages_processed = per_page_info.len(); - let mut info_iter = per_page_info.into_iter(); - while let Some(info) = info_iter.next() { + let info_iter = per_page_info.into_iter(); + for info in info_iter { let page_from = info.page_id; loop { let page_to = if let Some(id) = defragmented_pages.pop_front() { @@ -407,6 +407,7 @@ mod tests { ); /// Creates an EmptyDataVacuum instance from a WorkTable + #[allow(clippy::type_complexity)] fn create_vacuum( table: &TestWorkTable, ) -> EmptyDataVacuum< @@ -622,7 +623,7 @@ mod tests { let table = TestWorkTable::default(); let mut ids = HashMap::new(); - let strings = vec![ + let strings = [ "a", "bbbb", "cccccc", @@ -835,7 +836,7 @@ mod tests { let vacuum = create_vacuum(&table); vacuum.defragment().await; - assert!(table.0.data.get_empty_pages().len() > 0); + assert!(!table.0.data.get_empty_pages().is_empty()); for (id, expected) in ids .into_iter() diff --git a/tests/worktable/vacuum.rs b/tests/worktable/vacuum.rs index 30a0a5a..34bcfa6 100644 --- a/tests/worktable/vacuum.rs +++ b/tests/worktable/vacuum.rs @@ -22,8 +22,10 @@ worktable!( #[tokio::test(flavor = "multi_thread", worker_threads = 3)] async fn vacuum_parallel_with_selects() { - let mut config = VacuumManagerConfig::default(); - config.check_interval = Duration::from_millis(5); + let config = VacuumManagerConfig { + check_interval: Duration::from_millis(5), + ..Default::default() + }; let vacuum_manager = Arc::new(VacuumManager::with_config(config)); let table = Arc::new(VacuumTestWorkTable::default()); @@ -70,8 +72,10 @@ async fn vacuum_parallel_with_selects() { #[tokio::test(flavor = "multi_thread", worker_threads = 3)] async fn vacuum_parallel_with_inserts() { - let mut config = VacuumManagerConfig::default(); - config.check_interval = Duration::from_millis(5); + let config = VacuumManagerConfig { + check_interval: Duration::from_millis(5), + ..Default::default() + }; let vacuum_manager = Arc::new(VacuumManager::with_config(config)); let table = Arc::new(VacuumTestWorkTable::default()); @@ -136,8 +140,10 @@ async fn vacuum_parallel_with_inserts() { #[tokio::test(flavor = "multi_thread", worker_threads = 3)] async fn vacuum_parallel_with_upserts() { - let mut config = VacuumManagerConfig::default(); - config.check_interval = Duration::from_millis(5); + let config = VacuumManagerConfig { + check_interval: Duration::from_millis(5), + ..Default::default() + }; let vacuum_manager = Arc::new(VacuumManager::with_config(config)); let table = Arc::new(VacuumTestWorkTable::default()); @@ -205,9 +211,12 @@ async fn vacuum_parallel_with_upserts() { } #[tokio::test(flavor = "multi_thread", worker_threads = 3)] +#[ignore] async fn vacuum_loop_test() { - let mut config = VacuumManagerConfig::default(); - config.check_interval = Duration::from_millis(1_000); + let config = VacuumManagerConfig { + check_interval: Duration::from_millis(1_000), + ..Default::default() + }; let vacuum_manager = Arc::new(VacuumManager::with_config(config)); let table = Arc::new(VacuumTestWorkTable::default()); From 7dcd4533d91d80cd41b5bfeb6cea0d003c63bf61 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:18:27 +0300 Subject: [PATCH 30/32] corrections --- src/in_memory/pages.rs | 227 +++++++++++++++++++++++++++++++++--- src/table/vacuum/manager.rs | 2 +- src/table/vacuum/vacuum.rs | 5 +- tests/worktable/vacuum.rs | 8 +- 4 files changed, 223 insertions(+), 19 deletions(-) diff --git a/src/in_memory/pages.rs b/src/in_memory/pages.rs index eaab1bd..edf83a8 100644 --- a/src/in_memory/pages.rs +++ b/src/in_memory/pages.rs @@ -201,8 +201,24 @@ where } } - /// Allocates new page but **NOT** sets it as `current`. - pub fn allocate_new_page(&self) -> Arc::WrappedRow, DATA_LENGTH>> { + /// Allocates a new page or reuses a free page from `empty_pages`. + /// Does **NOT** set the page as `current`. + pub fn allocate_new_or_pop_free( + &self, + ) -> Arc::WrappedRow, DATA_LENGTH>> { + let page_id = { + let mut empty_pages = self.empty_pages.write(); + empty_pages.pop_front() + }; + + if let Some(page_id) = page_id { + let pages = self.pages.read(); + let index = page_id_mapper(page_id.into()); + let page = pages[index].clone(); + page.reset(); + return page; + } + let mut pages = self.pages.write(); let index = self.last_page_id.fetch_add(1, Ordering::AcqRel) + 1; let page = Arc::new(Data::new(index.into())); @@ -391,6 +407,37 @@ where } } + /// Marks [`Page`] as full if it's not current [`Page`], which means put + /// [`Link`] from it's current offset to the end of the page in + /// [`EmptyLinkRegistry`] and set `free_offset` to max value. + /// + /// [`Page`]: Data + pub fn mark_page_full(&self, page_id: PageId) { + if u32::from(page_id) == self.current_page_id.load(Ordering::Acquire) { + return; + } + + let pages = self.pages.read(); + let index = page_id_mapper(page_id.into()); + + if let Some(page) = pages.get(index) { + let free_offset = page.free_offset.load(Ordering::Acquire); + let remaining = DATA_LENGTH.saturating_sub(free_offset as usize); + + if remaining > 0 { + let link = Link { + page_id, + offset: free_offset, + length: remaining as u32, + }; + self.empty_links.push(link); + } + + page.free_offset + .store(DATA_LENGTH as u32, Ordering::Release); + } + } + pub fn get_empty_pages(&self) -> Vec { let g = self.empty_pages.read(); g.iter().copied().collect() @@ -469,6 +516,7 @@ mod tests { use rkyv::with::{AtomicLoad, Relaxed}; use rkyv::{Archive, Deserialize, Serialize}; + use crate::in_memory::data::Data; use crate::in_memory::pages::{DataPages, ExecutionError}; use crate::in_memory::{DATA_INNER_LENGTH, PagesExecutionError, RowWrapper, StorableRow}; use crate::prelude::ArchivedRowWrapper; @@ -818,14 +866,14 @@ mod tests { } #[test] - fn allocate_new_page_creates_page_correctly() { + fn allocate_new_or_pop_free_creates_page_correctly() { let pages = DataPages::::new(); let initial_last_id = pages.last_page_id.load(Ordering::Relaxed); let initial_current = pages.current_page_id.load(Ordering::Relaxed); let initial_count = pages.get_page_count(); - let _allocated_page = pages.allocate_new_page(); + let _allocated_page = pages.allocate_new_or_pop_free(); assert_eq!( pages.last_page_id.load(Ordering::Relaxed), @@ -835,7 +883,7 @@ mod tests { assert_eq!( pages.current_page_id.load(Ordering::Relaxed), initial_current, - "current_page_id should NOT change after allocate_new_page" + "current_page_id should NOT change after allocate_new_or_pop_free" ); assert_eq!(pages.get_page_count(), initial_count + 1); @@ -851,9 +899,9 @@ mod tests { let initial_last_id = pages.last_page_id.load(Ordering::Relaxed); let initial_current = pages.current_page_id.load(Ordering::Relaxed); - let _page2 = pages.allocate_new_page(); - let _page3 = pages.allocate_new_page(); - let _page4 = pages.allocate_new_page(); + let _page2 = pages.allocate_new_or_pop_free(); + let _page3 = pages.allocate_new_or_pop_free(); + let _page4 = pages.allocate_new_or_pop_free(); assert_eq!( pages.last_page_id.load(Ordering::Relaxed), @@ -870,7 +918,7 @@ mod tests { fn insert_continues_on_current_page_after_allocation() { let pages = DataPages::::new(); - pages.allocate_new_page(); + pages.allocate_new_or_pop_free(); let row = TestRow { a: 42, b: 99 }; let link = pages.insert(row).unwrap(); @@ -879,7 +927,7 @@ mod tests { } #[test] - fn allocate_new_page_concurrent() { + fn allocate_new_or_pop_free_concurrent() { let pages = Arc::new(DataPages::::new()); let mut handles = Vec::new(); @@ -887,7 +935,7 @@ mod tests { let pages_clone = pages.clone(); let handle = thread::spawn(move || { for _ in 0..10 { - pages_clone.allocate_new_page(); + pages_clone.allocate_new_or_pop_free(); } }); handles.push(handle); @@ -905,7 +953,7 @@ mod tests { fn allocated_page_has_correct_initial_state() { let pages = DataPages::::new(); - let allocated = pages.allocate_new_page(); + let allocated = pages.allocate_new_or_pop_free(); assert_eq!(allocated.free_offset.load(Ordering::Relaxed), 0); assert_eq!(allocated.free_space(), DATA_INNER_LENGTH); @@ -916,7 +964,7 @@ mod tests { let pages = DataPages::::new(); // Allocate page explicitly - pages.allocate_new_page(); + pages.allocate_new_or_pop_free(); assert_eq!(pages.last_page_id.load(Ordering::Relaxed), 2); assert_eq!(pages.current_page_id.load(Ordering::Relaxed), 1); @@ -942,4 +990,157 @@ mod tests { assert_eq!(pages.current_page_id.load(Ordering::Relaxed), 3); assert_eq!(pages.get_page_count(), 3); } + + #[test] + fn allocate_new_or_pop_free_reuses_empty_page() { + let pages = DataPages::::from_data(vec![ + Arc::new(Data::new(1.into())), + Arc::new(Data::new(2.into())), + Arc::new(Data::new(3.into())), + ]); + + pages.mark_page_empty(2.into()); + + let initial_last_id = pages.last_page_id.load(Ordering::Relaxed); + let initial_page_count = pages.get_page_count(); + + let reused_page = pages.allocate_new_or_pop_free(); + + assert_eq!(reused_page.id, 2.into(), "Should reuse page 2"); + assert_eq!( + pages.last_page_id.load(Ordering::Relaxed), + initial_last_id, + "last_page_id should NOT increment when reusing" + ); + assert_eq!( + pages.get_page_count(), + initial_page_count, + "Page count should NOT increase when reusing" + ); + assert_eq!( + reused_page.free_offset.load(Ordering::Relaxed), + 0, + "Reused page should be reset (free_offset = 0)" + ); + assert_eq!( + reused_page.free_space(), + DATA_INNER_LENGTH, + "Reused page should have full free space" + ); + + let row = TestRow { a: 111, b: 222 }; + let link = pages.insert(row).unwrap(); + assert_eq!(link.page_id, 3.into()); + + pages.current_page_id.store(2, Ordering::Release); + let row2 = TestRow { a: 333, b: 444 }; + let link2 = pages.insert(row2).unwrap(); + assert_eq!(link2.page_id, 2.into(), "Should write to reused page 2"); + + let retrieved = pages.select(link2).unwrap(); + assert_eq!(retrieved, row2); + } + + #[test] + fn mark_page_full_adds_empty_link_and_sets_free_offset() { + let pages = DataPages::::from_data(vec![ + Arc::new(Data::new(1.into())), + Arc::new(Data::new(2.into())), + ]); + + let row = TestRow { a: 10, b: 20 }; + let _link = pages.insert(row).unwrap(); + + pages.current_page_id.store(2, Ordering::Release); + pages.mark_page_full(1.into()); + + let empty_links = pages.get_empty_links(); + assert!(!empty_links.is_empty(), "Should have empty links"); + + let link = empty_links.first().unwrap(); + assert_eq!(link.page_id, 1.into()); + assert_eq!( + link.length, 24, + "Should have remaining space = DATA_INNER_LENGTH - 24" + ); + + let page = pages.get_page(1.into()).unwrap(); + assert_eq!( + page.free_offset.load(Ordering::Relaxed), + DATA_INNER_LENGTH as u32, + "free_offset should be set to DATA_LENGTH" + ); + } + + #[test] + fn mark_page_full_does_nothing_for_current_or_nonexistent_page() { + let pages = DataPages::::new(); + + let initial_empty_links = pages.get_empty_links().len(); + pages.mark_page_full(1.into()); + + assert_eq!( + pages.get_empty_links().len(), + initial_empty_links, + "Should not add empty links for current page" + ); + + let page = pages.get_page(1.into()).unwrap(); + assert_ne!( + page.free_offset.load(Ordering::Relaxed), + DATA_INNER_LENGTH as u32, + "free_offset should NOT be modified for current page" + ); + + pages.mark_page_full(999.into()); + + assert!(pages.get_empty_links().is_empty()); + } + + #[test] + fn mark_page_full_with_partial_page() { + let pages = DataPages::::from_data(vec![ + Arc::new(Data::new(1.into())), + Arc::new(Data::new(2.into())), + ]); + + for _ in 0..10 { + let row = TestRow { a: 42, b: 99 }; + pages.insert(row).unwrap(); + } + + let page = pages.get_page(1.into()).unwrap(); + let free_offset_before = page.free_offset.load(Ordering::Relaxed); + let expected_remaining = DATA_INNER_LENGTH as u32 - free_offset_before; + + pages.current_page_id.store(2, Ordering::Release); + pages.mark_page_full(1.into()); + + let empty_links = pages.get_empty_links(); + let link = empty_links.first().unwrap(); + assert_eq!(link.offset, free_offset_before); + assert_eq!(link.length, expected_remaining); + + assert_eq!( + page.free_offset.load(Ordering::Relaxed), + DATA_INNER_LENGTH as u32 + ); + } + + #[test] + fn mark_page_full_with_no_remaining_space() { + let pages = DataPages::::from_data(vec![ + Arc::new(Data::new(1.into())), + Arc::new(Data::new(2.into())), + ]); + + let page = pages.get_page(1.into()).unwrap(); + page.free_offset + .store(DATA_INNER_LENGTH as u32, Ordering::Release); + + pages.current_page_id.store(2, Ordering::Release); + pages.mark_page_full(1.into()); + + assert!(pages.get_empty_links().is_empty()); + } } diff --git a/src/table/vacuum/manager.rs b/src/table/vacuum/manager.rs index 0f972f3..a3967fb 100644 --- a/src/table/vacuum/manager.rs +++ b/src/table/vacuum/manager.rs @@ -82,7 +82,7 @@ impl VacuumManager { let info = vacuum.analyze_fragmentation(); log::debug!("vacuum info: {:?}", info); - //println!("vacuum info: {:?}", info); + // println!("vacuum info: {:?}", info); if info.overall_fragmentation_ratio < self.config.low_fragmentation_threshold && info.overall_fragmentation_ratio != 0.0 diff --git a/src/table/vacuum/vacuum.rs b/src/table/vacuum/vacuum.rs index a60e70c..e8041d2 100644 --- a/src/table/vacuum/vacuum.rs +++ b/src/table/vacuum/vacuum.rs @@ -126,7 +126,7 @@ where OrderedFloat(l.filled_empty_ratio).cmp(&OrderedFloat(r.filled_empty_ratio)) }); let initial_bytes_freed: u64 = per_page_info.iter().map(|i| i.empty_bytes as u64).sum(); - let additional_allocated_page = self.data_pages.allocate_new_page(); + let additional_allocated_page = self.data_pages.allocate_new_or_pop_free(); let mut free_pages = VecDeque::new(); let mut defragmented_pages = VecDeque::new(); @@ -175,6 +175,9 @@ where for id in free_pages { self.data_pages.mark_page_empty(id) } + for id in defragmented_pages { + self.data_pages.mark_page_full(id) + } VacuumStats { pages_processed, diff --git a/tests/worktable/vacuum.rs b/tests/worktable/vacuum.rs index 34bcfa6..0a97f1d 100644 --- a/tests/worktable/vacuum.rs +++ b/tests/worktable/vacuum.rs @@ -211,7 +211,7 @@ async fn vacuum_parallel_with_upserts() { } #[tokio::test(flavor = "multi_thread", worker_threads = 3)] -#[ignore] +//#[ignore] async fn vacuum_loop_test() { let config = VacuumManagerConfig { check_interval: Duration::from_millis(1_000), @@ -236,7 +236,7 @@ async fn vacuum_loop_test() { let insert_table = table.clone(); let _task = tokio::spawn(async move { - let mut i = 3000; + let mut i = 3001; loop { let row = VacuumTestRow { id: insert_table.get_next_pk().into(), @@ -252,10 +252,10 @@ async fn vacuum_loop_test() { tokio::time::sleep(Duration::from_millis(1_000)).await; loop { - tokio::time::sleep(Duration::from_millis(500)).await; + tokio::time::sleep(Duration::from_millis(1_000)).await; let outdated_ts = chrono::Utc::now() - .checked_sub_signed(TimeDelta::new(0, 500 * 1_000_000).unwrap()) + .checked_sub_signed(TimeDelta::new(1, 0).unwrap()) .unwrap() .timestamp_nanos_opt() .unwrap(); From 82583918ac674668e32ff93fb77e9e7ea7437ed2 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:31:48 +0300 Subject: [PATCH 31/32] corrections --- Cargo.toml | 4 ++-- src/in_memory/pages.rs | 6 +++++- tests/worktable/vacuum.rs | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b10ac55..13f93fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,9 +28,9 @@ lockfree = { version = "0.5.1" } fastrand = "2.3.0" futures = "0.3.30" uuid = { version = "1.10.0", features = ["v4", "v7"] } -# data_bucket = "=0.3.10" +data_bucket = "=0.3.11" # data_bucket = { git = "https://github.com/pathscale/DataBucket", branch = "page_cdc_correction", version = "0.2.7" } -data_bucket = { path = "../DataBucket", version = "0.3.10" } +# data_bucket = { path = "../DataBucket", version = "0.3.10" } performance_measurement_codegen = { path = "performance_measurement/codegen", version = "0.1.0", optional = true } performance_measurement = { path = "performance_measurement", version = "0.1.0", optional = true } indexset = { version = "=0.14.0", features = ["concurrent", "cdc", "multimap"] } diff --git a/src/in_memory/pages.rs b/src/in_memory/pages.rs index edf83a8..92c3603 100644 --- a/src/in_memory/pages.rs +++ b/src/in_memory/pages.rs @@ -1048,6 +1048,9 @@ mod tests { Arc::new(Data::new(2.into())), ]); + // to manually insert on page 1 + pages.current_page_id.store(1, Ordering::Release); + let row = TestRow { a: 10, b: 20 }; let _link = pages.insert(row).unwrap(); @@ -1060,7 +1063,8 @@ mod tests { let link = empty_links.first().unwrap(); assert_eq!(link.page_id, 1.into()); assert_eq!( - link.length, 24, + link.length, + DATA_INNER_LENGTH as u32 - 24, "Should have remaining space = DATA_INNER_LENGTH - 24" ); diff --git a/tests/worktable/vacuum.rs b/tests/worktable/vacuum.rs index 0a97f1d..863ea49 100644 --- a/tests/worktable/vacuum.rs +++ b/tests/worktable/vacuum.rs @@ -211,7 +211,7 @@ async fn vacuum_parallel_with_upserts() { } #[tokio::test(flavor = "multi_thread", worker_threads = 3)] -//#[ignore] +#[ignore] async fn vacuum_loop_test() { let config = VacuumManagerConfig { check_interval: Duration::from_millis(1_000), From 50d22e88c7a4c174c2707a9803028588e0c41b90 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:35:39 +0300 Subject: [PATCH 32/32] bump --- Cargo.toml | 4 ++-- codegen/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 13f93fe..2fca76a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["codegen", "examples", "performance_measurement", "performance_measur [package] name = "worktable" -version = "0.8.19" +version = "0.8.20" edition = "2024" authors = ["Handy-caT"] license = "MIT" @@ -16,7 +16,7 @@ perf_measurements = ["dep:performance_measurement", "dep:performance_measurement # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -worktable_codegen = { path = "codegen", version = "=0.8.19" } +worktable_codegen = { path = "codegen", version = "=0.8.20" } async-trait = "0.1.89" eyre = "0.6.12" diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index e143384..0fc68f8 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "worktable_codegen" -version = "0.8.19" +version = "0.8.20" edition = "2024" license = "MIT" description = "WorkTable codegeneration crate"