diff --git a/src/core/interface/database/mod.rs b/src/core/interface/database/mod.rs index b60575c..3f9ab5f 100644 --- a/src/core/interface/database/mod.rs +++ b/src/core/interface/database/mod.rs @@ -11,7 +11,7 @@ use crate::core::collection::{self, Collection}; #[cfg_attr(test, automock)] pub trait IDatabase { /// Load collection from the database. - fn load(&self) -> Result; + fn load(&mut self) -> Result; /// Save collection to the database. fn save(&mut self, collection: &Collection) -> Result<(), SaveError>; @@ -21,7 +21,7 @@ pub trait IDatabase { pub struct NullDatabase; impl IDatabase for NullDatabase { - fn load(&self) -> Result { + fn load(&mut self) -> Result { Ok(vec![]) } @@ -100,7 +100,7 @@ mod tests { #[test] fn null_database_load() { - let database = NullDatabase; + let mut database = NullDatabase; assert!(database.load().unwrap().is_empty()); } diff --git a/src/external/database/json/mod.rs b/src/external/database/json/mod.rs index 0b7edf9..3d4709b 100644 --- a/src/external/database/json/mod.rs +++ b/src/external/database/json/mod.rs @@ -49,7 +49,7 @@ impl JsonDatabase { } impl IDatabase for JsonDatabase { - fn load(&self) -> Result { + fn load(&mut self) -> Result { let serialized = self.backend.read()?; let database: DeserializeDatabase = serde_json::from_str(&serialized)?; Ok(database.into()) diff --git a/src/external/database/serde/deserialize.rs b/src/external/database/serde/deserialize.rs index 182c9c6..1ce3da6 100644 --- a/src/external/database/serde/deserialize.rs +++ b/src/external/database/serde/deserialize.rs @@ -32,22 +32,22 @@ impl From for Collection { #[derive(Debug, Deserialize)] pub struct DeserializeArtist { - name: String, - sort: Option, - musicbrainz: DeserializeMbRefOption, - properties: HashMap>, - albums: Vec, + pub name: String, + pub sort: Option, + pub musicbrainz: DeserializeMbRefOption, + pub properties: HashMap>, + pub albums: Vec, } #[derive(Debug, Deserialize)] pub struct DeserializeAlbum { - title: String, - lib_id: SerdeAlbumLibId, - date: SerdeAlbumDate, - seq: u8, - musicbrainz: DeserializeMbRefOption, - primary_type: Option, - secondary_types: Vec, + pub title: String, + pub lib_id: SerdeAlbumLibId, + pub date: SerdeAlbumDate, + pub seq: u8, + pub musicbrainz: DeserializeMbRefOption, + pub primary_type: Option, + pub secondary_types: Vec, } #[derive(Debug, Deserialize)] @@ -110,6 +110,8 @@ impl<'de> Deserialize<'de> for DeserializeMbid { impl From for Artist { fn from(artist: DeserializeArtist) -> Self { + let mut albums: Vec = artist.albums.into_iter().map(Into::into).collect(); + albums.sort_unstable(); Artist { meta: ArtistMeta { id: ArtistId { @@ -121,7 +123,7 @@ impl From for Artist { properties: artist.properties, }, }, - albums: artist.albums.into_iter().map(Into::into).collect(), + albums, } } } diff --git a/src/external/database/serde/mod.rs b/src/external/database/serde/mod.rs index 6ccd8c4..9ca8585 100644 --- a/src/external/database/serde/mod.rs +++ b/src/external/database/serde/mod.rs @@ -1,5 +1,5 @@ //! Helper module for backends that can use serde for (de)serialisation. -mod common; +pub mod common; pub mod deserialize; pub mod serialize; diff --git a/src/external/database/sql/backend.rs b/src/external/database/sql/backend.rs index ecd4510..06fc99d 100644 --- a/src/external/database/sql/backend.rs +++ b/src/external/database/sql/backend.rs @@ -2,11 +2,20 @@ use std::path::PathBuf; -use rusqlite::{self, CachedStatement, Connection, Params, Statement, Transaction}; +use rusqlite::{ + self, types::FromSql, CachedStatement, Connection, Params, Row, Rows, Statement, Transaction, +}; -use crate::external::database::{ - serde::serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase}, - sql::{Error, ISqlDatabaseBackend, ISqlTransactionBackend}, +use crate::{ + collection::album::AlbumDate, + external::database::{ + serde::{ + common::SerdeAlbumDate, + deserialize::{DeserializeAlbum, DeserializeArtist, DeserializeDatabase}, + serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase}, + }, + sql::{Error, ISqlDatabaseBackend, ISqlTransactionBackend}, + }, }; /// SQLite database backend that uses SQLite as the implementation. @@ -45,6 +54,21 @@ impl<'conn> SqlTransactionSqliteBackend<'conn> { .map(|_| ()) .map_err(|err| Error::ExecError(err.to_string())) } + + fn query<'s, P: Params>(stmt: &'s mut Statement, params: P) -> Result, Error> { + stmt.query(params) + .map_err(|err| Error::ExecError(err.to_string())) + } + + fn next_row<'r, 's>(rows: &'r mut Rows<'s>) -> Result>, Error> { + rows.next() + .map_err(|err| Error::SerDeError(err.to_string())) + } + + fn get_value(row: &Row<'_>, idx: usize) -> Result { + row.get(idx) + .map_err(|err| Error::SerDeError(err.to_string())) + } } impl From for Error { @@ -116,7 +140,7 @@ impl<'conn> ISqlTransactionBackend for SqlTransactionSqliteBackend<'conn> { seq INT NOT NULL, primary_type JSON NOT NULL DEFAULT 'null', secondary_types JSON NOT NULL DEFAULT '[]', - FOREIGN KEY (artist_name) REFERENCES artists(name) + FOREIGN KEY (artist_name) REFERENCES artists(name) ON DELETE CASCADE ON UPDATE NO ACTION )", )?; Self::execute(&mut stmt, ()) @@ -138,6 +162,25 @@ impl<'conn> ISqlTransactionBackend for SqlTransactionSqliteBackend<'conn> { Self::execute(&mut stmt, ("version", version)) } + fn select_database_version<'a>(&self) -> Result { + let mut stmt = + self.prepare_cached("SELECT value FROM database_metadata WHERE name = 'version'")?; + let mut rows = Self::query(&mut stmt, ())?; + + match Self::next_row(&mut rows)? { + Some(row) => { + let version: String = Self::get_value(&row, 0)?; + match version.as_str() { + "V20250103" => Ok(DeserializeDatabase::V20250103(vec![])), + s @ _ => Err(Error::SerDeError(format!("unknown database version: {s}"))), + } + } + None => Err(Error::SerDeError(String::from( + "database version is missing", + ))), + } + } + fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<(), Error> { let mut stmt = self.prepare_cached( "INSERT INTO artists (name, sort, mbid, properties) @@ -154,6 +197,24 @@ impl<'conn> ISqlTransactionBackend for SqlTransactionSqliteBackend<'conn> { ) } + fn select_all_artists(&self) -> Result, Error> { + let mut stmt = self.prepare_cached("SELECT name, sort, mbid, properties FROM artists")?; + let mut rows = Self::query(&mut stmt, ())?; + + let mut artists = vec![]; + while let Some(row) = Self::next_row(&mut rows)? { + artists.push(DeserializeArtist { + name: Self::get_value(row, 0)?, + sort: Self::get_value(row, 1)?, + musicbrainz: serde_json::from_str(&Self::get_value::(row, 2)?)?, + properties: serde_json::from_str(&Self::get_value::(row, 3)?)?, + albums: vec![], + }); + } + + Ok(artists) + } + fn insert_album<'a>(&self, artist_name: &str, album: &SerializeAlbum<'a>) -> Result<(), Error> { let mut stmt = self.prepare_cached( "INSERT INTO albums (title, lib_id, mbid, artist_name, @@ -176,4 +237,31 @@ impl<'conn> ISqlTransactionBackend for SqlTransactionSqliteBackend<'conn> { ), ) } + + fn select_artist_albums(&self, artist_name: &str) -> Result, Error> { + let mut stmt = self.prepare_cached( + "SELECT title, lib_id, year, month, day, seq, mbid, primary_type, secondary_types + FROM albums WHERE artist_name = ?1", + )?; + let mut rows = Self::query(&mut stmt, [artist_name])?; + + let mut albums = vec![]; + while let Some(row) = Self::next_row(&mut rows)? { + albums.push(DeserializeAlbum { + title: Self::get_value(row, 0)?, + lib_id: serde_json::from_str(&Self::get_value::(row, 1)?)?, + date: SerdeAlbumDate(AlbumDate::new( + Self::get_value(row, 2)?, + Self::get_value(row, 3)?, + Self::get_value(row, 4)?, + )), + seq: Self::get_value(row, 5)?, + musicbrainz: serde_json::from_str(&Self::get_value::(row, 6)?)?, + primary_type: serde_json::from_str(&Self::get_value::(row, 7)?)?, + secondary_types: serde_json::from_str(&Self::get_value::(row, 8)?)?, + }); + } + + Ok(albums) + } } diff --git a/src/external/database/sql/mod.rs b/src/external/database/sql/mod.rs index 4b1f198..137ecee 100644 --- a/src/external/database/sql/mod.rs +++ b/src/external/database/sql/mod.rs @@ -12,7 +12,10 @@ use crate::{ collection::Collection, interface::database::{IDatabase, LoadError, SaveError}, }, - external::database::serde::serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase}, + external::database::serde::{ + deserialize::{DeserializeAlbum, DeserializeArtist, DeserializeDatabase}, + serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase}, + }, }; /// Trait for the SQL database backend. @@ -50,11 +53,20 @@ pub trait ISqlTransactionBackend { /// Set the database version. fn insert_database_version<'a>(&self, version: &SerializeDatabase<'a>) -> Result<(), Error>; + /// Get the database version. + fn select_database_version<'a>(&self) -> Result; + /// Insert an artist into the artist table. fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<(), Error>; + /// Get all artists from the artist table. + fn select_all_artists(&self) -> Result, Error>; + /// Insert an artist into the artist table. fn insert_album<'a>(&self, artist_name: &str, album: &SerializeAlbum<'a>) -> Result<(), Error>; + + /// Get all albums from the album table. + fn select_artist_albums(&self, artist_name: &str) -> Result, Error>; } /// Errors for SQL database backend. @@ -95,6 +107,15 @@ impl From for SaveError { } } +impl From for LoadError { + fn from(value: Error) -> Self { + match value { + Error::SerDeError(s) => LoadError::SerDeError(s), + _ => LoadError::IoError(value.to_string()), + } + } +} + /// SQL database. pub struct SqlDatabase { backend: SDB, @@ -127,8 +148,21 @@ impl ISqlDatabaseBackend<'conn>> SqlDatabase { } impl ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase { - fn load(&self) -> Result { - Ok(vec![]) + fn load(&mut self) -> Result { + let tx = self.backend.transaction()?; + + let mut database = tx.select_database_version()?; + match database { + DeserializeDatabase::V20250103(ref mut coll) => { + coll.extend(tx.select_all_artists()?); + for artist in coll.iter_mut() { + artist.albums.extend(tx.select_artist_albums(&artist.name)?); + } + } + } + + tx.commit()?; + Ok(database.into()) } fn save(&mut self, collection: &Collection) -> Result<(), SaveError> { diff --git a/src/external/musicbrainz/api/mod.rs b/src/external/musicbrainz/api/mod.rs index bc3cc60..75c3c66 100644 --- a/src/external/musicbrainz/api/mod.rs +++ b/src/external/musicbrainz/api/mod.rs @@ -227,7 +227,7 @@ impl<'de> Deserialize<'de> for SerdeMbid { } #[derive(Debug, Clone)] -pub struct SerdeAlbumDate(AlbumDate); +pub struct SerdeAlbumDate(pub AlbumDate); impl From for AlbumDate { fn from(value: SerdeAlbumDate) -> Self { diff --git a/tests/database/json.rs b/tests/database/json.rs index da96d9e..9e13334 100644 --- a/tests/database/json.rs +++ b/tests/database/json.rs @@ -43,7 +43,7 @@ fn save() { #[test] fn load() { let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE); - let database = JsonDatabase::new(backend); + let mut database = JsonDatabase::new(backend); let read_data: Vec = database.load().unwrap(); diff --git a/tests/database/sql.rs b/tests/database/sql.rs index ccdedb9..ea7461a 100644 --- a/tests/database/sql.rs +++ b/tests/database/sql.rs @@ -3,64 +3,61 @@ use std::{fs, path::PathBuf}; use once_cell::sync::Lazy; use musichoard::{ + collection::{artist::Artist, Collection}, external::database::sql::{backend::SqlDatabaseSqliteBackend, SqlDatabase}, interface::database::IDatabase, }; +use tempfile::NamedTempFile; use crate::testlib::COLLECTION; pub static DATABASE_TEST_FILE: Lazy = Lazy::new(|| fs::canonicalize("./tests/files/database/database.db").unwrap()); -// fn expected() -> Collection { -// let mut expected = COLLECTION.to_owned(); -// for artist in expected.iter_mut() { -// for album in artist.albums.iter_mut() { -// album.tracks.clear(); -// } -// } -// expected -// } +fn expected() -> Collection { + let mut expected = COLLECTION.to_owned(); + for artist in expected.iter_mut() { + for album in artist.albums.iter_mut() { + album.tracks.clear(); + } + } + expected +} #[test] fn save() { - // let file = NamedTempFile::new().unwrap(); + let file = NamedTempFile::new().unwrap(); - let backend = SqlDatabaseSqliteBackend::new(&*DATABASE_TEST_FILE).unwrap(); + let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap(); let mut database = SqlDatabase::new(backend).unwrap(); let write_data = COLLECTION.to_owned(); database.save(&write_data).unwrap(); - - // let expected = fs::read_to_string(&*DATABASE_TEST_FILE).unwrap(); - // let actual = fs::read_to_string(file.path()).unwrap(); - - // assert_eq!(actual, expected); } -// #[test] -// fn load() { -// let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE); -// let database = JsonDatabase::new(backend); +#[test] +fn load() { + let backend = SqlDatabaseSqliteBackend::new(&*DATABASE_TEST_FILE).unwrap(); + let mut database = SqlDatabase::new(backend).unwrap(); -// let read_data: Vec = database.load().unwrap(); + let read_data: Vec = database.load().unwrap(); -// let expected = expected(); -// assert_eq!(read_data, expected); -// } + let expected = expected(); + assert_eq!(read_data, expected); +} -// #[test] -// fn reverse() { -// let file = NamedTempFile::new().unwrap(); +#[test] +fn reverse() { + let file = NamedTempFile::new().unwrap(); -// let backend = JsonDatabaseFileBackend::new(file.path()); -// let mut database = JsonDatabase::new(backend); + let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap(); + let mut database = SqlDatabase::new(backend).unwrap(); -// let write_data = COLLECTION.to_owned(); -// database.save(&write_data).unwrap(); -// let read_data: Vec = database.load().unwrap(); + let write_data = COLLECTION.to_owned(); + database.save(&write_data).unwrap(); + let read_data: Vec = database.load().unwrap(); -// // Album data is not saved into database. -// let expected = expected(); -// assert_eq!(read_data, expected); -// } + // Not all data is saved into database. + let expected = expected(); + assert_eq!(read_data, expected); +} diff --git a/tests/files/database/database.db b/tests/files/database/database.db new file mode 100644 index 0000000..9fe47ad Binary files /dev/null and b/tests/files/database/database.db differ