From d4ecd1a6ccf2fdb527bbf0dd9ce5503ef117b0a0 Mon Sep 17 00:00:00 2001 From: davidemazzocchi Date: Wed, 22 Oct 2025 06:33:30 +0200 Subject: [PATCH] chore: extract catalog function into their own impl file also get rid of - get_by_class_and_attribute - list_by_class_and_attribute --- 01.workspace/heave/src/imp/catalog_delete.rs | 28 + 01.workspace/heave/src/imp/catalog_get.rs | 29 + 01.workspace/heave/src/imp/catalog_init.rs | 23 + .../heave/src/imp/catalog_insert_many.rs | 26 + .../heave/src/imp/catalog_list_by_class.rs | 23 + .../imp/catalog_list_by_class_and_subclass.rs | 28 + .../heave/src/imp/catalog_load_by_class.rs | 32 + .../heave/src/imp/catalog_load_by_filter.rs | 33 + .../heave/src/imp/catalog_load_by_id.rs | 34 + 01.workspace/heave/src/imp/catalog_new.rs | 23 + 01.workspace/heave/src/imp/catalog_persist.rs | 43 ++ 01.workspace/heave/src/imp/catalog_upsert.rs | 34 + 01.workspace/heave/src/imp/mod.rs | 12 + 01.workspace/heave/src/str/catalog.rs | 589 +----------------- 14 files changed, 369 insertions(+), 588 deletions(-) create mode 100644 01.workspace/heave/src/imp/catalog_delete.rs create mode 100644 01.workspace/heave/src/imp/catalog_get.rs create mode 100644 01.workspace/heave/src/imp/catalog_init.rs create mode 100644 01.workspace/heave/src/imp/catalog_insert_many.rs create mode 100644 01.workspace/heave/src/imp/catalog_list_by_class.rs create mode 100644 01.workspace/heave/src/imp/catalog_list_by_class_and_subclass.rs create mode 100644 01.workspace/heave/src/imp/catalog_load_by_class.rs create mode 100644 01.workspace/heave/src/imp/catalog_load_by_filter.rs create mode 100644 01.workspace/heave/src/imp/catalog_load_by_id.rs create mode 100644 01.workspace/heave/src/imp/catalog_new.rs create mode 100644 01.workspace/heave/src/imp/catalog_persist.rs create mode 100644 01.workspace/heave/src/imp/catalog_upsert.rs diff --git a/01.workspace/heave/src/imp/catalog_delete.rs b/01.workspace/heave/src/imp/catalog_delete.rs new file mode 100644 index 0000000..8b31bc4 --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_delete.rs @@ -0,0 +1,28 @@ +use crate::*; + +impl Catalog { + /// Marks an entity for deletion from the in-memory catalog. + /// + /// This is a purely in-memory operation. The entity will not be removed from the + /// database until `persist()` is called. + /// + /// # Effects + /// + /// - **In-Memory State:** If the entity exists, its state is set to + /// `EntityState::ToDelete`. If it was in a `New` state, it will simply be + /// forgotten upon the next `persist()` call without ever touching the database. + /// - **Database State:** Unchanged. Call `persist()` to apply the deletion. + /// + /// # Arguments + /// + /// * `id` - The ID of the entity to mark for deletion. + pub fn delete(&mut self, id: &str) { + let entity = self.items.get_mut(id); + if let Some(entity) = entity { + entity.state = EntityState::ToDelete; + } + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/catalog_get.rs b/01.workspace/heave/src/imp/catalog_get.rs new file mode 100644 index 0000000..8eaf6cb --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_get.rs @@ -0,0 +1,29 @@ +use crate::*; + +impl Catalog { + /// Retrieves an entity by its ID from the in-memory catalog. + /// + /// This is a purely in-memory operation and does not interact with the database. + /// It will only find entities that have been loaded into or created in the catalog. + /// + /// # Arguments + /// + /// * `id` - The ID of the entity to retrieve. + /// + /// # Returns + /// + /// An `Option` containing the converted entity if found in the in-memory + /// cache, otherwise `None`. + pub fn get(&self, id: &str) -> Result, FailedTo> + where + T: EAV, + { + let entity = self.items.get(id); + entity + .map(|e| T::try_from(e.clone()).map_err(|_| FailedTo::ConvertEntity)) + .transpose() + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/catalog_init.rs b/01.workspace/heave/src/imp/catalog_init.rs new file mode 100644 index 0000000..f7ab991 --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_init.rs @@ -0,0 +1,23 @@ +use crate::*; + +impl Catalog { + /// Initializes the database by creating the file and schema if they don't exist. + /// + /// This method interacts with the filesystem to ensure the database file and its + /// underlying tables are ready. It has no effect on the in-memory state of the + /// catalog. + /// + /// # Effects + /// + /// - **Database State:** Creates the SQLite file and required tables if they are not + /// present. If the file already exists, it does nothing. + /// - **In-Memory State:** This method does not alter the in-memory entity cache. + pub fn init(&self) -> result::Result<(), FailedTo> { + let path = path::Path::new(&self.path); + sqlite::init::db(path).map_err(|_| FailedTo::InitDatabase)?; + Ok(()) + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/catalog_insert_many.rs b/01.workspace/heave/src/imp/catalog_insert_many.rs new file mode 100644 index 0000000..ae0020f --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_insert_many.rs @@ -0,0 +1,26 @@ +use crate::*; + +impl Catalog { + /// Inserts or updates multiple objects in the in-memory catalog. + /// + /// This method calls `upsert()` for each object in the provided vector. Like + /// `upsert()`, this is a purely in-memory operation. + /// + /// # Effects + /// + /// - **In-Memory State:** Adds or updates multiple entities in the cache. + /// - **Database State:** Unchanged. Call `persist()` to save the changes. + /// + /// # Arguments + /// + /// * `objects` - A vector of objects to insert or update. + pub fn insert_many(&mut self, objects: Vec) -> Result<(), FailedTo> { + for object in objects { + self.upsert(object)?; + } + Ok(()) + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/catalog_list_by_class.rs b/01.workspace/heave/src/imp/catalog_list_by_class.rs new file mode 100644 index 0000000..199bc88 --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_list_by_class.rs @@ -0,0 +1,23 @@ +use crate::*; + +impl Catalog { + /// Returns an iterator over all entities of a specific class in the in-memory catalog. + /// + /// This is a purely in-memory operation and does not interact with the database. + /// + /// # Returns + /// + /// An iterator that yields items of type `T` from the in-memory cache. + pub fn list_by_class(&self) -> impl Iterator> + where + T: EAV, + { + self.items + .values() + .filter(move |item| item.class == T::class()) + .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity)) + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/catalog_list_by_class_and_subclass.rs b/01.workspace/heave/src/imp/catalog_list_by_class_and_subclass.rs new file mode 100644 index 0000000..073ad9b --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_list_by_class_and_subclass.rs @@ -0,0 +1,28 @@ +use crate::*; + +impl Catalog { + /// Returns an iterator over all entities of a specific class and subclass + /// in the in-memory catalog. + /// + /// This is a purely in-memory operation and does not interact with the database. + /// + /// # Returns + /// + /// An iterator that yields items of type `T` from the in-memory cache. + pub fn list_by_class_and_subclass( + &self, + subclass: &str, + ) -> impl Iterator> + where + T: EAV, + { + self.items + .values() + .filter(move |item| item.class == T::class()) + .filter(move |item| item.subclass == Some(subclass.to_string())) + .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity)) + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/catalog_load_by_class.rs b/01.workspace/heave/src/imp/catalog_load_by_class.rs new file mode 100644 index 0000000..c45cdb6 --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_load_by_class.rs @@ -0,0 +1,32 @@ +use crate::*; + +impl Catalog { + /// Loads all entities of a specific class from the database into the in-memory catalog. + /// + /// This method fetches all entities matching the given class from the database. + /// If any of the loaded entities have IDs that match entities already in the + /// in-memory catalog, the in-memory versions will be **overwritten**. + /// + /// # Effects + /// + /// - **In-Memory State:** + /// - All found entities are inserted or updated in the in-memory cache with the + /// state `EntityState::Loaded`. + /// - Any existing in-memory entities with matching IDs are replaced. + /// - **Database State:** Unchanged. + pub fn load_by_class(&mut self) -> Result<(), FailedTo> + where + T: EAV, + { + let class = T::class(); + let path = path::Path::new(&self.path); + let entities = sqlite::load::by_class(path, class).map_err(|_| FailedTo::LoadFromDB)?; + for entity in entities { + self.items.insert(entity.id.clone(), entity); + } + Ok(()) + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/catalog_load_by_filter.rs b/01.workspace/heave/src/imp/catalog_load_by_filter.rs new file mode 100644 index 0000000..5be7ed7 --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_load_by_filter.rs @@ -0,0 +1,33 @@ +use crate::*; + +impl Catalog { + /// Loads entities from the database that match a given `Filter`. + /// + /// This method queries the database for all entities that satisfy the conditions + /// specified in the filter. If any of the loaded entities have IDs that match + /// entities already in the in-memory catalog, the in-memory versions will be + /// **overwritten**. + /// + /// **Warning:** The filter is applied at the attribute level. If different entity + /// classes share attribute names, this method may load entities of multiple + /// classes. + /// + /// # Effects + /// + /// - **In-Memory State:** + /// - All found entities are inserted or updated in the in-memory cache with the + /// state `EntityState::Loaded`. + /// - Any existing in-memory entities with matching IDs are replaced. + /// - **Database State:** Unchanged. + pub fn load_by_filter(&mut self, filter: &Filter) -> Result<(), FailedTo> { + let path = path::Path::new(&self.path); + let entities = sqlite::load::by_filter(path, filter).map_err(|_| FailedTo::LoadFromDB)?; + for entity in entities { + self.items.insert(entity.id.clone(), entity); + } + Ok(()) + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/catalog_load_by_id.rs b/01.workspace/heave/src/imp/catalog_load_by_id.rs new file mode 100644 index 0000000..14c9159 --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_load_by_id.rs @@ -0,0 +1,34 @@ +use crate::*; + +impl Catalog { + /// Loads a single entity by its ID from the database into the in-memory catalog. + /// + /// This method fetches the entity from the database and updates the in-memory + /// catalog. If an entity with the same ID already exists in memory, it will be + /// **overwritten** with the version from the database. + /// + /// # Effects + /// + /// - **In-Memory State:** + /// - If an entity is found in the database, it is inserted or updated in the + /// in-memory cache with the state `EntityState::Loaded`. + /// - Any existing in-memory entity with the same ID, regardless of its state + /// (`New`, `Updated`), will be replaced. + /// - If no entity is found in the database, the in-memory cache is not modified. + /// - **Database State:** Unchanged. + /// + /// # Arguments + /// + /// * `id` - The ID of the entity to load. + pub fn load_by_id(&mut self, id: &str) -> Result<(), FailedTo> { + let path = path::Path::new(&self.path); + let entity = sqlite::load::by_id(path, id).map_err(|_| FailedTo::LoadFromDB)?; + if let Some(entity) = entity { + self.items.insert(entity.id.clone(), entity); + } + Ok(()) + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/catalog_new.rs b/01.workspace/heave/src/imp/catalog_new.rs new file mode 100644 index 0000000..e0ca65d --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_new.rs @@ -0,0 +1,23 @@ +use crate::*; + +impl Catalog { + /// Creates a new, empty in-memory `Catalog` for the database at the given path. + /// + /// This method does not create the database file or connect to it. It only + /// initializes an empty catalog in memory. The database file will be accessed + /// when `init()`, `persist()`, or `load_*` methods are called. + /// + /// # Arguments + /// + /// * `path` - The path to the SQLite database file that this catalog will manage. + /// + /// # Returns + /// + /// A new `Catalog` instance with an empty in-memory item cache. + pub fn new(path: &str) -> Self { + Self { + path: String::from(path), + ..Catalog::default() + } + } +} diff --git a/01.workspace/heave/src/imp/catalog_persist.rs b/01.workspace/heave/src/imp/catalog_persist.rs new file mode 100644 index 0000000..1bda57c --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_persist.rs @@ -0,0 +1,43 @@ +use crate::*; + +impl Catalog { + /// Persists all in-memory changes to the database and updates the in-memory state. + /// + /// This method synchronizes the state of the in-memory catalog with the database + /// by writing all pending changes (new, updated, and deleted entities). + /// + /// # Effects + /// + /// - **Database State:** + /// - Entities marked as `EntityState::New` are inserted into the database. + /// - Entities marked as `EntityState::Updated` are updated in the database. + /// - Entities marked as `EntityState::ToDelete` are deleted from the database. + /// + /// - **In-Memory State:** + /// - After a successful database write, entities marked `ToDelete` are permanently + /// removed from the in-memory catalog. + /// - All other entities that were successfully persisted (new or updated) have + /// their state changed to `EntityState::Loaded`. + pub fn persist(&mut self) -> result::Result<(), FailedTo> { + let path = path::Path::new(&self.path); + sqlite::persist::catalog(path, self).map_err(|_| FailedTo::PersistCatalog)?; + // cleaning catalog state after db write + self.items = self + .items + .extract_if(|_, item| item.state != EntityState::ToDelete) + .map(|(k, item)| { + ( + k, + Entity { + state: EntityState::Loaded, + ..item + }, + ) + }) + .collect(); + Ok(()) + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/catalog_upsert.rs b/01.workspace/heave/src/imp/catalog_upsert.rs new file mode 100644 index 0000000..7d57f2c --- /dev/null +++ b/01.workspace/heave/src/imp/catalog_upsert.rs @@ -0,0 +1,34 @@ +use crate::*; + +impl Catalog { + /// Inserts or updates an object in the in-memory catalog. + /// + /// This is a purely in-memory operation. The change will not be written to the + /// database until `persist()` is called. + /// + /// # Effects + /// + /// - **In-Memory State:** + /// - If the entity does not exist in the catalog, it is added with the state + /// `EntityState::New`. + /// - If the entity already exists, it is overwritten and its state is set to + /// `EntityState::Updated`. + /// - **Database State:** Unchanged. Call `persist()` to save the changes. + /// + /// # Arguments + /// + /// * `object` - The object to insert or update, which must implement the `EAV` trait. + pub fn upsert(&mut self, object: impl EAV) -> Result<(), FailedTo> { + let mut entity = object.try_into().map_err(|_| FailedTo::ConvertObject)?; + if self.items.contains_key(&entity.id) { + entity.state = EntityState::Updated; + } else { + entity.state = EntityState::New; + } + self.items.insert(entity.id.clone(), entity); + Ok(()) + } +} + +// #[cfg(test)] +// mod unit_tests { use super::*; } diff --git a/01.workspace/heave/src/imp/mod.rs b/01.workspace/heave/src/imp/mod.rs index a15767e..600b5fd 100644 --- a/01.workspace/heave/src/imp/mod.rs +++ b/01.workspace/heave/src/imp/mod.rs @@ -1,4 +1,16 @@ pub mod bool_try_from_value; +pub mod catalog_delete; +pub mod catalog_get; +pub mod catalog_init; +pub mod catalog_insert_many; +pub mod catalog_list_by_class; +pub mod catalog_list_by_class_and_subclass; +pub mod catalog_load_by_class; +pub mod catalog_load_by_filter; +pub mod catalog_load_by_id; +pub mod catalog_new; +pub mod catalog_persist; +pub mod catalog_upsert; pub mod f64_try_from_value; pub mod i32_try_from_value; pub mod i64_try_from_value; diff --git a/01.workspace/heave/src/str/catalog.rs b/01.workspace/heave/src/str/catalog.rs index 238d231..06a25c5 100644 --- a/01.workspace/heave/src/str/catalog.rs +++ b/01.workspace/heave/src/str/catalog.rs @@ -7,345 +7,9 @@ use std::collections::HashMap; /// them, as well as to persist changes to and load data from a database file. #[derive(Debug, Default, PartialEq, Clone)] pub struct O { - path: String, + pub(crate) path: String, pub(crate) items: HashMap, } -impl Catalog { - /// Creates a new, empty in-memory `Catalog` for the database at the given path. - /// - /// This method does not create the database file or connect to it. It only - /// initializes an empty catalog in memory. The database file will be accessed - /// when `init()`, `persist()`, or `load_*` methods are called. - /// - /// # Arguments - /// - /// * `path` - The path to the SQLite database file that this catalog will manage. - /// - /// # Returns - /// - /// A new `Catalog` instance with an empty in-memory item cache. - pub fn new(path: &str) -> Self { - Self { - path: String::from(path), - ..O::default() - } - } - /// Initializes the database by creating the file and schema if they don't exist. - /// - /// This method interacts with the filesystem to ensure the database file and its - /// underlying tables are ready. It has no effect on the in-memory state of the - /// catalog. - /// - /// # Effects - /// - /// - **Database State:** Creates the SQLite file and required tables if they are not - /// present. If the file already exists, it does nothing. - /// - **In-Memory State:** This method does not alter the in-memory entity cache. - pub fn init(&self) -> result::Result<(), FailedTo> { - let path = path::Path::new(&self.path); - sqlite::init::db(path).map_err(|_| FailedTo::InitDatabase)?; - Ok(()) - } - /// Inserts or updates an object in the in-memory catalog. - /// - /// This is a purely in-memory operation. The change will not be written to the - /// database until `persist()` is called. - /// - /// # Effects - /// - /// - **In-Memory State:** - /// - If the entity does not exist in the catalog, it is added with the state - /// `EntityState::New`. - /// - If the entity already exists, it is overwritten and its state is set to - /// `EntityState::Updated`. - /// - **Database State:** Unchanged. Call `persist()` to save the changes. - /// - /// # Arguments - /// - /// * `object` - The object to insert or update, which must implement the `EAV` trait. - pub fn upsert(&mut self, object: impl EAV) -> Result<(), FailedTo> { - let mut entity = object.try_into().map_err(|_| FailedTo::ConvertObject)?; - if self.items.contains_key(&entity.id) { - entity.state = EntityState::Updated; - } else { - entity.state = EntityState::New; - } - self.items.insert(entity.id.clone(), entity); - Ok(()) - } - /// Inserts or updates multiple objects in the in-memory catalog. - /// - /// This method calls `upsert()` for each object in the provided vector. Like - /// `upsert()`, this is a purely in-memory operation. - /// - /// # Effects - /// - /// - **In-Memory State:** Adds or updates multiple entities in the cache. - /// - **Database State:** Unchanged. Call `persist()` to save the changes. - /// - /// # Arguments - /// - /// * `objects` - A vector of objects to insert or update. - pub fn insert_many(&mut self, objects: Vec) -> Result<(), FailedTo> { - for object in objects { - self.upsert(object)?; - } - Ok(()) - } - /// Retrieves an entity by its ID from the in-memory catalog. - /// - /// This is a purely in-memory operation and does not interact with the database. - /// It will only find entities that have been loaded into or created in the catalog. - /// - /// # Arguments - /// - /// * `id` - The ID of the entity to retrieve. - /// - /// # Returns - /// - /// An `Option` containing the converted entity if found in the in-memory - /// cache, otherwise `None`. - pub fn get(&self, id: &str) -> Result, FailedTo> - where - T: EAV, - { - let entity = self.items.get(id); - entity - .map(|e| T::try_from(e.clone()).map_err(|_| FailedTo::ConvertEntity)) - .transpose() - } - /// Retrieves the first entity from the in-memory catalog that matches a given - /// class, attribute, and value. - /// - /// This is a purely in-memory operation and does not interact with the database. - /// - /// # Arguments - /// - /// * `attribute` - The attribute of the entity to match. - /// * `value` - The value of the attribute to match. - /// - /// # Returns - /// - /// An `Option` containing the first matching converted entity if found, - /// otherwise `None`. - pub fn get_by_class_and_attribute( - &self, - attribute: &str, - value: impl Into + Clone, - ) -> Result, FailedTo> - where - T: EAV, - { - let mut items = self - .items - .values() - .filter(|item| item.class == T::class()) - .filter(|item| item.value_of(attribute) == Some(&value.clone().into())) - .take(1) - .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity)); - items.next().transpose() - } - /// Returns an iterator over all entities of a specific class in the in-memory catalog. - /// - /// This is a purely in-memory operation and does not interact with the database. - /// - /// # Returns - /// - /// An iterator that yields items of type `T` from the in-memory cache. - pub fn list_by_class(&self) -> impl Iterator> - where - T: EAV, - { - self.items - .values() - .filter(move |item| item.class == T::class()) - .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity)) - } - /// Returns an iterator over all entities of a specific class and subclass - /// in the in-memory catalog. - /// - /// This is a purely in-memory operation and does not interact with the database. - /// - /// # Returns - /// - /// An iterator that yields items of type `T` from the in-memory cache. - pub fn list_by_class_and_subclass( - &self, - subclass: &str, - ) -> impl Iterator> - where - T: EAV, - { - self.items - .values() - .filter(move |item| item.class == T::class()) - .filter(move |item| item.subclass == Some(subclass.to_string())) - .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity)) - } - /// Returns an iterator over entities in the in-memory catalog that match a given - /// class, attribute, and value. - /// - /// This is a purely in-memory operation and does not interact with the database. - /// - /// # Arguments - /// - /// * `attribute` - The attribute of the entities to match. - /// * `value` - The value of the attribute to match. - /// - /// # Returns - /// - /// An iterator that yields matching items of type `T` from the in-memory cache. - pub fn list_by_class_and_attribute( - &self, - attribute: &str, - value: impl Into + Clone, - ) -> impl Iterator> - where - T: EAV, - { - let value: Value = value.into(); - self.items - .values() - .filter(move |item| item.class == T::class()) - .filter(move |item| item.value_of(attribute) == Some(&value)) - .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity)) - } - /// Marks an entity for deletion from the in-memory catalog. - /// - /// This is a purely in-memory operation. The entity will not be removed from the - /// database until `persist()` is called. - /// - /// # Effects - /// - /// - **In-Memory State:** If the entity exists, its state is set to - /// `EntityState::ToDelete`. If it was in a `New` state, it will simply be - /// forgotten upon the next `persist()` call without ever touching the database. - /// - **Database State:** Unchanged. Call `persist()` to apply the deletion. - /// - /// # Arguments - /// - /// * `id` - The ID of the entity to mark for deletion. - pub fn delete(&mut self, id: &str) { - let entity = self.items.get_mut(id); - if let Some(entity) = entity { - entity.state = EntityState::ToDelete; - } - } - /// Persists all in-memory changes to the database and updates the in-memory state. - /// - /// This method synchronizes the state of the in-memory catalog with the database - /// by writing all pending changes (new, updated, and deleted entities). - /// - /// # Effects - /// - /// - **Database State:** - /// - Entities marked as `EntityState::New` are inserted into the database. - /// - Entities marked as `EntityState::Updated` are updated in the database. - /// - Entities marked as `EntityState::ToDelete` are deleted from the database. - /// - /// - **In-Memory State:** - /// - After a successful database write, entities marked `ToDelete` are permanently - /// removed from the in-memory catalog. - /// - All other entities that were successfully persisted (new or updated) have - /// their state changed to `EntityState::Loaded`. - pub fn persist(&mut self) -> result::Result<(), FailedTo> { - let path = path::Path::new(&self.path); - sqlite::persist::catalog(path, self).map_err(|_| FailedTo::PersistCatalog)?; - // cleaning catalog state after db write - self.items = self - .items - .extract_if(|_, item| item.state != EntityState::ToDelete) - .map(|(k, item)| { - ( - k, - Entity { - state: EntityState::Loaded, - ..item - }, - ) - }) - .collect(); - Ok(()) - } - /// Loads a single entity by its ID from the database into the in-memory catalog. - /// - /// This method fetches the entity from the database and updates the in-memory - /// catalog. If an entity with the same ID already exists in memory, it will be - /// **overwritten** with the version from the database. - /// - /// # Effects - /// - /// - **In-Memory State:** - /// - If an entity is found in the database, it is inserted or updated in the - /// in-memory cache with the state `EntityState::Loaded`. - /// - Any existing in-memory entity with the same ID, regardless of its state - /// (`New`, `Updated`), will be replaced. - /// - If no entity is found in the database, the in-memory cache is not modified. - /// - **Database State:** Unchanged. - /// - /// # Arguments - /// - /// * `id` - The ID of the entity to load. - pub fn load_by_id(&mut self, id: &str) -> Result<(), FailedTo> { - let path = path::Path::new(&self.path); - let entity = sqlite::load::by_id(path, id).map_err(|_| FailedTo::LoadFromDB)?; - if let Some(entity) = entity { - self.items.insert(entity.id.clone(), entity); - } - Ok(()) - } - /// Loads all entities of a specific class from the database into the in-memory catalog. - /// - /// This method fetches all entities matching the given class from the database. - /// If any of the loaded entities have IDs that match entities already in the - /// in-memory catalog, the in-memory versions will be **overwritten**. - /// - /// # Effects - /// - /// - **In-Memory State:** - /// - All found entities are inserted or updated in the in-memory cache with the - /// state `EntityState::Loaded`. - /// - Any existing in-memory entities with matching IDs are replaced. - /// - **Database State:** Unchanged. - pub fn load_by_class(&mut self) -> Result<(), FailedTo> - where - T: EAV, - { - let class = T::class(); - let path = path::Path::new(&self.path); - let entities = sqlite::load::by_class(path, class).map_err(|_| FailedTo::LoadFromDB)?; - for entity in entities { - self.items.insert(entity.id.clone(), entity); - } - Ok(()) - } - /// Loads entities from the database that match a given `Filter`. - /// - /// This method queries the database for all entities that satisfy the conditions - /// specified in the filter. If any of the loaded entities have IDs that match - /// entities already in the in-memory catalog, the in-memory versions will be - /// **overwritten**. - /// - /// **Warning:** The filter is applied at the attribute level. If different entity - /// classes share attribute names, this method may load entities of multiple - /// classes. - /// - /// # Effects - /// - /// - **In-Memory State:** - /// - All found entities are inserted or updated in the in-memory cache with the - /// state `EntityState::Loaded`. - /// - Any existing in-memory entities with matching IDs are replaced. - /// - **Database State:** Unchanged. - pub fn load_by_filter(&mut self, filter: &Filter) -> Result<(), FailedTo> { - let path = path::Path::new(&self.path); - let entities = sqlite::load::by_filter(path, filter).map_err(|_| FailedTo::LoadFromDB)?; - for entity in entities { - self.items.insert(entity.id.clone(), entity); - } - Ok(()) - } -} #[cfg(test)] mod tests { @@ -582,103 +246,6 @@ mod tests { let retrieved_item: Option = catalog.get("nonexistent-id").unwrap(); assert!(retrieved_item.is_none()); } - // ## 'get_by_class_and_attribute()' - #[test] - fn get_by_class_and_attribute_should_retrieve_correct_entity() { - // Should retrieve the first entity matching the class, attribute, and value. - let mut catalog = Catalog::new("dummy.db"); - let item1 = Item { - id: "item-1".to_string(), - name: "Test Item".to_string(), - price: 100, - sell_trend: 0, - in_stock: true, - ..Item::default() - }; - let item2 = Item { - id: "item-2".to_string(), - subclass: Some("subitem".to_string()), - name: "Unique Item".to_string(), - price: 200, - sell_trend: 0, - in_stock: false, - ..Item::default() - }; - let _ = catalog.upsert(item1.clone()); - let _ = catalog.upsert(item2.clone()); - let retrieved_item: Option = catalog - .get_by_class_and_attribute("name", "Unique Item") - .unwrap(); - assert_eq!(retrieved_item, Some(item2)); - } - #[test] - fn get_by_class_and_attribute_should_work_with_different_value_types() { - // Should work with different value types (String, u64, bool). - let mut catalog = Catalog::new("dummy.db"); - let item1 = Item { - id: "item-1".to_string(), - subclass: Some("subitem".to_string()), - name: "Item One".to_string(), - price: 100, - sell_trend: 10, - in_stock: true, - ..Item::default() - }; - let item2 = Item { - id: "item-2".to_string(), - subclass: Some("subitem".to_string()), - name: "Item Two".to_string(), - price: 250, - sell_trend: 20, - in_stock: false, - ..Item::default() - }; - let _ = catalog.upsert(item1.clone()); - let _ = catalog.upsert(item2.clone()); - // Test with &str for String attribute - let retrieved_by_name: Option = catalog - .get_by_class_and_attribute("name", "Item One") - .unwrap(); - assert_eq!(retrieved_by_name, Some(item1.clone())); - // Test with u64 for price attribute - let retrieved_by_price: Option = - catalog.get_by_class_and_attribute("price", 250u64).unwrap(); - assert_eq!(retrieved_by_price, Some(item2.clone())); - // Test with bool for in_stock attribute - let retrieved_by_stock: Option = catalog - .get_by_class_and_attribute("in_stock", true) - .unwrap(); - assert_eq!(retrieved_by_stock, Some(item1.clone())); - // Test with i64 for sell_trend attribute - let retrieved_by_sell_trend: Option = catalog - .get_by_class_and_attribute("sell_trend", 10i64) - .unwrap(); - assert_eq!(retrieved_by_sell_trend, Some(item1.clone())); - } - #[test] - fn get_by_class_and_attribute_should_return_none_if_no_match() { - // Should return 'None' if no entity matches the criteria. - let mut catalog = Catalog::new("dummy.db"); - let item = Item { - id: "item-1".to_string(), - name: "Test Item".to_string(), - price: 100, - sell_trend: 0, - in_stock: true, - ..Item::default() - }; - let _ = catalog.upsert(item.clone()); - // Test with a value that doesn't exist - let retrieved_item: Option = catalog - .get_by_class_and_attribute("name", "Non-existent Name") - .unwrap(); - assert!(retrieved_item.is_none()); - // Test with an attribute that doesn't exist - let retrieved_item_2: Option = catalog - .get_by_class_and_attribute("non-existent-attribute", "Test Item") - .unwrap(); - assert!(retrieved_item_2.is_none()); - } // ## 'list_by_class()' #[test] fn list_by_class_should_return_all_entities_of_class() { @@ -722,83 +289,6 @@ mod tests { .collect(); assert!(results.is_empty()); } - // ## 'list_by_class_and_attribute()' - #[test] - fn list_by_class_and_attribute_should_return_all_matching_entities() { - // Should return an iterator with all entities matching the class, attribute, and value. - let mut catalog = Catalog::new("dummy.db"); - let item1 = Item { - id: "item-1".to_string(), - subclass: Some("subitem".to_string()), - name: "Item One".to_string(), - price: 100, - sell_trend: 0, - in_stock: true, - ..Item::default() - }; - let item2 = Item { - id: "item-2".to_string(), - subclass: Some("subitem".to_string()), - name: "Item Two".to_string(), - price: 200, - sell_trend: 0, - in_stock: false, - ..Item::default() - }; - let item3 = Item { - id: "item-3".to_string(), - subclass: Some("subitem".to_string()), - name: "Item Three".to_string(), - price: 300, - sell_trend: 0, - in_stock: true, - ..Item::default() - }; - let _ = catalog.upsert(item1.clone()); - let _ = catalog.upsert(item2.clone()); - let _ = catalog.upsert(item3.clone()); - let results: Vec = catalog - .list_by_class_and_attribute("in_stock", true) - .map(|item| item.unwrap()) - .collect(); - assert_eq!(results.len(), 2); - assert!(results.contains(&item1)); - assert!(results.contains(&item3)); - assert!(!results.contains(&item2)); - } - #[test] - fn list_by_class_and_attribute_should_return_empty_iterator_if_no_match() { - // Should return an empty iterator if no entities match. - let mut catalog = Catalog::new("dummy.db"); - let item = Item { - id: "item-1".to_string(), - name: "Test Item".to_string(), - price: 100, - sell_trend: 0, - in_stock: true, - ..Item::default() - }; - let _ = catalog.upsert(item); - // Search for a value that doesn't exist - let results: Vec = catalog - .list_by_class_and_attribute("in_stock", false) - .map(|item| item.unwrap()) - .collect(); - assert!(results.is_empty()); - // Search for an attribute that doesn't exist - let results_2: Vec = catalog - .list_by_class_and_attribute("non-existent-attribute", true) - .map(|item| item.unwrap()) - .collect(); - assert!(results_2.is_empty()); - // Search in a completely empty catalog - let empty_catalog = Catalog::new("dummy.db"); - let results_3: Vec = empty_catalog - .list_by_class_and_attribute("any_attribute", "any_value") - .map(|item| item.unwrap()) - .collect(); - assert!(results_3.is_empty()); - } // ## 'delete()' #[test] fn delete_should_mark_entity_as_to_delete() { @@ -2326,83 +1816,6 @@ mod tests { std::fs::remove_file(path).unwrap(); } #[test] - fn integration_test_insert_get_by_class_and_attribute() { - // Scenario: 'insert' -> 'get_by_class_and_attribute' should return the correct item. - // This test focuses on in-memory functionality. - let mut catalog = Catalog::new("dummy.db"); - let item1 = Item { - id: "item-1".to_string(), - name: "First Item".to_string(), - price: 100, - sell_trend: 0, - in_stock: true, - ..Item::default() - }; - let item2 = Item { - id: "item-2".to_string(), - subclass: Some("subitem".to_string()), - name: "Second Item".to_string(), - price: 200, - sell_trend: 0, - in_stock: false, - ..Item::default() - }; - let _ = catalog.upsert(item1.clone()); - let _ = catalog.upsert(item2.clone()); - // Retrieve by a unique attribute - let retrieved_item: Option = catalog - .get_by_class_and_attribute("name", "Second Item") - .unwrap(); - // Verify the correct item was retrieved - assert_eq!(retrieved_item, Some(item2)); - } - #[test] - fn integration_test_insert_many_list_by_class_and_attribute() { - // Scenario: 'insert_many' -> 'list_by_class_and_attribute' should return all matching items. - // This test focuses on in-memory functionality. - let mut catalog = Catalog::new("dummy.db"); - let item1 = Item { - id: "item-1".to_string(), - subclass: Some("subitem".to_string()), - name: "Item One".to_string(), - price: 100, - sell_trend: 0, - in_stock: true, - ..Item::default() - }; - let item2 = Item { - id: "item-2".to_string(), - subclass: Some("subitem".to_string()), - name: "Item Two".to_string(), - price: 200, - sell_trend: 0, - in_stock: false, - ..Item::default() - }; - let item3 = Item { - id: "item-3".to_string(), - subclass: Some("subitem".to_string()), - name: "Item Three".to_string(), - price: 150, - sell_trend: 0, - in_stock: true, - ..Item::default() - }; - let _ = catalog.insert_many(vec![item1.clone(), item2.clone(), item3.clone()]); - // List all items that are in stock - let mut results: Vec = catalog - .list_by_class_and_attribute("in_stock", true) - .map(|item| item.unwrap()) - .collect(); - // Sort for deterministic comparison - results.sort_by(|a, b| a.id.cmp(&b.id)); - let mut expected = vec![item1, item3]; - expected.sort_by(|a, b| a.id.cmp(&b.id)); - // Verify the correct items were retrieved - assert_eq!(results.len(), 2); - assert_eq!(results, expected); - } - #[test] #[ignore] // This test can be flaky as it depends on thread scheduling. fn integration_test_concurrency() { // Scenario: Concurrency - what happens if two 'Catalog' instances point to the same file?