diff --git a/src/core/interface/database/mod.rs b/src/core/interface/database/mod.rs index 27293d0..cc300f3 100644 --- a/src/core/interface/database/mod.rs +++ b/src/core/interface/database/mod.rs @@ -10,6 +10,9 @@ use crate::core::collection::{self, Collection}; /// Trait for interacting with the database. #[cfg_attr(test, automock)] pub trait IDatabase { + /// Reset all content. + fn reset(&mut self) -> Result<(), SaveError>; + /// Load collection from the database. fn load(&mut self) -> Result; @@ -21,6 +24,10 @@ pub trait IDatabase { pub struct NullDatabase; impl IDatabase for NullDatabase { + fn reset(&mut self) -> Result<(), SaveError> { + Ok(()) + } + fn load(&mut self) -> Result { Ok(vec![]) } @@ -98,6 +105,12 @@ mod tests { use super::*; + #[test] + fn null_database_reset() { + let mut database = NullDatabase; + assert!(database.reset().is_ok()); + } + #[test] fn null_database_load() { let mut database = NullDatabase; diff --git a/src/external/database/sql/backend.rs b/src/external/database/sql/backend.rs index 3ad96db..3a87781 100644 --- a/src/external/database/sql/backend.rs +++ b/src/external/database/sql/backend.rs @@ -108,7 +108,8 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> { name TEXT NOT NULL, mbid JSON NOT NULL DEFAULT '\"None\"', sort TEXT NULL, - properties JSON NOT NULL DEFAULT '{}' + properties JSON NOT NULL DEFAULT '{}', + UNIQUE(name, mbid) )", ); Self::execute(&mut stmt, ()) @@ -131,7 +132,8 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> { day INT NULL, seq INT NOT NULL, primary_type JSON NOT NULL DEFAULT 'null', - secondary_types JSON NOT NULL DEFAULT '[]' + secondary_types JSON NOT NULL DEFAULT '[]', + UNIQUE(title, lib_id, mbid) )", ); Self::execute(&mut stmt, ()) @@ -145,7 +147,11 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> { fn insert_database_version(&self, version: &str) -> Result<(), Error> { let mut stmt = self.prepare_cached( "INSERT INTO database_metadata (name, value) - VALUES (?1, ?2)", + VALUES (?1, ?2) + ON CONFLICT(name) DO UPDATE SET value = ?2 + WHERE EXISTS ( + SELECT 1 EXCEPT SELECT 1 WHERE value = ?2 + )", ); Self::execute(&mut stmt, ("version", version)) } @@ -163,7 +169,11 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> { fn insert_artist(&self, artist: &SerializeArtist<'_>) -> Result<(), Error> { let mut stmt = self.prepare_cached( "INSERT INTO artists (name, mbid, sort, properties) - VALUES (?1, ?2, ?3, ?4)", + VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(name, mbid) DO UPDATE SET sort = ?3, properties = ?4 + WHERE EXISTS ( + SELECT 1 EXCEPT SELECT 1 WHERE sort = ?3 AND properties = ?4 + )", ); Self::execute( &mut stmt, @@ -198,7 +208,15 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> { let mut stmt = self.prepare_cached( "INSERT INTO albums (title, lib_id, mbid, artist_name, year, month, day, seq, primary_type, secondary_types) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10) + ON CONFLICT(title, lib_id, mbid) DO UPDATE SET + artist_name = ?4, year = ?5, month = ?6, day = ?7, seq = ?8, primary_type = ?9, + secondary_types = ?10 + WHERE EXISTS ( + SELECT 1 EXCEPT SELECT 1 WHERE + artist_name = ?4 AND year = ?5 AND month = ?6 AND day = ?7 AND seq = ?8 AND + primary_type = ?9 AND secondary_types = ?10 + )", ); Self::execute( &mut stmt, diff --git a/src/external/database/sql/mod.rs b/src/external/database/sql/mod.rs index 78a3305..711ee86 100644 --- a/src/external/database/sql/mod.rs +++ b/src/external/database/sql/mod.rs @@ -152,6 +152,15 @@ impl ISqlDatabaseBackend<'conn>> SqlDatabase { } impl ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase { + fn reset(&mut self) -> Result<(),SaveError> { + let tx = self.backend.transaction()?; + + Self::drop_tables(&tx)?; + Self::create_tables(&tx)?; + + Ok(tx.commit()?) + } + fn load(&mut self) -> Result { let tx = self.backend.transaction()?; @@ -178,7 +187,6 @@ impl ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase let database: SerializeDatabase = collection.into(); let tx = self.backend.transaction()?; - Self::drop_tables(&tx)?; Self::create_tables(&tx)?; match database { @@ -300,13 +308,22 @@ mod tests { SqlDatabase::new(backend).unwrap() } + #[test] + fn reset() { + let mut tx = MockISqlTransactionBackend::new(); + let mut seq = Sequence::new(); + expect_drop!(tx, seq); + expect_create!(tx, seq); + then0!(tx, seq, expect_commit); + assert!(database(VecDeque::from([tx])).reset().is_ok()); + } + #[test] fn save() { let write_data = FULL_COLLECTION.to_owned(); let mut tx = MockISqlTransactionBackend::new(); let mut seq = Sequence::new(); - expect_drop!(tx, seq); expect_create!(tx, seq); then1!(tx, seq, expect_insert_database_version).with(predicate::eq(V20250103)); for artist in write_data.iter() { @@ -396,7 +413,6 @@ mod tests { fn save_backend_exec_error() { let mut tx = MockISqlTransactionBackend::new(); let mut seq = Sequence::new(); - expect_drop!(tx, seq); expect_create!(tx, seq); then!(tx, seq, expect_insert_database_version) .with(predicate::eq(V20250103)) @@ -426,7 +442,6 @@ mod tests { let mut tx = MockISqlTransactionBackend::new(); let mut seq = Sequence::new(); - expect_drop!(tx, seq); expect_create!(tx, seq); then1!(tx, seq, expect_insert_database_version).with(predicate::eq(V20250103)); then!(tx, seq, expect_insert_artist) diff --git a/tests/database/sql.rs b/tests/database/sql.rs index ea7461a..017167b 100644 --- a/tests/database/sql.rs +++ b/tests/database/sql.rs @@ -9,7 +9,7 @@ use musichoard::{ }; use tempfile::NamedTempFile; -use crate::testlib::COLLECTION; +use crate::{copy_file_into_temp, testlib::COLLECTION}; pub static DATABASE_TEST_FILE: Lazy = Lazy::new(|| fs::canonicalize("./tests/files/database/database.db").unwrap()); @@ -24,6 +24,16 @@ fn expected() -> Collection { expected } +#[test] +fn reset() { + let file = NamedTempFile::new().unwrap(); + + let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap(); + let mut database = SqlDatabase::new(backend).unwrap(); + + database.reset().unwrap(); +} + #[test] fn save() { let file = NamedTempFile::new().unwrap(); @@ -61,3 +71,18 @@ fn reverse() { let expected = expected(); assert_eq!(read_data, expected); } + +#[test] +fn reverse_with_reset() { + let file = copy_file_into_temp(&*DATABASE_TEST_FILE); + + let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap(); + let mut database = SqlDatabase::new(backend).unwrap(); + + let expected: Vec = database.load().unwrap(); + database.reset().unwrap(); + database.save(&expected).unwrap(); + let read_data: Vec = database.load().unwrap(); + + assert_eq!(read_data, expected); +} diff --git a/tests/files/database/database.db b/tests/files/database/database.db index d611935..75559cc 100644 Binary files a/tests/files/database/database.db and b/tests/files/database/database.db differ diff --git a/tests/lib.rs b/tests/lib.rs index 551af37..6cceef8 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -19,7 +19,7 @@ use tempfile::NamedTempFile; use crate::testlib::COLLECTION; -fn copy_file_into_temp>(path: P) -> NamedTempFile { +pub fn copy_file_into_temp>(path: P) -> NamedTempFile { let temp = NamedTempFile::new().unwrap(); fs::copy(path.into(), temp.path()).unwrap(); temp