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 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 f64_try_from_value;
|
||||||
pub mod i32_try_from_value;
|
pub mod i32_try_from_value;
|
||||||
pub mod i64_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.
|
/// them, as well as to persist changes to and load data from a database file.
|
||||||
#[derive(Debug, Default, PartialEq, Clone)]
|
#[derive(Debug, Default, PartialEq, Clone)]
|
||||||
pub struct O {
|
pub struct O {
|
||||||
path: String,
|
pub(crate) path: String,
|
||||||
pub(crate) items: HashMap<String, Entity>,
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -582,103 +246,6 @@ mod tests {
|
|||||||
let retrieved_item: Option<Item> = catalog.get("nonexistent-id").unwrap();
|
let retrieved_item: Option<Item> = catalog.get("nonexistent-id").unwrap();
|
||||||
assert!(retrieved_item.is_none());
|
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()'
|
// ## 'list_by_class()'
|
||||||
#[test]
|
#[test]
|
||||||
fn list_by_class_should_return_all_entities_of_class() {
|
fn list_by_class_should_return_all_entities_of_class() {
|
||||||
@@ -722,83 +289,6 @@ mod tests {
|
|||||||
.collect();
|
.collect();
|
||||||
assert!(results.is_empty());
|
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()'
|
// ## 'delete()'
|
||||||
#[test]
|
#[test]
|
||||||
fn delete_should_mark_entity_as_to_delete() {
|
fn delete_should_mark_entity_as_to_delete() {
|
||||||
@@ -2326,83 +1816,6 @@ mod tests {
|
|||||||
std::fs::remove_file(path).unwrap();
|
std::fs::remove_file(path).unwrap();
|
||||||
}
|
}
|
||||||
#[test]
|
#[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.
|
#[ignore] // This test can be flaky as it depends on thread scheduling.
|
||||||
fn integration_test_concurrency() {
|
fn integration_test_concurrency() {
|
||||||
// Scenario: Concurrency - what happens if two 'Catalog' instances point to the same file?
|
// Scenario: Concurrency - what happens if two 'Catalog' instances point to the same file?
|
||||||
|
|||||||
Reference in New Issue
Block a user