feat: add class and subclass to filter struct

This commit is contained in:
2025-10-20 18:09:27 +02:00
parent 276d6bd0bb
commit 314c9ce323
5 changed files with 246 additions and 77 deletions

View File

@@ -50,5 +50,11 @@ pub fn run<'a>(filter: &'a Filter) -> Result<Vec<Box<dyn ToSql + 'a>>, FailedTo>
_ => return Err(FailedTo::ComposeFilter), _ => return Err(FailedTo::ComposeFilter),
} }
} }
if let Some(value) = filter.class() {
params.push(Box::new(value));
}
if let Some(value) = filter.subclass() {
params.push(Box::new(value));
}
Ok(params) Ok(params)
} }

View File

@@ -7,6 +7,7 @@ const INNER_JOIN_FRAGMENT: &str = r#"
AND attribute_{index}.id = '{attribute_id}' AND attribute_{index}.id = '{attribute_id}'
AND attribute_{index}.{field} {op} ?{index} AND attribute_{index}.{field} {op} ?{index}
"#; "#;
const WHERE: &str = r#" WHERE 1=1"#;
fn compose_fragment(name: &str, field: &str, op: &str, index: usize) -> String { fn compose_fragment(name: &str, field: &str, op: &str, index: usize) -> String {
INNER_JOIN_FRAGMENT INNER_JOIN_FRAGMENT
@@ -16,76 +17,98 @@ fn compose_fragment(name: &str, field: &str, op: &str, index: usize) -> String {
.replace("{index}", &index.to_string()) .replace("{index}", &index.to_string())
} }
fn from_condition(
i: usize,
name: &str,
comparison: &Comparison,
condition: &Condition,
) -> Result<String, FailedTo> {
let fragment = match (comparison, condition) {
// BOOL
(Comparison::Equal, Condition::Bool(_)) => compose_fragment(name, "value_bool", "=", i + 1),
(_, Condition::Bool(_)) => return Err(FailedTo::ComposeFilter),
// SIGNED INT
(Comparison::Equal, Condition::SignedInt(_)) => {
compose_fragment(name, "value_int", "=", i + 1)
}
(Comparison::Greater, Condition::SignedInt(_)) => {
compose_fragment(name, "value_int", ">", i + 1)
}
(Comparison::Lesser, Condition::SignedInt(_)) => {
compose_fragment(name, "value_int", "<", i + 1)
}
(Comparison::GreaterOrEqual, Condition::SignedInt(_)) => {
compose_fragment(name, "value_int", ">=", i + 1)
}
(Comparison::LesserOrEqual, Condition::SignedInt(_)) => {
compose_fragment(name, "value_int", "<=", i + 1)
}
(_, Condition::SignedInt(_)) => return Err(FailedTo::ComposeFilter),
// UNSIGNED INT
(Comparison::Equal, Condition::UnsignedInt(_)) => {
compose_fragment(name, "value_uint", "=", i + 1)
}
(Comparison::Greater, Condition::UnsignedInt(_)) => {
compose_fragment(name, "value_uint", ">", i + 1)
}
(Comparison::Lesser, Condition::UnsignedInt(_)) => {
compose_fragment(name, "value_uint", "<", i + 1)
}
(Comparison::GreaterOrEqual, Condition::UnsignedInt(_)) => {
compose_fragment(name, "value_uint", ">=", i + 1)
}
(Comparison::LesserOrEqual, Condition::UnsignedInt(_)) => {
compose_fragment(name, "value_uint", "<=", i + 1)
}
(_, Condition::UnsignedInt(_)) => return Err(FailedTo::ComposeFilter),
// REAL
(Comparison::Equal, Condition::Real(_)) => compose_fragment(name, "value_real", "=", i + 1),
(Comparison::Greater, Condition::Real(_)) => {
compose_fragment(name, "value_real", ">", i + 1)
}
(Comparison::Lesser, Condition::Real(_)) => {
compose_fragment(name, "value_real", "<", i + 1)
}
(Comparison::GreaterOrEqual, Condition::Real(_)) => {
compose_fragment(name, "value_real", ">=", i + 1)
}
(Comparison::LesserOrEqual, Condition::Real(_)) => {
compose_fragment(name, "value_real", "<=", i + 1)
}
(_, Condition::Real(_)) => return Err(FailedTo::ComposeFilter),
// TEXT
(Comparison::IsExactly, Condition::Text(_)) => {
compose_fragment(name, "value_text", "LIKE", i + 1)
}
(
Comparison::StartsWith | Comparison::EndsWith | Comparison::Contains,
Condition::Text(_),
) => compose_fragment(name, "value_text", "LIKE", i + 1),
(_, Condition::Text(_)) => return Err(FailedTo::ComposeFilter),
};
Ok(fragment)
}
pub fn run(filter: &Filter) -> Result<String, FailedTo> { pub fn run(filter: &Filter) -> Result<String, FailedTo> {
// base statement
let mut statement = String::from(BASE_SELECT); let mut statement = String::from(BASE_SELECT);
let mut idx = 0;
// for each condition add an inner join fragment
for (i, (name, comparison, condition)) in filter.conditions().enumerate() { for (i, (name, comparison, condition)) in filter.conditions().enumerate() {
let fragment = match (comparison, condition) { idx = i;
// BOOL let fragment = from_condition(i, name, comparison, condition)?;
(Comparison::Equal, Condition::Bool(_)) => { statement.push_str(&fragment);
compose_fragment(name, "value_bool", "=", i + 1) }
} // add a neutral where condition
(_, Condition::Bool(_)) => return Err(FailedTo::ComposeFilter), statement.push_str(WHERE);
// SIGNED INT if filter.class().is_some() {
(Comparison::Equal, Condition::SignedInt(_)) => { idx += 1;
compose_fragment(name, "value_int", "=", i + 1) let fragment = format!(" AND entity.class = ?{}", idx);
} statement.push_str(&fragment);
(Comparison::Greater, Condition::SignedInt(_)) => { }
compose_fragment(name, "value_int", ">", i + 1) if filter.subclass().is_some() {
} idx += 1;
(Comparison::Lesser, Condition::SignedInt(_)) => { let fragment = format!(" AND entity.subclass = ?{}", idx);
compose_fragment(name, "value_int", "<", i + 1)
}
(Comparison::GreaterOrEqual, Condition::SignedInt(_)) => {
compose_fragment(name, "value_int", ">=", i + 1)
}
(Comparison::LesserOrEqual, Condition::SignedInt(_)) => {
compose_fragment(name, "value_int", "<=", i + 1)
}
(_, Condition::SignedInt(_)) => return Err(FailedTo::ComposeFilter),
// UNSIGNED INT
(Comparison::Equal, Condition::UnsignedInt(_)) => {
compose_fragment(name, "value_uint", "=", i + 1)
}
(Comparison::Greater, Condition::UnsignedInt(_)) => {
compose_fragment(name, "value_uint", ">", i + 1)
}
(Comparison::Lesser, Condition::UnsignedInt(_)) => {
compose_fragment(name, "value_uint", "<", i + 1)
}
(Comparison::GreaterOrEqual, Condition::UnsignedInt(_)) => {
compose_fragment(name, "value_uint", ">=", i + 1)
}
(Comparison::LesserOrEqual, Condition::UnsignedInt(_)) => {
compose_fragment(name, "value_uint", "<=", i + 1)
}
(_, Condition::UnsignedInt(_)) => return Err(FailedTo::ComposeFilter),
// REAL
(Comparison::Equal, Condition::Real(_)) => {
compose_fragment(name, "value_real", "=", i + 1)
}
(Comparison::Greater, Condition::Real(_)) => {
compose_fragment(name, "value_real", ">", i + 1)
}
(Comparison::Lesser, Condition::Real(_)) => {
compose_fragment(name, "value_real", "<", i + 1)
}
(Comparison::GreaterOrEqual, Condition::Real(_)) => {
compose_fragment(name, "value_real", ">=", i + 1)
}
(Comparison::LesserOrEqual, Condition::Real(_)) => {
compose_fragment(name, "value_real", "<=", i + 1)
}
(_, Condition::Real(_)) => return Err(FailedTo::ComposeFilter),
// TEXT
(Comparison::IsExactly, Condition::Text(_)) => {
compose_fragment(name, "value_text", "LIKE", i + 1)
}
(
Comparison::StartsWith | Comparison::EndsWith | Comparison::Contains,
Condition::Text(_),
) => compose_fragment(name, "value_text", "LIKE", i + 1),
(_, Condition::Text(_)) => return Err(FailedTo::ComposeFilter),
};
statement.push_str(&fragment); statement.push_str(&fragment);
} }
Ok(statement) Ok(statement)

View File

@@ -17,8 +17,8 @@ const DELETE_ENTITY_STATEMENT: &str = r#"
"#; "#;
const INSERT_ENTITY_STATEMENT: &str = r#" const INSERT_ENTITY_STATEMENT: &str = r#"
INSERT INTO entity (id, class, ref_date) INSERT INTO entity (id, class, subclass, ref_date)
VALUES (?1, ?2, ?3); VALUES (?1, ?2, ?3, ?4);
"#; "#;
const INSERT_ATTRIBUTE_STATEMENT_TEMPLATE: &str = r#" const INSERT_ATTRIBUTE_STATEMENT_TEMPLATE: &str = r#"
@@ -51,7 +51,7 @@ fn delete_entity(entity: &Entity, transaction: &rusqlite::Transaction) -> Result
fn write_entity(entity: &Entity, transaction: &rusqlite::Transaction) -> Result<(), FailedTo> { fn write_entity(entity: &Entity, transaction: &rusqlite::Transaction) -> Result<(), FailedTo> {
let entity_id = [&entity.id]; let entity_id = [&entity.id];
let entity_values = (&entity.id, &entity.class, entity.ref_date); let entity_values = (&entity.id, &entity.class, &entity.subclass, entity.ref_date);
transaction transaction
.execute(DELETE_ENTITY_STATEMENT, entity_id) .execute(DELETE_ENTITY_STATEMENT, entity_id)
.map_err(|_| sqlite::FailedTo::ExecuteStatement)?; .map_err(|_| sqlite::FailedTo::ExecuteStatement)?;

View File

@@ -369,6 +369,7 @@ mod tests {
fn from(value: Item) -> Entity { fn from(value: Item) -> Entity {
let mut entity = Entity::new::<Item>() let mut entity = Entity::new::<Item>()
.with_id(&value.id) .with_id(&value.id)
.with_subclass("subitem")
.with_attribute("name", value.name) .with_attribute("name", value.name)
.with_attribute("price", value.price) .with_attribute("price", value.price)
.with_attribute("discount", value.discount) .with_attribute("discount", value.discount)
@@ -554,6 +555,7 @@ mod tests {
let mut catalog = Catalog::new("dummy.db"); let mut catalog = Catalog::new("dummy.db");
let item = Item { let item = Item {
id: "item-123".to_string(), id: "item-123".to_string(),
subclass: Some("subitem".to_string()),
name: "Test Item".to_string(), name: "Test Item".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -595,6 +597,7 @@ mod tests {
}; };
let item2 = Item { let item2 = Item {
id: "item-2".to_string(), id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Unique Item".to_string(), name: "Unique Item".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -614,6 +617,7 @@ mod tests {
let mut catalog = Catalog::new("dummy.db"); let mut catalog = Catalog::new("dummy.db");
let item1 = Item { let item1 = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(), name: "Item One".to_string(),
price: 100, price: 100,
sell_trend: 10, sell_trend: 10,
@@ -622,6 +626,7 @@ mod tests {
}; };
let item2 = Item { let item2 = Item {
id: "item-2".to_string(), id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(), name: "Item Two".to_string(),
price: 250, price: 250,
sell_trend: 20, sell_trend: 20,
@@ -681,6 +686,7 @@ mod tests {
let mut catalog = Catalog::new("dummy.db"); let mut catalog = Catalog::new("dummy.db");
let item1 = Item { let item1 = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(), name: "Item One".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -689,6 +695,7 @@ mod tests {
}; };
let item2 = Item { let item2 = Item {
id: "item-2".to_string(), id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(), name: "Item Two".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -722,6 +729,7 @@ mod tests {
let mut catalog = Catalog::new("dummy.db"); let mut catalog = Catalog::new("dummy.db");
let item1 = Item { let item1 = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(), name: "Item One".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -730,6 +738,7 @@ mod tests {
}; };
let item2 = Item { let item2 = Item {
id: "item-2".to_string(), id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(), name: "Item Two".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -738,6 +747,7 @@ mod tests {
}; };
let item3 = Item { let item3 = Item {
id: "item-3".to_string(), id: "item-3".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Three".to_string(), name: "Item Three".to_string(),
price: 300, price: 300,
sell_trend: 0, sell_trend: 0,
@@ -841,6 +851,7 @@ mod tests {
catalog1.init().unwrap(); catalog1.init().unwrap();
let item1 = Item { let item1 = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Test Item".to_string(), name: "Test Item".to_string(),
price: 123, price: 123,
sell_trend: 0, sell_trend: 0,
@@ -906,6 +917,7 @@ mod tests {
catalog1.init().unwrap(); catalog1.init().unwrap();
let original_item = Item { let original_item = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Original Name".to_string(), name: "Original Name".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -920,6 +932,7 @@ mod tests {
// 3. Upsert updated data for the same item. This should mark it as 'Updated'. // 3. Upsert updated data for the same item. This should mark it as 'Updated'.
let updated_item = Item { let updated_item = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Updated Name".to_string(), name: "Updated Name".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -957,6 +970,7 @@ mod tests {
catalog_setup.init().unwrap(); catalog_setup.init().unwrap();
let item_to_update_original = Item { let item_to_update_original = Item {
id: "update-me".to_string(), id: "update-me".to_string(),
subclass: Some("subitem".to_string()),
name: "Original".to_string(), name: "Original".to_string(),
price: 10, price: 10,
sell_trend: 0, sell_trend: 0,
@@ -965,6 +979,7 @@ mod tests {
}; };
let item_to_delete = Item { let item_to_delete = Item {
id: "delete-me".to_string(), id: "delete-me".to_string(),
subclass: Some("subitem".to_string()),
name: "Delete Me".to_string(), name: "Delete Me".to_string(),
price: 20, price: 20,
sell_trend: 0, sell_trend: 0,
@@ -973,6 +988,7 @@ mod tests {
}; };
let item_to_keep = Item { let item_to_keep = Item {
id: "keep-me".to_string(), id: "keep-me".to_string(),
subclass: Some("subitem".to_string()),
name: "Keep Me".to_string(), name: "Keep Me".to_string(),
price: 30, price: 30,
sell_trend: 0, sell_trend: 0,
@@ -989,6 +1005,7 @@ mod tests {
// A new item to be inserted. // A new item to be inserted.
let item_to_add = Item { let item_to_add = Item {
id: "add-me".to_string(), id: "add-me".to_string(),
subclass: Some("subitem".to_string()),
name: "Add Me".to_string(), name: "Add Me".to_string(),
price: 40, price: 40,
sell_trend: 0, sell_trend: 0,
@@ -999,6 +1016,7 @@ mod tests {
// An updated version of an existing item. // An updated version of an existing item.
let item_to_update_new = Item { let item_to_update_new = Item {
id: "update-me".to_string(), id: "update-me".to_string(),
subclass: Some("subitem".to_string()),
name: "Updated".to_string(), name: "Updated".to_string(),
price: 11, price: 11,
sell_trend: 0, sell_trend: 0,
@@ -1194,6 +1212,7 @@ mod tests {
catalog1.init().unwrap(); catalog1.init().unwrap();
let item_to_persist = Item { let item_to_persist = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Test Item".to_string(), name: "Test Item".to_string(),
price: 123, price: 123,
sell_trend: 0, sell_trend: 0,
@@ -1245,6 +1264,7 @@ mod tests {
catalog1.init().unwrap(); catalog1.init().unwrap();
let item_in_db = Item { let item_in_db = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item from DB".to_string(), name: "Item from DB".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -1257,6 +1277,7 @@ mod tests {
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let item_in_memory = Item { let item_in_memory = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "In-memory version".to_string(), name: "In-memory version".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -1343,6 +1364,7 @@ mod tests {
catalog1.init().unwrap(); catalog1.init().unwrap();
let item1 = Item { let item1 = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(), name: "Item One".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -1351,6 +1373,7 @@ mod tests {
}; };
let item2 = Item { let item2 = Item {
id: "item-2".to_string(), id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(), name: "Item Two".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -1395,6 +1418,7 @@ mod tests {
catalog1.init().unwrap(); catalog1.init().unwrap();
let item_in_db = Item { let item_in_db = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "DB Version".to_string(), name: "DB Version".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -1407,6 +1431,7 @@ mod tests {
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let item_in_memory = Item { let item_in_memory = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Memory Version".to_string(), name: "Memory Version".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -1452,6 +1477,7 @@ mod tests {
// 3. Add an item to memory and try loading again from the empty DB. // 3. Add an item to memory and try loading again from the empty DB.
let item_in_memory = Item { let item_in_memory = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "In-memory only".to_string(), name: "In-memory only".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -1622,6 +1648,7 @@ mod tests {
catalog_setup.init().unwrap(); catalog_setup.init().unwrap();
let item_in_db = Item { let item_in_db = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "DB Version".to_string(), name: "DB Version".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -1633,6 +1660,7 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let item_in_memory = Item { let item_in_memory = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Memory Version".to_string(), name: "Memory Version".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -2191,6 +2219,7 @@ mod tests {
catalog1.init().unwrap(); catalog1.init().unwrap();
let item_to_insert = Item { let item_to_insert = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Integration Test Item".to_string(), name: "Integration Test Item".to_string(),
price: 999, price: 999,
sell_trend: 0, sell_trend: 0,
@@ -2223,6 +2252,7 @@ mod tests {
let items_to_insert = vec![ let items_to_insert = vec![
Item { Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(), name: "Item One".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -2231,6 +2261,7 @@ mod tests {
}, },
Item { Item {
id: "item-2".to_string(), id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(), name: "Item Two".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -2309,6 +2340,7 @@ mod tests {
}; };
let item2 = Item { let item2 = Item {
id: "item-2".to_string(), id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Second Item".to_string(), name: "Second Item".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -2331,6 +2363,7 @@ mod tests {
let mut catalog = Catalog::new("dummy.db"); let mut catalog = Catalog::new("dummy.db");
let item1 = Item { let item1 = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(), name: "Item One".to_string(),
price: 100, price: 100,
sell_trend: 0, sell_trend: 0,
@@ -2339,6 +2372,7 @@ mod tests {
}; };
let item2 = Item { let item2 = Item {
id: "item-2".to_string(), id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(), name: "Item Two".to_string(),
price: 200, price: 200,
sell_trend: 0, sell_trend: 0,
@@ -2347,6 +2381,7 @@ mod tests {
}; };
let item3 = Item { let item3 = Item {
id: "item-3".to_string(), id: "item-3".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Three".to_string(), name: "Item Three".to_string(),
price: 150, price: 150,
sell_trend: 0, sell_trend: 0,
@@ -3149,10 +3184,8 @@ mod tests {
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.items.len(), 1);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.items.contains_key("item-1"));
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
fn list_by_class_and_subclass_should_return_matching_entities() { fn list_by_class_and_subclass_should_return_matching_entities() {
let mut catalog = Catalog::new("dummy.db"); let mut catalog = Catalog::new("dummy.db");
@@ -3174,18 +3207,15 @@ mod tests {
let _ = catalog.upsert(item1.clone()); let _ = catalog.upsert(item1.clone());
let _ = catalog.upsert(item2.clone()); let _ = catalog.upsert(item2.clone());
let _ = catalog.upsert(item3.clone()); let _ = catalog.upsert(item3.clone());
let results: Vec<Item> = catalog let results: Vec<Item> = catalog
.list_by_class_and_subclass("electronics") .list_by_class_and_subclass("electronics")
.map(|item| item.unwrap()) .map(|item| item.unwrap())
.collect(); .collect();
assert_eq!(results.len(), 2); assert_eq!(results.len(), 2);
assert!(results.contains(&item1)); assert!(results.contains(&item1));
assert!(results.contains(&item3)); assert!(results.contains(&item3));
assert!(!results.contains(&item2)); assert!(!results.contains(&item2));
} }
#[test] #[test]
fn list_by_class_and_subclass_should_return_empty_if_no_match() { fn list_by_class_and_subclass_should_return_empty_if_no_match() {
let mut catalog = Catalog::new("dummy.db"); let mut catalog = Catalog::new("dummy.db");
@@ -3195,12 +3225,102 @@ mod tests {
..Default::default() ..Default::default()
}; };
let _ = catalog.upsert(item1.clone()); let _ = catalog.upsert(item1.clone());
let results: Vec<Item> = catalog let results: Vec<Item> = catalog
.list_by_class_and_subclass("books") .list_by_class_and_subclass("books")
.map(|item| item.unwrap()) .map(|item| item.unwrap())
.collect(); .collect();
assert!(results.is_empty()); assert!(results.is_empty());
} }
#[test]
fn load_by_filter_with_class_and_subclass_clauses() {
// Verifies that class and subclass clauses in a filter correctly load entities.
let db_path = "target/test_dbs/lbf_with_class_subclass_clauses.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
let mut catalog_setup = Catalog::new(db_path);
catalog_setup.init().unwrap();
// Define a second struct with a different class
#[derive(Debug, Default, PartialEq, Clone)]
struct AnotherItem {
pub id: String,
}
impl EAV for AnotherItem {
fn class() -> &'static str {
"another_item"
}
}
impl From<AnotherItem> for Entity {
fn from(value: AnotherItem) -> Entity {
Entity::new::<AnotherItem>().with_id(&value.id)
}
}
impl From<Entity> for AnotherItem {
fn from(entity: Entity) -> Self {
Self {
id: entity.id.clone(),
}
}
}
let item1 = Item {
id: "item-1".to_string(),
subclass: Some("sub-a".to_string()),
..Default::default()
};
let item2 = Item {
id: "item-2".to_string(),
subclass: Some("sub-b".to_string()),
..Default::default()
};
let item3 = Item {
id: "item-3".to_string(),
subclass: Some("sub-a".to_string()),
..Default::default()
};
let item4 = Item {
id: "item-4".to_string(),
subclass: None, // -> "subitem"
..Default::default()
};
let another_item = AnotherItem {
id: "another-1".to_string(),
};
catalog_setup.upsert(item1).unwrap();
catalog_setup.upsert(item2).unwrap();
catalog_setup.upsert(item3).unwrap();
catalog_setup.upsert(item4).unwrap();
catalog_setup.upsert(another_item).unwrap();
catalog_setup.persist().unwrap();
// Test 1: Filter by class "item"
let mut catalog1 = Catalog::new(db_path);
let filter1 = Filter::new().with_class("item");
assert!(catalog1.load_by_filter(&filter1).is_ok());
assert_eq!(catalog1.items.len(), 4);
assert!(catalog1.items.contains_key("item-1"));
assert!(catalog1.items.contains_key("item-2"));
assert!(catalog1.items.contains_key("item-3"));
assert!(catalog1.items.contains_key("item-4"));
// Test 2: Filter by class "another_item"
let mut catalog2 = Catalog::new(db_path);
let filter2 = Filter::new().with_class("another_item");
assert!(catalog2.load_by_filter(&filter2).is_ok());
assert_eq!(catalog2.items.len(), 1);
assert!(catalog2.items.contains_key("another-1"));
// Test 3: Filter by class "item" and subclass "sub-a"
let mut catalog3 = Catalog::new(db_path);
let filter3 = Filter::new().with_class("item").with_subclass("sub-a");
assert!(catalog3.load_by_filter(&filter3).is_ok());
assert_eq!(catalog3.items.len(), 2);
assert!(catalog3.items.contains_key("item-1"));
assert!(catalog3.items.contains_key("item-3"));
// Test 4: Filter by class "item" and subclass "subitem" (the default)
let mut catalog4 = Catalog::new(db_path);
let filter4 = Filter::new().with_class("item").with_subclass("subitem");
assert!(catalog4.load_by_filter(&filter4).is_ok());
assert_eq!(catalog4.items.len(), 1);
assert!(catalog4.items.contains_key("item-4"));
std::fs::remove_file(path).unwrap();
}
} }

View File

@@ -7,6 +7,8 @@ use crate::*;
/// all specified criteria. /// all specified criteria.
#[derive(Debug, Default, PartialEq, Clone)] #[derive(Debug, Default, PartialEq, Clone)]
pub struct O<'a> { pub struct O<'a> {
class: Option<String>,
subclass: Option<String>,
conditions: Vec<(String, Comparison, Condition<'a>)>, conditions: Vec<(String, Comparison, Condition<'a>)>,
} }
@@ -14,9 +16,21 @@ impl<'a> Filter<'a> {
/// Creates a new, empty `Filter`. /// Creates a new, empty `Filter`.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
class: None,
subclass: None,
conditions: Vec::new(), conditions: Vec::new(),
} }
} }
/// Adds a class condition to the filter.
pub fn with_class(mut self, value: &str) -> Self {
self.class = Some(value.to_string());
self
}
/// Adds a subclass condition to the filter.
pub fn with_subclass(mut self, value: &str) -> Self {
self.subclass = Some(value.to_string());
self
}
/// Adds a boolean condition to the filter. /// Adds a boolean condition to the filter.
/// ///
/// This is a shorthand for `with_bool(name, Comparison::Equal, value)`. /// This is a shorthand for `with_bool(name, Comparison::Equal, value)`.
@@ -85,4 +99,10 @@ impl<'a> Filter<'a> {
pub(crate) fn conditions(&self) -> impl Iterator<Item = &(String, Comparison, Condition<'a>)> { pub(crate) fn conditions(&self) -> impl Iterator<Item = &(String, Comparison, Condition<'a>)> {
self.conditions.iter() self.conditions.iter()
} }
pub(crate) fn class(&self) -> &Option<String> {
&self.class
}
pub(crate) fn subclass(&self) -> &Option<String> {
&self.subclass
}
} }