test(db): add logging to tests

This commit is contained in:
ryan 2025-03-06 15:09:28 +03:00
parent 6c2442a82b
commit 8ac1d77e06

317
src/db.rs
View file

@ -1,13 +1,13 @@
#[cfg(feature = "ssr")]
mod db_impl {
use crate::models::item::Item;
use leptos::logging;
use leptos::logging::log;
use rusqlite::{Connection, Error};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tokio::sync::Mutex;
use leptos::logging;
use std::collections::{HashMap, HashSet};
use crate::models::item::Item;
use leptos::logging::log;
#[cfg(test)]
mod tests {
@ -17,19 +17,29 @@ mod db_impl {
// Helper function to create test database
async fn create_test_db() -> Database {
log!("[TEST] Creating in-memory test database");
let db = Database::new(":memory:").unwrap();
db.create_schema().await.unwrap();
log!("[TEST] Database schema created");
db
}
// Test database schema creation
#[tokio::test]
async fn test_schema_creation() {
log!("[TEST] Starting test_schema_creation");
let db = create_test_db().await;
// Verify tables exist
let conn = db.conn.lock().await;
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table'").unwrap();
let tables: Vec<String> = stmt.query_map([], |row| row.get(0)).unwrap().collect::<Result<_, _>>().unwrap();
let mut stmt = conn
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
.unwrap();
let tables: Vec<String> = stmt
.query_map([], |row| row.get(0))
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert!(tables.contains(&"urls".to_string()));
assert!(tables.contains(&"items".to_string()));
@ -41,6 +51,7 @@ mod db_impl {
// Item Lifecycle Tests
#[tokio::test]
async fn test_full_item_lifecycle() {
log!("[TEST] Starting test_full_item_lifecycle");
let db = create_test_db().await;
let test_url = "https://example.com";
let test_item = Item {
@ -50,62 +61,87 @@ mod db_impl {
wikidata_id: Some("Q123".into()),
custom_properties: vec![
("price".into(), "100".into()),
("color".into(), "red".into())
].into_iter().collect(),
("color".into(), "red".into()),
]
.into_iter()
.collect(),
};
// Test insertion
log!("[TEST] Testing item insertion");
db.insert_item_by_url(test_url, &test_item).await.unwrap();
log!("[TEST] Item insertion - PASSED");
// Test retrieval
log!("[TEST] Testing item retrieval");
let items = db.get_items_by_url(test_url).await.unwrap();
assert_eq!(items.len(), 1);
let stored_item = &items[0];
assert_eq!(stored_item.name, test_item.name);
assert_eq!(stored_item.custom_properties.len(), 2);
log!("[TEST] Item retrieval and validation - PASSED");
// Test update
log!("[TEST] Testing item update");
let mut updated_item = test_item.clone();
updated_item.name = "Updated Name".into();
db.insert_item_by_url(test_url, &updated_item).await.unwrap();
db.insert_item_by_url(test_url, &updated_item)
.await
.unwrap();
// Verify update
let items = db.get_items_by_url(test_url).await.unwrap();
assert_eq!(items[0].name, "Updated Name");
log!("[TEST] Item update - PASSED");
// Test deletion
db.delete_item_by_url(test_url, &test_item.id).await.unwrap();
log!("[TEST] Testing item deletion");
db.delete_item_by_url(test_url, &test_item.id)
.await
.unwrap();
let items = db.get_items_by_url(test_url).await.unwrap();
assert!(items.is_empty());
log!("[TEST] Item deletion - PASSED");
log!("[TEST] test_full_item_lifecycle completed successfully");
}
//URL Management Tests
#[tokio::test]
async fn test_url_management() {
log!("[TEST] Starting test_url_management");
let db = create_test_db().await;
let test_url = "https://test.com";
// Test URL creation
log!("[TEST] Testing URL creation");
let url_id = db.insert_url(test_url).await.unwrap();
assert!(url_id > 0);
log!("[TEST] URL creation - PASSED");
// Test duplicate URL handling
log!("[TEST] Testing duplicate URL handling");
let duplicate_id = db.insert_url(test_url).await.unwrap();
assert_eq!(url_id, duplicate_id);
log!("[TEST] Duplicate URL handling - PASSED");
// Test URL retrieval
log!("[TEST] Testing URL retrieval");
let conn = db.conn.lock().await;
let stored_url: String = conn.query_row(
"SELECT url FROM urls WHERE id = ?",
[url_id],
|row| row.get(0)
).unwrap();
let stored_url: String = conn
.query_row("SELECT url FROM urls WHERE id = ?", [url_id], |row| {
row.get(0)
})
.unwrap();
assert_eq!(stored_url, test_url);
log!("[TEST] URL retrieval - PASSED");
log!("[TEST] test_url_management completed successfully");
}
//property management tests
#[tokio::test]
async fn test_property_operations() {
log!("[TEST] Starting test_property_operations");
let db = create_test_db().await;
let test_url = "https://props.com";
let test_item = Item {
@ -115,43 +151,59 @@ mod db_impl {
wikidata_id: Some("Q123".into()),
custom_properties: vec![
("price".into(), "100".into()),
("color".into(), "red".into())
].into_iter().collect(),
};
("color".into(), "red".into()),
]
.into_iter()
.collect(),
};
// Test property creation
log!("[TEST] Testing property creation");
db.insert_item_by_url(test_url, &test_item).await.unwrap();
// Verify properties stored
let items = db.get_items_by_url(test_url).await.unwrap();
assert_eq!(items[0].custom_properties.len(), 2);
log!("[TEST] Property creation - PASSED");
// Test property deletion
log!("[TEST] Testing property deletion");
db.delete_property_by_url(test_url, "price").await.unwrap();
let items = db.get_items_by_url(test_url).await.unwrap();
assert_eq!(items[0].custom_properties.len(), 1);
assert!(!items[0].custom_properties.contains_key("price"));
log!("[TEST] Property deletion - PASSED");
log!("[TEST] test_property_operations completed successfully");
}
//selected properties test
#[tokio::test]
async fn test_selected_properties() {
log!("[TEST] Starting test_selected_properties");
let db = create_test_db().await;
let test_url = "https://selected.com";
// Add test properties
log!("[TEST] Adding selected properties");
db.add_selected_property(test_url, "price").await.unwrap();
db.add_selected_property(test_url, "weight").await.unwrap();
// Test retrieval
log!("[TEST] Testing property retrieval");
let props = db.get_selected_properties(test_url).await.unwrap();
assert_eq!(props.len(), 2);
assert!(props.contains(&"price".to_string()));
assert!(props.contains(&"weight".to_string()));
log!("[TEST] Property retrieval - PASSED");
// Test duplicate prevention
log!("[TEST] Testing duplicate prevention");
db.add_selected_property(test_url, "price").await.unwrap();
let props = db.get_selected_properties(test_url).await.unwrap();
assert_eq!(props.len(), 2); // No duplicate added
log!("[TEST] Duplicate prevention - PASSED");
log!("[TEST] test_selected_properties completed successfully");
}
}
@ -165,7 +217,7 @@ mod db_impl {
// Create a new database connection
pub fn new(db_path: &str) -> Result<Self, Error> {
let conn = Connection::open(db_path)?;
logging::log!("Database connection established at: {}", db_path);
logging::log!("Database connection established at: {}", db_path);
Ok(Database {
conn: Arc::new(Mutex::new(conn)),
})
@ -174,15 +226,16 @@ mod db_impl {
// Create the database schema
pub async fn create_schema(&self) -> Result<(), Error> {
let conn = self.conn.lock().await;
// 1. Properties table
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS properties (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
global_usage_count INTEGER DEFAULT 0
);"
).map_err(|e| {
);",
)
.map_err(|e| {
eprintln!("Failed creating properties table: {}", e);
e
})?;
@ -194,7 +247,8 @@ mod db_impl {
url TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);",
).map_err(|e| {
)
.map_err(|e| {
eprintln!("Failed creating urls table: {}", e);
e
})?;
@ -209,7 +263,8 @@ mod db_impl {
wikidata_id TEXT,
FOREIGN KEY (url_id) REFERENCES urls(id) ON DELETE CASCADE
);",
).map_err(|e| {
)
.map_err(|e| {
eprintln!("Failed creating items table: {}", e);
e
})?;
@ -222,12 +277,13 @@ mod db_impl {
PRIMARY KEY (url_id, property_id),
FOREIGN KEY (url_id) REFERENCES urls(id) ON DELETE CASCADE,
FOREIGN KEY (property_id) REFERENCES properties(id) ON DELETE CASCADE
);"
).map_err(|e| {
);",
)
.map_err(|e| {
eprintln!("Failed creating properties table: {}", e);
e
})?;
// 5. Junction table for custom properties
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS item_properties (
@ -237,8 +293,9 @@ mod db_impl {
PRIMARY KEY (item_id, property_id),
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
FOREIGN KEY (property_id) REFERENCES properties(id) ON DELETE CASCADE
);"
).map_err(|e| {
);",
)
.map_err(|e| {
eprintln!("Failed creating item_properties table: {}", e);
e
})?;
@ -249,19 +306,13 @@ mod db_impl {
pub async fn insert_url(&self, url: &str) -> Result<i64, Error> {
let mut conn = self.conn.lock().await;
let tx = conn.transaction()?;
// Use INSERT OR IGNORE to handle duplicates
tx.execute(
"INSERT OR IGNORE INTO urls (url) VALUES (?)",
[url]
)?;
tx.execute("INSERT OR IGNORE INTO urls (url) VALUES (?)", [url])?;
// Get the URL ID whether it was inserted or already existed
let url_id = tx.query_row(
"SELECT id FROM urls WHERE url = ?",
[url],
|row| row.get(0)
)?;
let url_id =
tx.query_row("SELECT id FROM urls WHERE url = ?", [url], |row| row.get(0))?;
tx.commit()?;
logging::log!("URL inserted: {}", url);
@ -277,12 +328,15 @@ mod db_impl {
pub async fn delete_property(&self, property: &str) -> Result<(), Error> {
let conn = self.conn.lock().await;
let query = format!("UPDATE items SET custom_properties = json_remove(custom_properties, '$.{}')", property);
let query = format!(
"UPDATE items SET custom_properties = json_remove(custom_properties, '$.{}')",
property
);
conn.execute(&query, []).map_err(|e| Error::from(e))?;
logging::log!("Property deleted: {}", property);
Ok(())
}
// Retrieve all items from the database
pub async fn get_items(&self) -> Result<Vec<DbItem>, Error> {
let conn = self.conn.lock().await;
@ -306,16 +360,15 @@ mod db_impl {
// Retrieve all items from the database for a specific URL
pub async fn get_items_by_url(&self, url: &str) -> Result<Vec<Item>, Error> {
let conn = self.conn.lock().await;
let url_id: Option<i64> = match conn.query_row(
"SELECT id FROM urls WHERE url = ?",
&[url],
|row| row.get(0)
) {
Ok(id) => Some(id),
Err(rusqlite::Error::QueryReturnedNoRows) => None,
Err(e) => return Err(e),
};
let url_id: Option<i64> =
match conn.query_row("SELECT id FROM urls WHERE url = ?", &[url], |row| {
row.get(0)
}) {
Ok(id) => Some(id),
Err(rusqlite::Error::QueryReturnedNoRows) => None,
Err(e) => return Err(e),
};
let url_id = match url_id {
Some(id) => id,
None => return Ok(Vec::new()), // Return empty list if URL not found
@ -323,28 +376,27 @@ mod db_impl {
log!("Fetching items for URL '{}' (ID: {})", url, url_id);
let mut stmt = conn.prepare(
"SELECT i.id, i.name, i.description, i.wikidata_id,
p.name AS prop_name, ip.value
FROM items i
LEFT JOIN item_properties ip ON i.id = ip.item_id
LEFT JOIN properties p ON ip.property_id = p.id
WHERE i.url_id = ?"
WHERE i.url_id = ?",
)?;
let mut items: HashMap<String, Item> = HashMap::new();
let rows = stmt.query_map([url_id], |row| {
Ok((
row.get::<_, String>(0)?, // id
row.get::<_, String>(1)?, // name
row.get::<_, String>(2)?, // description
row.get::<_, Option<String>>(3)?, // wikidata_id
row.get::<_, Option<String>>(4)?, // prop_name
row.get::<_, Option<String>>(5)?, // value
row.get::<_, String>(0)?, // id
row.get::<_, String>(1)?, // name
row.get::<_, String>(2)?, // description
row.get::<_, Option<String>>(3)?, // wikidata_id
row.get::<_, Option<String>>(4)?, // prop_name
row.get::<_, Option<String>>(5)?, // value
))
})?;
for row in rows {
let (id, name, desc, wd_id, prop, val) = row?;
let item = items.entry(id.clone()).or_insert(Item {
@ -354,25 +406,23 @@ mod db_impl {
wikidata_id: wd_id,
custom_properties: HashMap::new(),
});
if let (Some(p), Some(v)) = (prop, val) {
item.custom_properties.insert(p, v);
}
}
Ok(items.into_values().collect())
}
async fn get_or_create_property(
&self,
tx: &mut rusqlite::Transaction<'_>,
prop: &str
&self,
tx: &mut rusqlite::Transaction<'_>,
prop: &str,
) -> Result<i64, Error> {
match tx.query_row(
"SELECT id FROM properties WHERE name = ?",
[prop],
|row| row.get::<_, i64>(0)
) {
match tx.query_row("SELECT id FROM properties WHERE name = ?", [prop], |row| {
row.get::<_, i64>(0)
}) {
Ok(id) => Ok(id),
Err(rusqlite::Error::QueryReturnedNoRows) => {
tx.execute("INSERT INTO properties (name) VALUES (?)", [prop])?;
@ -385,30 +435,28 @@ mod db_impl {
// Insert a new item into the database for a specific URL
pub async fn insert_item_by_url(&self, url: &str, item: &Item) -> Result<(), Error> {
log!("[DB] Starting insert for URL: {}, Item: {}", url, item.id);
// 1. Check database lock acquisition
let lock_start = std::time::Instant::now();
let mut conn = self.conn.lock().await;
log!("[DB] Lock acquired in {:?}", lock_start.elapsed());
// 2. Transaction handling
log!("[DB] Starting transaction");
let mut tx = conn.transaction().map_err(|e| {
log!("[DB] Transaction start failed: {:?}", e);
e
})?;
// 3. URL handling
log!("[DB] Checking URL existence: {}", url);
let url_id = match tx.query_row(
"SELECT id FROM urls WHERE url = ?",
[url],
|row| row.get::<_, i64>(0)
) {
let url_id = match tx.query_row("SELECT id FROM urls WHERE url = ?", [url], |row| {
row.get::<_, i64>(0)
}) {
Ok(id) => {
log!("[DB] Found existing URL ID: {}", id);
id
},
}
Err(rusqlite::Error::QueryReturnedNoRows) => {
log!("[DB] Inserting new URL");
tx.execute("INSERT INTO urls (url) VALUES (?)", [url])?;
@ -418,7 +466,7 @@ mod db_impl {
}
Err(e) => return Err(e.into()),
};
// 4. Item insertion
log!("[DB] Upserting item");
tx.execute(
@ -446,7 +494,7 @@ mod db_impl {
"SELECT p.name, ip.value
FROM item_properties ip
JOIN properties p ON ip.property_id = p.id
WHERE ip.item_id = ?"
WHERE ip.item_id = ?",
)?;
let mapped_rows = stmt.query_map([&item.id], |row| {
@ -455,13 +503,18 @@ mod db_impl {
mapped_rows.collect::<Result<HashMap<String, String>, _>>()?
};
for (prop, value) in &item.custom_properties {
// Update existing or insert new
let prop_id = self.get_or_create_property(&mut tx, prop).await?;
if let Some(existing_value) = existing_props.get(prop) {
if existing_value != value {
log!("[DB] Updating property {} from '{}' to '{}'", prop, existing_value, value);
log!(
"[DB] Updating property {} from '{}' to '{}'",
prop,
existing_value,
value
);
tx.execute(
"UPDATE item_properties
SET value = ?
@ -481,7 +534,8 @@ mod db_impl {
}
// Remove deleted properties
let current_props: HashSet<&str> = item.custom_properties.keys().map(|s| s.as_str()).collect();
let current_props: HashSet<&str> =
item.custom_properties.keys().map(|s| s.as_str()).collect();
for (existing_prop, _) in existing_props {
if !current_props.contains(existing_prop.as_str()) {
log!("[DB] Removing deleted property {}", existing_prop);
@ -504,18 +558,15 @@ mod db_impl {
let tx = conn.transaction()?;
// Get URL ID
let url_id: i64 = tx.query_row(
"SELECT id FROM urls WHERE url = ?",
[url],
|row| row.get(0)
)?;
let url_id: i64 =
tx.query_row("SELECT id FROM urls WHERE url = ?", [url], |row| row.get(0))?;
// Delete item and properties
tx.execute(
"DELETE FROM items WHERE id = ? AND url_id = ?",
[item_id, &url_id.to_string()],
)?;
tx.commit()?;
Ok(())
}
@ -524,14 +575,11 @@ mod db_impl {
pub async fn delete_property_by_url(&self, url: &str, property: &str) -> Result<(), Error> {
let mut conn = self.conn.lock().await;
let tx = conn.transaction()?;
// Get URL ID
let url_id: i64 = tx.query_row(
"SELECT id FROM urls WHERE url = ?",
[url],
|row| row.get(0)
)?;
let url_id: i64 =
tx.query_row("SELECT id FROM urls WHERE url = ?", [url], |row| row.get(0))?;
// Delete property from all items in this URL
tx.execute(
"DELETE FROM item_properties
@ -543,7 +591,7 @@ mod db_impl {
)",
[property, &url_id.to_string()],
)?;
tx.commit()?;
Ok(())
}
@ -551,25 +599,20 @@ mod db_impl {
pub async fn add_selected_property(&self, url: &str, property: &str) -> Result<(), Error> {
let mut conn = self.conn.lock().await;
let tx = conn.transaction()?;
// Insert URL if it does not exists
tx.execute(
"INSERT OR IGNORE INTO urls (url) VALUES (?)",
[url]
)?;
tx.execute("INSERT OR IGNORE INTO urls (url) VALUES (?)", [url])?;
// Get URL ID
let url_id = tx.query_row(
"SELECT id FROM urls WHERE url = ?",
[url],
|row| row.get::<_, i64>(0)
)?;
let url_id = tx.query_row("SELECT id FROM urls WHERE url = ?", [url], |row| {
row.get::<_, i64>(0)
})?;
// Get/Create property
let prop_id = match tx.query_row(
"SELECT id FROM properties WHERE name = ?",
[property],
|row| row.get::<_, i64>(0)
|row| row.get::<_, i64>(0),
) {
Ok(id) => id,
Err(_) => {
@ -577,13 +620,13 @@ mod db_impl {
tx.last_insert_rowid()
}
};
// Insert into selected_properties
tx.execute(
"INSERT OR IGNORE INTO selected_properties (url_id, property_id) VALUES (?, ?)",
[url_id, prop_id]
[url_id, prop_id],
)?;
tx.commit()?;
Ok(())
}
@ -595,34 +638,42 @@ mod db_impl {
FROM selected_properties sp
JOIN properties p ON sp.property_id = p.id
JOIN urls u ON sp.url_id = u.id
WHERE u.url = ?"
WHERE u.url = ?",
)?;
let properties = stmt.query_map([url], |row| row.get(0))?;
properties.collect()
}
// function to log database state
pub async fn debug_dump(&self) -> Result<(), Error> {
let conn = self.conn.lock().await;
log!("[DATABASE DEBUG] URLs:");
let mut stmt = conn.prepare("SELECT id, url FROM urls")?;
let urls = stmt.query_map([], |row| {
Ok(format!("ID: {}, URL: {}", row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
Ok(format!(
"ID: {}, URL: {}",
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?
))
})?;
for url in urls {
log!("[DATABASE DEBUG] {}", url?);
}
log!("[DATABASE DEBUG] Items:");
let mut stmt = conn.prepare("SELECT id, name FROM items")?;
let items = stmt.query_map([], |row| {
Ok(format!("ID: {}, Name: '{}'", row.get::<_, String>(0)?, row.get::<_, String>(1)?))
Ok(format!(
"ID: {}, Name: '{}'",
row.get::<_, String>(0)?,
row.get::<_, String>(1)?
))
})?;
for item in items {
log!("[DATABASE DEBUG] {}", item?);
}
Ok(())
}
}
@ -638,4 +689,4 @@ mod db_impl {
}
#[cfg(feature = "ssr")]
pub use db_impl::{Database, DbItem};
pub use db_impl::{Database, DbItem};