diff --git a/Cargo.toml b/Cargo.toml index be46378..0fd79d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["codegen", "examples", "performance_measurement", "performance_measur [package] name = "worktable" -version = "0.6.13" +version = "0.6.14" 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.6.13" } +worktable_codegen = { path = "codegen", version = "0.6.14" } eyre = "0.6.12" derive_more = { version = "1.0.0", features = ["from", "error", "display", "into"] } diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index be0b8f3..588451e 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "worktable_codegen" -version = "0.6.13" +version = "0.6.14" edition = "2024" license = "MIT" description = "WorkTable codegeneration crate" diff --git a/codegen/src/name_generator.rs b/codegen/src/name_generator.rs index 7f8fc77..40e1414 100644 --- a/codegen/src/name_generator.rs +++ b/codegen/src/name_generator.rs @@ -87,6 +87,13 @@ impl WorktableNameGenerator { Ident::new(format!("{}Wrapper", self.name).as_str(), Span::mixed_site()) } + pub fn get_archived_wrapper_type_ident(&self) -> Ident { + Ident::new( + format!("Archived{}Wrapper", self.name).as_str(), + Span::mixed_site(), + ) + } + pub fn get_lock_type_ident(&self) -> Ident { Ident::new(format!("{}Lock", self.name).as_str(), Span::mixed_site()) } diff --git a/codegen/src/worktable/generator/queries/delete.rs b/codegen/src/worktable/generator/queries/delete.rs index dc77b13..353fa4e 100644 --- a/codegen/src/worktable/generator/queries/delete.rs +++ b/codegen/src/worktable/generator/queries/delete.rs @@ -179,7 +179,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(*link).map_err(WorkTableError::PagesError)?; + let row = self.0.data.select_non_ghosted(*link).map_err(WorkTableError::PagesError)?; self.delete(row.id.into()).await?; } core::result::Result::Ok(()) @@ -201,7 +201,7 @@ impl Generator { 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); if let Some(link) = row_to_update { - let row = self.0.data.select(link).map_err(WorkTableError::PagesError)?; + let row = self.0.data.select_non_ghosted(link).map_err(WorkTableError::PagesError)?; self.delete(row.id.into()).await?; } core::result::Result::Ok(()) diff --git a/codegen/src/worktable/generator/queries/select.rs b/codegen/src/worktable/generator/queries/select.rs index 1f50c3b..1676746 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(*link).ok()); + .filter_map(|(_, link)| self.0.data.select_non_ghosted(*link).ok()); SelectQueryBuilder::new(iter) } diff --git a/codegen/src/worktable/generator/queries/update.rs b/codegen/src/worktable/generator/queries/update.rs index 79c14ae..5330528 100644 --- a/codegen/src/worktable/generator/queries/update.rs +++ b/codegen/src/worktable/generator/queries/update.rs @@ -66,7 +66,7 @@ impl Generator { let lock = { #full_row_lock }; - let row_old = self.0.data.select(link)?; + let row_old = self.0.data.select_non_ghosted(link)?; self.reinsert(row_old, row)?; lock.unlock(); @@ -295,7 +295,7 @@ impl Generator { let avt_type_ident = name_generator.get_available_type_ident(); let diff_container = if idx_idents.is_some() { quote! { - let row_old = self.0.data.select(link)?; + let row_old = self.0.data.select_non_ghosted(link)?; let row_new = row.clone(); let updated_bytes: Vec = vec![]; let mut diffs: std::collections::HashMap<&str, Difference<#avt_type_ident>> = std::collections::HashMap::new(); @@ -514,7 +514,7 @@ impl Generator { let mut locks = std::collections::HashMap::new(); for link in links.iter() { - let pk = self.0.data.select(*link)?.get_primary_key().clone(); + let pk = self.0.data.select_non_ghosted(*link)?.get_primary_key().clone(); let op_lock = { #custom_lock }; @@ -525,7 +525,7 @@ impl Generator { 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() { - let pk = self.0.data.select(link)?.get_primary_key().clone(); + let pk = self.0.data.select_non_ghosted(link)?.get_primary_key().clone(); let mut bytes = rkyv::to_bytes::(&row) .map_err(|_| WorkTableError::SerializeError)?; @@ -610,7 +610,7 @@ impl Generator { .get(#by) .map(|kv| kv.get().value) .ok_or(WorkTableError::NotFound)?; - let pk = self.0.data.select(link)?.get_primary_key().clone(); + let pk = self.0.data.select_non_ghosted(link)?.get_primary_key().clone(); let lock = { #custom_lock diff --git a/codegen/src/worktable/generator/table/impls.rs b/codegen/src/worktable/generator/table/impls.rs index 2c18440..5212258 100644 --- a/codegen/src/worktable/generator/table/impls.rs +++ b/codegen/src/worktable/generator/table/impls.rs @@ -243,7 +243,7 @@ impl Generator { return Ok(()) }; - let data = self.0.data.select(link).map_err(WorkTableError::PagesError)?; + let data = self.0.data.select_non_ghosted(link).map_err(WorkTableError::PagesError)?; #func let mut ind = false; @@ -258,7 +258,7 @@ impl Generator { } }; if let Some((key, link)) = next { - let data = self.0.data.select(link).map_err(WorkTableError::PagesError)?; + let data = self.0.data.select_non_ghosted(link).map_err(WorkTableError::PagesError)?; #func k = key } else { diff --git a/codegen/src/worktable/generator/table/index_fns.rs b/codegen/src/worktable/generator/table/index_fns.rs index b316e73..205f832 100644 --- a/codegen/src/worktable/generator/table/index_fns.rs +++ b/codegen/src/worktable/generator/table/index_fns.rs @@ -66,7 +66,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)?; - self.0.data.select(link).ok() + 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(*link).ok()) + .filter_map(|(_, link)| self.0.data.select_non_ghosted(*link).ok()) .filter(move |r| &r.#row_field_ident == &by); SelectQueryBuilder::new(rows) diff --git a/codegen/src/worktable/generator/wrapper.rs b/codegen/src/worktable/generator/wrapper.rs index c50df98..b77f0ad 100644 --- a/codegen/src/worktable/generator/wrapper.rs +++ b/codegen/src/worktable/generator/wrapper.rs @@ -8,11 +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(); quote! { #type_ #impl_ #storable_impl + #ghost_wrapper_impl } } @@ -26,6 +28,7 @@ impl Generator { #[repr(C)] pub struct #wrapper_ident { inner: #row_ident, + is_ghosted: bool, is_deleted: bool, } } @@ -43,10 +46,15 @@ impl Generator { self.inner } + fn is_ghosted(&self) -> bool { + self.is_ghosted + } + fn from_inner(inner: #row_ident) -> Self { Self { inner, - is_deleted: Default::default(), + is_ghosted: true, + is_deleted: false, } } } @@ -64,4 +72,17 @@ impl Generator { } } } + + fn get_wrapper_ghost_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 { + fn unghost(&mut self) { + self.is_ghosted = false; + } + } + } + } } diff --git a/src/in_memory/data.rs b/src/in_memory/data.rs index a20099f..2ac1381 100644 --- a/src/in_memory/data.rs +++ b/src/in_memory/data.rs @@ -220,7 +220,7 @@ impl Data { } /// Error that can appear on [`Data`] page operations. -#[derive(Copy, Clone, Debug, Display, Error)] +#[derive(Copy, Clone, Debug, Display, Error, PartialEq)] pub enum ExecutionError { /// Error of trying to save row in [`Data`] page with not enough space left. #[display("need {}, but {} left", need, left)] diff --git a/src/in_memory/mod.rs b/src/in_memory/mod.rs index 126cdeb..43b59e2 100644 --- a/src/in_memory/mod.rs +++ b/src/in_memory/mod.rs @@ -4,4 +4,4 @@ mod row; pub use data::{Data, ExecutionError as DataExecutionError, DATA_INNER_LENGTH}; pub use pages::{DataPages, ExecutionError as PagesExecutionError}; -pub use row::{RowWrapper, StorableRow}; +pub use row::{GhostWrapper, RowWrapper, StorableRow}; diff --git a/src/in_memory/pages.rs b/src/in_memory/pages.rs index f51f9da..894cbd7 100644 --- a/src/in_memory/pages.rs +++ b/src/in_memory/pages.rs @@ -203,6 +203,27 @@ where Ok(gen_row.get_inner()) } + pub fn select_non_ghosted(&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().unwrap(); + 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)?; + if gen_row.is_ghosted() { + return Err(ExecutionError::Ghosted); + } + Ok(gen_row.get_inner()) + } + #[cfg_attr( feature = "perf_measurements", performance_measurement(prefix_name = "DataPages") @@ -332,28 +353,31 @@ where } } -#[derive(Debug, Display, Error, From)] +#[derive(Debug, Display, Error, From, PartialEq)] pub enum ExecutionError { DataPageError(DataExecutionError), PageNotFound(#[error(not(source))] PageId), Locked, + + Ghosted, } #[cfg(test)] mod tests { use std::collections::HashSet; - use std::sync::atomic::Ordering; + use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; use std::thread; use std::time::Instant; - use crate::in_memory::pages::DataPages; - use crate::in_memory::row::GeneralRow; - use crate::in_memory::StorableRow; + use rkyv::with::{AtomicLoad, Relaxed}; use rkyv::{Archive, Deserialize, Serialize}; + use crate::in_memory::pages::DataPages; + use crate::in_memory::{PagesExecutionError, RowWrapper, StorableRow}; + #[derive( Archive, Copy, Clone, Deserialize, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, )] @@ -362,6 +386,41 @@ mod tests { b: u64, } + /// General `Row` wrapper that is used to append general data for every `Inner` + /// `Row`. + #[derive(Archive, Deserialize, Debug, Serialize)] + pub struct GeneralRow { + /// Inner generic `Row`. + pub inner: Inner, + + /// Indicator for ghosted rows. + #[rkyv(with = AtomicLoad)] + pub is_ghosted: AtomicBool, + + /// Indicator for deleted rows. + #[rkyv(with = AtomicLoad)] + pub deleted: AtomicBool, + } + + impl RowWrapper for GeneralRow { + fn get_inner(self) -> Inner { + self.inner + } + + fn is_ghosted(&self) -> bool { + self.is_ghosted.load(Ordering::Relaxed) + } + + /// Creates new [`GeneralRow`] from `Inner`. + fn from_inner(inner: Inner) -> Self { + Self { + inner, + is_ghosted: AtomicBool::new(true), + deleted: AtomicBool::new(false), + } + } + } + impl StorableRow for TestRow { type WrappedRow = GeneralRow; } @@ -404,6 +463,17 @@ mod tests { assert_eq!(res, row) } + #[test] + fn select_non_ghosted() { + let pages = DataPages::::new(); + + let row = TestRow { a: 10, b: 20 }; + let link = pages.insert(row).unwrap(); + let res = pages.select_non_ghosted(link); + assert!(res.is_err()); + assert_eq!(res.err(), Some(PagesExecutionError::Ghosted)) + } + #[test] fn update() { let pages = DataPages::::new(); diff --git a/src/in_memory/row.rs b/src/in_memory/row.rs index 7586dec..8404607 100644 --- a/src/in_memory/row.rs +++ b/src/in_memory/row.rs @@ -1,8 +1,6 @@ use std::fmt::Debug; -use std::sync::atomic::AtomicBool; -use rkyv::with::{AtomicLoad, Relaxed}; -use rkyv::{Archive, Deserialize, Serialize}; +use rkyv::Archive; /// Common trait for the `Row`s that can be stored on the [`Data`] page. /// @@ -13,32 +11,10 @@ pub trait StorableRow { pub trait RowWrapper { fn get_inner(self) -> Inner; - + fn is_ghosted(&self) -> bool; fn from_inner(inner: Inner) -> Self; } -/// General `Row` wrapper that is used to append general data for every `Inner` -/// `Row`. -#[derive(Archive, Deserialize, Debug, Serialize)] -pub struct GeneralRow { - /// Inner generic `Row`. - pub inner: Inner, - - /// Indicator for deleted rows. - #[rkyv(with = AtomicLoad)] - pub deleted: AtomicBool, -} - -impl RowWrapper for GeneralRow { - fn get_inner(self) -> Inner { - self.inner - } - - /// Creates new [`GeneralRow`] from `Inner`. - fn from_inner(inner: Inner) -> Self { - Self { - inner, - deleted: AtomicBool::new(false), - } - } +pub trait GhostWrapper { + fn unghost(&mut self); } diff --git a/src/index/unsized_node.rs b/src/index/unsized_node.rs index 4391262..40f7033 100644 --- a/src/index/unsized_node.rs +++ b/src/index/unsized_node.rs @@ -114,7 +114,7 @@ where } fn need_to_split(&self, _: usize) -> bool { - self.length >= self.length_capacity + self.length >= self.length_capacity && self.inner.len() > 1 } fn len(&self) -> usize { diff --git a/src/lib.rs b/src/lib.rs index 494f15a..6dae667 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ pub use table::*; pub use worktable_codegen::worktable; pub mod prelude { - pub use crate::in_memory::{Data, DataPages, RowWrapper, StorableRow}; + pub use crate::in_memory::{Data, DataPages, GhostWrapper, RowWrapper, StorableRow}; pub use crate::lock::LockMap; pub use crate::lock::{Lock, RowLock}; pub use crate::mem_stat::MemStat; diff --git a/src/table/mod.rs b/src/table/mod.rs index d4ade9c..7f19b0e 100644 --- a/src/table/mod.rs +++ b/src/table/mod.rs @@ -4,7 +4,7 @@ pub mod system_info; use std::fmt::Debug; use std::marker::PhantomData; -use crate::in_memory::{DataPages, RowWrapper, StorableRow}; +use crate::in_memory::{DataPages, GhostWrapper, RowWrapper, StorableRow}; use crate::lock::LockMap; use crate::persistence::{InsertOperation, Operation}; use crate::prelude::{OperationId, PrimaryKeyGeneratorState}; @@ -172,6 +172,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, + <::WrappedRow as Archive>::Archived: GhostWrapper, PrimaryKey: Clone, AvailableTypes: 'static, SecondaryIndexes: TableSecondaryIndex, @@ -182,12 +183,8 @@ where .data .insert(row.clone()) .map_err(WorkTableError::PagesError)?; - if self - .pk_map - .insert(pk.clone(), link) - .map_or(Ok(()), |_| Err(WorkTableError::AlreadyExists)) - .is_err() - { + if let Some(existed_link) = self.pk_map.insert(pk.clone(), link) { + self.pk_map.insert(pk.clone(), existed_link); self.data.delete(link).map_err(WorkTableError::PagesError)?; return Err(WorkTableError::AlreadyExists); }; @@ -207,6 +204,11 @@ where IndexError::NotFound => Err(WorkTableError::NotFound), }; } + unsafe { + self.data + .with_mut_ref(link, |r| r.unghost()) + .map_err(WorkTableError::PagesError)? + } Ok(pk) } @@ -232,6 +234,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, + <::WrappedRow as Archive>::Archived: GhostWrapper, PrimaryKey: Clone, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc, @@ -239,12 +242,13 @@ where AvailableIndexes: Debug, { let pk = row.get_primary_key().clone(); - let (link, bytes) = self + let (link, _) = self .data .insert_cdc(row.clone()) .map_err(WorkTableError::PagesError)?; let (exists, primary_key_events) = self.pk_map.insert_cdc(pk.clone(), link); - if exists.is_some() { + if let Some(existed_link) = exists { + self.pk_map.insert(pk.clone(), existed_link); self.data.delete(link).map_err(WorkTableError::PagesError)?; return Err(WorkTableError::AlreadyExists); } @@ -265,6 +269,15 @@ where IndexError::NotFound => Err(WorkTableError::NotFound), }; } + unsafe { + self.data + .with_mut_ref(link, |r| r.unghost()) + .map_err(WorkTableError::PagesError)? + } + let bytes = self + .data + .select_raw(link) + .map_err(WorkTableError::PagesError)?; let op = Operation::Insert(InsertOperation { id: OperationId::Single(Uuid::now_v7()), @@ -297,6 +310,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, + <::WrappedRow as Archive>::Archived: GhostWrapper, PrimaryKey: Clone, AvailableTypes: 'static, SecondaryIndexes: TableSecondaryIndex, @@ -312,6 +326,11 @@ where .data .insert(row_new.clone()) .map_err(WorkTableError::PagesError)?; + unsafe { + self.data + .with_mut_ref(new_link, |r| r.unghost()) + .map_err(WorkTableError::PagesError)? + } // we can not check for existence here. self.pk_map.insert(pk.clone(), new_link); self.indexes @@ -346,6 +365,7 @@ where + for<'a> Serialize< Strategy, Share>, rkyv::rancor::Error>, >, + <::WrappedRow as Archive>::Archived: GhostWrapper, PrimaryKey: Clone, SecondaryIndexes: TableSecondaryIndex + TableSecondaryIndexCdc, @@ -358,10 +378,15 @@ where .get(&pk) .map(|v| v.get().value) .ok_or(WorkTableError::NotFound)?; - let (new_link, bytes) = self + let (new_link, _) = self .data .insert_cdc(row_new.clone()) .map_err(WorkTableError::PagesError)?; + unsafe { + self.data + .with_mut_ref(new_link, |r| r.unghost()) + .map_err(WorkTableError::PagesError)? + } // we can not check for existence here. let (_, primary_key_events) = self.pk_map.insert_cdc(pk.clone(), new_link); let secondary_keys_events = self @@ -371,6 +396,10 @@ where self.data .delete(old_link) .map_err(WorkTableError::PagesError)?; + let bytes = self + .data + .select_raw(new_link) + .map_err(WorkTableError::PagesError)?; let op = Operation::Insert(InsertOperation { id: OperationId::Single(Uuid::now_v7()), diff --git a/tests/worktable/index/insert.rs b/tests/worktable/index/insert.rs index a3ac487..85a7faf 100644 --- a/tests/worktable/index/insert.rs +++ b/tests/worktable/index/insert.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; +use std::thread; use worktable::prelude::*; use worktable::worktable; @@ -9,10 +11,12 @@ worktable!( attr1: String, attr2: i16, attr3: u64, + attr4: String, }, indexes: { attr1_idx: attr1, attr2_idx: attr2 unique, + attr4_idx: attr4 unique, } ); @@ -25,6 +29,7 @@ fn insert() { attr1: "Attribute".to_string(), attr2: -128, attr3: 123456789, + attr4: "Attribute4".to_string(), }; let pk = table.insert(row.clone()).unwrap(); let selected_row = table.select(pk).unwrap(); @@ -42,6 +47,7 @@ fn insert_when_pk_exists() { attr1: "Attribute".to_string(), attr2: -128, attr3: 123456789, + attr4: "Attribute4".to_string(), }; let pk = table.insert(row.clone()).unwrap(); @@ -51,8 +57,10 @@ fn insert_when_pk_exists() { attr1: "some str".to_string(), attr2: 0, attr3: 0, + attr4: "Attributee".to_string(), }; assert!(table.insert(next_row.clone()).is_err()); + assert_eq!(table.select(pk.clone()).unwrap(), row); assert!(table .0 .indexes @@ -83,6 +91,7 @@ fn insert_when_secondary_unique_exists() { attr1: "Attribute".to_string(), attr2: -128, attr3: 123456789, + attr4: "Attribute4".to_string(), }; let _ = table.insert(row.clone()).unwrap(); @@ -92,6 +101,7 @@ fn insert_when_secondary_unique_exists() { attr1: "some str".to_string(), attr2: row.attr2, attr3: 0, + attr4: "Attributeee".to_string(), }; assert!(table.insert(next_row.clone()).is_err()); assert!(table @@ -125,3 +135,101 @@ fn insert_when_secondary_unique_exists() { .map(|r| r.get().value) ); } + +#[test] +fn insert_when_secondary_unique_string_exists() { + let table = TestWorkTable::default(); + let row = TestRow { + id: table.get_next_pk().into(), + val: 13, + attr1: "Attribute".to_string(), + attr2: -128, + attr3: 123456789, + attr4: "Attribute4".to_string(), + }; + let _ = table.insert(row.clone()).unwrap(); + + let next_row = TestRow { + id: table.get_next_pk().into(), + val: 0, + attr1: "some str".to_string(), + attr2: 128, + attr3: 0, + attr4: "Attribute4".to_string(), + }; + assert!(table.insert(next_row.clone()).is_err()); + assert!(table + .0 + .indexes + .attr1_idx + .get(&next_row.attr1) + .collect::>() + .is_empty()); + assert!(table.0.indexes.attr4_idx.get(&row.attr4).is_some()); + assert_eq!( + table + .0 + .indexes + .attr1_idx + .get(&row.attr1) + .collect::>() + .len(), + 1 + ); + assert_eq!( + table + .0 + .indexes + .attr4_idx + .get(&row.attr4) + .map(|r| r.get().value), + table + .0 + .pk_map + .get(&TestPrimaryKey(row.id)) + .map(|r| r.get().value) + ); +} + +#[test] +fn insert_when_unique_violated() { + let table = Arc::new(TestWorkTable::default()); + + let row = TestRow { + id: table.get_next_pk().into(), + val: 13, + attr1: "Attribute".to_string(), + attr2: -128, + attr3: 123456789, + attr4: "Attribute4".to_string(), + }; + let _ = table.insert(row.clone()).unwrap(); + + let row_new_attr_2 = 128; + let row_new_attr_4 = row.attr4.clone(); + + let shared = table.clone(); + let h = thread::spawn(move || { + for _ in 0..5_000 { + let row = TestRow { + id: shared.get_next_pk().into(), + val: 13, + attr1: "Attribute".to_string(), + attr2: row_new_attr_2, + attr3: 123456789, + attr4: row_new_attr_4.clone(), + }; + assert!(shared.insert(row).is_err()); + } + }); + + for _ in 0..5000 { + let attr_1_rows = table.select_by_attr1(row.attr1.clone()).execute().unwrap(); + 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()); + } + + h.join().unwrap(); +}