diff --git a/codegen/src/generators/in_memory/table/index_fns.rs b/codegen/src/generators/in_memory/table/index_fns.rs index 0545d63..0a73cce 100644 --- a/codegen/src/generators/in_memory/table/index_fns.rs +++ b/codegen/src/generators/in_memory/table/index_fns.rs @@ -63,7 +63,9 @@ impl InMemoryGenerator { let type_ = columns_map.get(i).ok_or(syn::Error::new(i.span(), "Row not found"))?; let fn_name = Ident::new(format!("select_by_{i}").as_str(), Span::mixed_site()); let field_ident = &idx.name; - let by = if is_float(type_.to_string().as_str()) { + let row_field_ident = &idx.field; + let is_float = is_float(type_.to_string().as_str()); + let by = if is_float { quote! { &OrderedFloat(by) } @@ -72,11 +74,21 @@ impl InMemoryGenerator { &by } }; + let predicate_matches = if is_float { + quote! { + OrderedFloat(row.#row_field_ident).eq(&OrderedFloat(by)) + } + } else { + quote! { + row.#row_field_ident.eq(&by) + } + }; Ok(quote! { 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() + let row = self.0.data.select_non_ghosted(link).ok()?; + #predicate_matches.then_some(row) } }) } diff --git a/codegen/src/generators/persist/table/index_fns.rs b/codegen/src/generators/persist/table/index_fns.rs index d682a6a..eb1ac14 100644 --- a/codegen/src/generators/persist/table/index_fns.rs +++ b/codegen/src/generators/persist/table/index_fns.rs @@ -63,7 +63,9 @@ impl PersistGenerator { let type_ = columns_map.get(i).ok_or(syn::Error::new(i.span(), "Row not found"))?; let fn_name = Ident::new(format!("select_by_{i}").as_str(), Span::mixed_site()); let field_ident = &idx.name; - let by = if is_float(type_.to_string().as_str()) { + let row_field_ident = &idx.field; + let is_float = is_float(type_.to_string().as_str()); + let by = if is_float { quote! { &OrderedFloat(by) } @@ -72,11 +74,21 @@ impl PersistGenerator { &by } }; + let predicate_matches = if is_float { + quote! { + OrderedFloat(row.#row_field_ident).eq(&OrderedFloat(by)) + } + } else { + quote! { + row.#row_field_ident.eq(&by) + } + }; Ok(quote! { 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() + let row = self.0.data.select_non_ghosted(link).ok()?; + #predicate_matches.then_some(row) } }) } diff --git a/codegen/src/generators/read_only/table/index_fns.rs b/codegen/src/generators/read_only/table/index_fns.rs index 819490d..f626ad7 100644 --- a/codegen/src/generators/read_only/table/index_fns.rs +++ b/codegen/src/generators/read_only/table/index_fns.rs @@ -63,7 +63,9 @@ impl ReadOnlyGenerator { let type_ = columns_map.get(i).ok_or(syn::Error::new(i.span(), "Row not found"))?; let fn_name = Ident::new(format!("select_by_{i}").as_str(), Span::mixed_site()); let field_ident = &idx.name; - let by = if is_float(type_.to_string().as_str()) { + let row_field_ident = &idx.field; + let is_float = is_float(type_.to_string().as_str()); + let by = if is_float { quote! { &OrderedFloat(by) } @@ -72,11 +74,21 @@ impl ReadOnlyGenerator { &by } }; + let predicate_matches = if is_float { + quote! { + OrderedFloat(row.#row_field_ident).eq(&OrderedFloat(by)) + } + } else { + quote! { + row.#row_field_ident.eq(&by) + } + }; Ok(quote! { 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() + let row = self.0.data.select_non_ghosted(link).ok()?; + #predicate_matches.then_some(row) } }) } diff --git a/tests/worktable/float.rs b/tests/worktable/float.rs index 02f751c..29747ee 100644 --- a/tests/worktable/float.rs +++ b/tests/worktable/float.rs @@ -16,6 +16,44 @@ worktable! ( } ); +worktable! ( + name: TestUniqueFloat, + columns: { + id: u64 primary_key autoincrement, + value: f64, + }, + indexes: { + value_idx: value unique, + } +); + +#[test] +fn unique_float_point_read_revalidates_the_returned_row() { + let table = TestUniqueFloatWorkTable::default(); + let first = TestUniqueFloatRow { + id: table.get_next_pk().into(), + value: 1.5, + }; + let second = TestUniqueFloatRow { + id: table.get_next_pk().into(), + value: 2.5, + }; + table.insert(first.clone()).unwrap(); + table.insert(second.clone()).unwrap(); + + let second_link = table + .0 + .primary_index + .pk_map + .get(&TestUniqueFloatPrimaryKey(second.id)) + .map(|entry| entry.get().value.0) + .unwrap(); + TableIndex::insert(&table.0.indexes.value_idx, OrderedFloat(first.value), second_link); + + assert!(table.select_by_value(first.value).is_none()); + assert_eq!(table.select_by_value(second.value), Some(second)); +} + #[test] fn select_all_range_float_test() { let table = TestFloatWorkTable::default(); diff --git a/tests/worktable/index/insert.rs b/tests/worktable/index/insert.rs index ec44733..910e106 100644 --- a/tests/worktable/index/insert.rs +++ b/tests/worktable/index/insert.rs @@ -38,6 +38,44 @@ async fn insert() { assert!(table.select(2).is_none()) } +#[test] +fn unique_point_read_revalidates_the_returned_row() { + let table = TestWorkTable::default(); + let first = TestRow { + id: table.get_next_pk().into(), + val: 13, + attr1: "first".to_string(), + attr2: -128, + attr3: 1, + attr4: "first-unique".to_string(), + }; + let second = TestRow { + id: table.get_next_pk().into(), + val: 14, + attr1: "second".to_string(), + attr2: 128, + attr3: 2, + attr4: "second-unique".to_string(), + }; + table.insert(first.clone()).unwrap(); + table.insert(second.clone()).unwrap(); + + let second_link = table + .0 + .primary_index + .pk_map + .get(&TestPrimaryKey(second.id)) + .map(|entry| entry.get().value.0) + .unwrap(); + + // Model the transient state where a unique-index entry still names a row + // whose indexed field has already changed. + TableIndex::insert(&table.0.indexes.attr2_idx, first.attr2, second_link); + + assert!(table.select_by_attr2(first.attr2).is_none()); + assert_eq!(table.select_by_attr2(second.attr2), Some(second)); +} + #[tokio::test] async fn insert_when_pk_exists() { let table = TestWorkTable::default();