chore: extract catalog function into their own impl file
also get rid of - get_by_class_and_attribute - list_by_class_and_attribute
This commit is contained in:
28
01.workspace/heave/src/imp/catalog_delete.rs
Normal file
28
01.workspace/heave/src/imp/catalog_delete.rs
Normal file
@@ -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::*; }
|
||||
29
01.workspace/heave/src/imp/catalog_get.rs
Normal file
29
01.workspace/heave/src/imp/catalog_get.rs
Normal file
@@ -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<T>` containing the converted entity if found in the in-memory
|
||||
/// cache, otherwise `None`.
|
||||
pub fn get<T>(&self, id: &str) -> Result<Option<T>, 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::*; }
|
||||
23
01.workspace/heave/src/imp/catalog_init.rs
Normal file
23
01.workspace/heave/src/imp/catalog_init.rs
Normal file
@@ -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::*; }
|
||||
26
01.workspace/heave/src/imp/catalog_insert_many.rs
Normal file
26
01.workspace/heave/src/imp/catalog_insert_many.rs
Normal file
@@ -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<impl EAV>) -> Result<(), FailedTo> {
|
||||
for object in objects {
|
||||
self.upsert(object)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod unit_tests { use super::*; }
|
||||
23
01.workspace/heave/src/imp/catalog_list_by_class.rs
Normal file
23
01.workspace/heave/src/imp/catalog_list_by_class.rs
Normal file
@@ -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<T>(&self) -> impl Iterator<Item = Result<T, FailedTo>>
|
||||
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::*; }
|
||||
@@ -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<T>(
|
||||
&self,
|
||||
subclass: &str,
|
||||
) -> impl Iterator<Item = Result<T, FailedTo>>
|
||||
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::*; }
|
||||
32
01.workspace/heave/src/imp/catalog_load_by_class.rs
Normal file
32
01.workspace/heave/src/imp/catalog_load_by_class.rs
Normal file
@@ -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<T>(&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::*; }
|
||||
33
01.workspace/heave/src/imp/catalog_load_by_filter.rs
Normal file
33
01.workspace/heave/src/imp/catalog_load_by_filter.rs
Normal file
@@ -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::*; }
|
||||
34
01.workspace/heave/src/imp/catalog_load_by_id.rs
Normal file
34
01.workspace/heave/src/imp/catalog_load_by_id.rs
Normal file
@@ -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::*; }
|
||||
23
01.workspace/heave/src/imp/catalog_new.rs
Normal file
23
01.workspace/heave/src/imp/catalog_new.rs
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
43
01.workspace/heave/src/imp/catalog_persist.rs
Normal file
43
01.workspace/heave/src/imp/catalog_persist.rs
Normal file
@@ -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::*; }
|
||||
34
01.workspace/heave/src/imp/catalog_upsert.rs
Normal file
34
01.workspace/heave/src/imp/catalog_upsert.rs
Normal file
@@ -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::*; }
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String, Entity>,
|
||||
}
|
||||
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<impl EAV>) -> 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<T>` containing the converted entity if found in the in-memory
|
||||
/// cache, otherwise `None`.
|
||||
pub fn get<T>(&self, id: &str) -> Result<Option<T>, 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<T>` containing the first matching converted entity if found,
|
||||
/// otherwise `None`.
|
||||
pub fn get_by_class_and_attribute<T>(
|
||||
&self,
|
||||
attribute: &str,
|
||||
value: impl Into<Value> + Clone,
|
||||
) -> Result<Option<T>, 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<T>(&self) -> impl Iterator<Item = Result<T, FailedTo>>
|
||||
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<T>(
|
||||
&self,
|
||||
subclass: &str,
|
||||
) -> impl Iterator<Item = Result<T, FailedTo>>
|
||||
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<T>(
|
||||
&self,
|
||||
attribute: &str,
|
||||
value: impl Into<Value> + Clone,
|
||||
) -> impl Iterator<Item = Result<T, FailedTo>>
|
||||
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<T>(&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<Item> = 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<Item> = 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<Item> = 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<Item> =
|
||||
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<Item> = 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<Item> = 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<Item> = 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<Item> = 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<Item> = 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<Item> = 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<Item> = 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<Item> = 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<Item> = 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<Item> = 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?
|
||||
|
||||
Reference in New Issue
Block a user