From 56f4990820dbc3cac87b54c558baecccffee9dea Mon Sep 17 00:00:00 2001 From: vincent-herlemont Date: Sun, 18 Aug 2024 12:23:22 +0200 Subject: [PATCH] fix: secondary index range --- src/db_type/key/key.rs | 17 +++- src/db_type/key/key_definition.rs | 2 +- src/metadata/table.rs | 2 - tests/modules.rs | 1 + tests/scan.rs | 146 +++++++++++++++++++++++++++ tests/upgrade/from_0_7_x_to_0_8_x.rs | 92 +++++++++++++++++ tests/upgrade/mod.rs | 1 + 7 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 tests/upgrade/from_0_7_x_to_0_8_x.rs diff --git a/src/db_type/key/key.rs b/src/db_type/key/key.rs index eb738c4..690ebf3 100644 --- a/src/db_type/key/key.rs +++ b/src/db_type/key/key.rs @@ -14,6 +14,11 @@ impl Key { self.0.extend(data.0.iter()); } + pub fn extend_with_delimiter(&mut self, data: &Key) { + self.0.push(0); + self.0.extend(data.0.iter()); + } + pub(crate) fn as_slice(&self) -> &[u8] { self.0.as_slice() } @@ -334,7 +339,11 @@ impl KeyRange { { match (bounds.start_bound(), bounds.end_bound()) { (Bound::Included(start), Bound::Included(end)) => { - KeyRange::RangeInclusive(start.to_key()..=end.to_key()) + let start = start.to_key(); + let mut end = end.to_key(); + // Add 255 to the end key to include the last key + end.extend(&Key::new(vec![255])); + KeyRange::RangeInclusive(start..=end) } (Bound::Included(start), Bound::Excluded(end)) => { KeyRange::Range(start.to_key()..end.to_key()) @@ -343,7 +352,11 @@ impl KeyRange { start: start.to_key(), }), (Bound::Excluded(start), Bound::Included(end)) => { - KeyRange::RangeInclusive(start.to_key()..=end.to_key()) + let start = start.to_key(); + let mut end = end.to_key(); + // Add 255 to the end key to include the last key + end.extend(&Key::new(vec![255])); + KeyRange::RangeInclusive(start..=end) } (Bound::Excluded(start), Bound::Excluded(end)) => { KeyRange::Range(start.to_key()..end.to_key()) diff --git a/src/db_type/key/key_definition.rs b/src/db_type/key/key_definition.rs index 1e37f54..c795d58 100644 --- a/src/db_type/key/key_definition.rs +++ b/src/db_type/key/key_definition.rs @@ -65,6 +65,6 @@ pub struct KeyOptions { pub fn composite_key(secondary_key: &Key, primary_key: &Key) -> Key { let mut secondary_key = secondary_key.clone(); - secondary_key.extend(primary_key); + secondary_key.extend_with_delimiter(primary_key); secondary_key } diff --git a/src/metadata/table.rs b/src/metadata/table.rs index aa5306a..0838ff0 100644 --- a/src/metadata/table.rs +++ b/src/metadata/table.rs @@ -30,7 +30,6 @@ pub fn load_or_create_metadata(database_instance: &DatabaseInstance) -> Result Result().unwrap(); + let db = Builder::new().create_in_memory(&models).unwrap(); + + let rw = db.rw_transaction().unwrap(); + rw.insert(item_1.clone()).unwrap(); + rw.insert(item_2.clone()).unwrap(); + rw.insert(item_3.clone()).unwrap(); + rw.insert(item_4.clone()).unwrap(); + rw.commit().unwrap(); + + let r = db.r_transaction().unwrap(); + let result = r + .scan() + .secondary(ItemScanRangeKey::nr) + .unwrap() + .range(0..10) + .collect::, _>>() + .unwrap() + .iter() + .map(|x| x.nr) + .collect::>(); + assert_eq!(result, vec![1, 2, 2, 3], "range 0..10 for nr"); + + let result = r + .scan() + .secondary(ItemScanRangeKey::nr) + .unwrap() + .range(2..3) + .collect::, _>>() + .unwrap() + .iter() + .map(|x| x.nr) + .collect::>(); + assert_eq!(result, vec![2, 2], "range 2..3 for nr"); + + let result = r + .scan() + .secondary(ItemScanRangeKey::unique_nr) + .unwrap() + .range(1..3) + .collect::, _>>() + .unwrap() + .iter() + .map(|x| x.unique_nr) + .collect::>(); + assert_eq!(result, vec![1, 2], "range 1..3 for unique_nr"); + + let result = r + .scan() + .secondary(ItemScanRangeKey::unique_nr) + .unwrap() + .range(1..=3) + .collect::, _>>() + .unwrap() + .iter() + .map(|x| x.unique_nr) + .collect::>(); + assert_eq!(result, vec![1, 2, 3], "range 1..=3 for unique_nr"); + + let result = r + .scan() + .secondary(ItemScanRangeKey::unique_nr) + .unwrap() + .range(3..=3) + .collect::, _>>() + .unwrap() + .iter() + .map(|x| x.unique_nr) + .collect::>(); + assert_eq!(result, vec![3], "range 3..=3 for unique_nr"); + + let result = r + .scan() + .secondary(ItemScanRangeKey::nr) + .unwrap() + .range(2..=3) + .collect::, _>>() + .unwrap() + .iter() + .map(|x| x.nr) + .collect::>(); + assert_eq!(result, vec![2, 2, 3], "range 2..=3 for nr"); + + let result = r + .scan() + .secondary(ItemScanRangeKey::nr) + .unwrap() + .range(2..=2) + .collect::, _>>() + .unwrap() + .iter() + .map(|x| x.nr) + .collect::>(); + assert_eq!(result, vec![2, 2], "range 2..=2 for nr"); + + let result = r + .scan() + .secondary(ItemScanRangeKey::nr) + .unwrap() + .range(0..=2) + .collect::, _>>() + .unwrap() + .iter() + .map(|x| x.nr) + .collect::>(); + assert_eq!(result, vec![1, 2, 2], "range 0..=2 for nr"); +} diff --git a/tests/upgrade/from_0_7_x_to_0_8_x.rs b/tests/upgrade/from_0_7_x_to_0_8_x.rs new file mode 100644 index 0000000..3bb8564 --- /dev/null +++ b/tests/upgrade/from_0_7_x_to_0_8_x.rs @@ -0,0 +1,92 @@ +use native_db::*; +use native_model::{native_model, Model}; +use redb::ReadableTable; +use serde::{Deserialize, Serialize}; + +const TABLE: redb::TableDefinition = redb::TableDefinition::new("2_1_name"); + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] +#[native_model(id = 1, version = 1)] +#[native_db] +struct Item1 { + #[primary_key] + id: u32, + #[secondary_key(unique)] + name: String, +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] +#[native_model(id = 2, version = 1)] +#[native_db] +struct Item2 { + #[primary_key] + id: u32, + #[secondary_key(optional)] + id2: Option, + #[secondary_key] + name: String, +} + +#[test] +fn upgrade_from_0_7_x_to_0_8_x() { + use std::path::PathBuf; + #[cfg(any(target_os = "android", target_os = "ios"))] + let database_path = { dinghy_test::test_project_path().join("tests/data/db_0_7_1") }; + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let database_path = { + let root_project_path = env!("CARGO_MANIFEST_DIR"); + PathBuf::from(format!("{}/tests/data/db_0_7_1", root_project_path)) + }; + + use shortcut_assert_fs::TmpFs; + let tmp = TmpFs::new().unwrap(); + + // Copy the legacy database to the temporary directory. + let tmp_database_path = tmp.path("db_0_7_1"); + std::fs::copy(&database_path, &tmp_database_path).unwrap(); + + // Check before refresh the number of bytes of the secondary table. + // We check that the delimiter is not included in the secondary table. + { + let db = redb::Database::open(&tmp_database_path).unwrap(); + let rx = db.begin_read().unwrap(); + let table = rx.open_table(TABLE).unwrap(); + let (key, _) = table.first().unwrap().unwrap(); + assert_eq!( + key.value(), + Key::new(vec![105, 116, 101, 109, 50, 95, 48, 0, 0, 0, 0]) + ); + } + + // Refresh the database + let mut models = Models::new(); + models.define::().unwrap(); + models.define::().unwrap(); + let db = Builder::new().open(&models, &tmp_database_path).unwrap(); + if db.upgrading_from_version("<0.8.0").unwrap() { + // Refresh the database + let rw = db.rw_transaction().unwrap(); + // Refresh the Item2 because there is a non-unique secondary key + rw.refresh::().unwrap(); + rw.commit().unwrap(); + } + drop(db); + + // Check after refresh the number of bytes of the secondary table. + // We check that the delimiter is not included in the secondary table. + let db = redb::Database::open(&tmp_database_path).unwrap(); + let rx = db.begin_read().unwrap(); + let table = rx.open_table(TABLE).unwrap(); + let (key, _) = table.first().unwrap().unwrap(); + // The delimiter is a "0" byte included between the secondary key and the primary key + // Ignore fmt + #[rustfmt::skip] + assert_eq!( + key.value(), + // | secondary key | delimiter | primary key | + Key::new(vec![105, 116, 101, 109, 50, 95, 48, 0, 0, 0, 0, 0]) + ); + drop(rx); + drop(db); +} diff --git a/tests/upgrade/mod.rs b/tests/upgrade/mod.rs index 89ae873..2810cb2 100644 --- a/tests/upgrade/mod.rs +++ b/tests/upgrade/mod.rs @@ -1 +1,2 @@ mod from_0_5_x_to_0_6_x; +mod from_0_7_x_to_0_8_x;