Complete implementation
Some checks failed
Cargo CI / Build and Test (pull_request) Failing after 2m59s
Cargo CI / Lint (pull_request) Failing after 1m4s

This commit is contained in:
Wojciech Kozlowski 2025-01-11 15:47:07 +01:00
parent a9782b74cc
commit 0fe6fe0be1
10 changed files with 185 additions and 64 deletions

View File

@ -11,7 +11,7 @@ use crate::core::collection::{self, Collection};
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IDatabase { pub trait IDatabase {
/// Load collection from the database. /// Load collection from the database.
fn load(&self) -> Result<Collection, LoadError>; fn load(&mut self) -> Result<Collection, LoadError>;
/// Save collection to the database. /// Save collection to the database.
fn save(&mut self, collection: &Collection) -> Result<(), SaveError>; fn save(&mut self, collection: &Collection) -> Result<(), SaveError>;
@ -21,7 +21,7 @@ pub trait IDatabase {
pub struct NullDatabase; pub struct NullDatabase;
impl IDatabase for NullDatabase { impl IDatabase for NullDatabase {
fn load(&self) -> Result<Collection, LoadError> { fn load(&mut self) -> Result<Collection, LoadError> {
Ok(vec![]) Ok(vec![])
} }
@ -100,7 +100,7 @@ mod tests {
#[test] #[test]
fn null_database_load() { fn null_database_load() {
let database = NullDatabase; let mut database = NullDatabase;
assert!(database.load().unwrap().is_empty()); assert!(database.load().unwrap().is_empty());
} }

View File

@ -49,7 +49,7 @@ impl<JDB: IJsonDatabaseBackend> JsonDatabase<JDB> {
} }
impl<JDB: IJsonDatabaseBackend> IDatabase for JsonDatabase<JDB> { impl<JDB: IJsonDatabaseBackend> IDatabase for JsonDatabase<JDB> {
fn load(&self) -> Result<Collection, LoadError> { fn load(&mut self) -> Result<Collection, LoadError> {
let serialized = self.backend.read()?; let serialized = self.backend.read()?;
let database: DeserializeDatabase = serde_json::from_str(&serialized)?; let database: DeserializeDatabase = serde_json::from_str(&serialized)?;
Ok(database.into()) Ok(database.into())

View File

@ -32,22 +32,22 @@ impl From<DeserializeDatabase> for Collection {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct DeserializeArtist { pub struct DeserializeArtist {
name: String, pub name: String,
sort: Option<String>, pub sort: Option<String>,
musicbrainz: DeserializeMbRefOption, pub musicbrainz: DeserializeMbRefOption,
properties: HashMap<String, Vec<String>>, pub properties: HashMap<String, Vec<String>>,
albums: Vec<DeserializeAlbum>, pub albums: Vec<DeserializeAlbum>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct DeserializeAlbum { pub struct DeserializeAlbum {
title: String, pub title: String,
lib_id: SerdeAlbumLibId, pub lib_id: SerdeAlbumLibId,
date: SerdeAlbumDate, pub date: SerdeAlbumDate,
seq: u8, pub seq: u8,
musicbrainz: DeserializeMbRefOption, pub musicbrainz: DeserializeMbRefOption,
primary_type: Option<SerdeAlbumPrimaryType>, pub primary_type: Option<SerdeAlbumPrimaryType>,
secondary_types: Vec<SerdeAlbumSecondaryType>, pub secondary_types: Vec<SerdeAlbumSecondaryType>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -110,6 +110,8 @@ impl<'de> Deserialize<'de> for DeserializeMbid {
impl From<DeserializeArtist> for Artist { impl From<DeserializeArtist> for Artist {
fn from(artist: DeserializeArtist) -> Self { fn from(artist: DeserializeArtist) -> Self {
let mut albums: Vec<Album> = artist.albums.into_iter().map(Into::into).collect();
albums.sort_unstable();
Artist { Artist {
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId { id: ArtistId {
@ -121,7 +123,7 @@ impl From<DeserializeArtist> for Artist {
properties: artist.properties, properties: artist.properties,
}, },
}, },
albums: artist.albums.into_iter().map(Into::into).collect(), albums,
} }
} }
} }

View File

@ -1,5 +1,5 @@
//! Helper module for backends that can use serde for (de)serialisation. //! Helper module for backends that can use serde for (de)serialisation.
mod common; pub mod common;
pub mod deserialize; pub mod deserialize;
pub mod serialize; pub mod serialize;

View File

@ -2,11 +2,20 @@
use std::path::PathBuf; 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::{ use crate::{
serde::serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase}, collection::album::AlbumDate,
external::database::{
serde::{
common::SerdeAlbumDate,
deserialize::{DeserializeAlbum, DeserializeArtist, DeserializeDatabase},
serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase},
},
sql::{Error, ISqlDatabaseBackend, ISqlTransactionBackend}, sql::{Error, ISqlDatabaseBackend, ISqlTransactionBackend},
},
}; };
/// SQLite database backend that uses SQLite as the implementation. /// SQLite database backend that uses SQLite as the implementation.
@ -45,6 +54,21 @@ impl<'conn> SqlTransactionSqliteBackend<'conn> {
.map(|_| ()) .map(|_| ())
.map_err(|err| Error::ExecError(err.to_string())) .map_err(|err| Error::ExecError(err.to_string()))
} }
fn query<'s, P: Params>(stmt: &'s mut Statement, params: P) -> Result<Rows<'s>, Error> {
stmt.query(params)
.map_err(|err| Error::ExecError(err.to_string()))
}
fn next_row<'r, 's>(rows: &'r mut Rows<'s>) -> Result<Option<&'r Row<'s>>, Error> {
rows.next()
.map_err(|err| Error::SerDeError(err.to_string()))
}
fn get_value<T: FromSql>(row: &Row<'_>, idx: usize) -> Result<T, Error> {
row.get(idx)
.map_err(|err| Error::SerDeError(err.to_string()))
}
} }
impl From<serde_json::Error> for Error { impl From<serde_json::Error> for Error {
@ -116,7 +140,7 @@ impl<'conn> ISqlTransactionBackend for SqlTransactionSqliteBackend<'conn> {
seq INT NOT NULL, seq INT NOT NULL,
primary_type JSON NOT NULL DEFAULT 'null', primary_type JSON NOT NULL DEFAULT 'null',
secondary_types JSON NOT NULL DEFAULT '[]', 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, ()) Self::execute(&mut stmt, ())
@ -138,6 +162,25 @@ impl<'conn> ISqlTransactionBackend for SqlTransactionSqliteBackend<'conn> {
Self::execute(&mut stmt, ("version", version)) Self::execute(&mut stmt, ("version", version))
} }
fn select_database_version<'a>(&self) -> Result<DeserializeDatabase, Error> {
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> { fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<(), Error> {
let mut stmt = self.prepare_cached( let mut stmt = self.prepare_cached(
"INSERT INTO artists (name, sort, mbid, properties) "INSERT INTO artists (name, sort, mbid, properties)
@ -154,6 +197,24 @@ impl<'conn> ISqlTransactionBackend for SqlTransactionSqliteBackend<'conn> {
) )
} }
fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, 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::<String>(row, 2)?)?,
properties: serde_json::from_str(&Self::get_value::<String>(row, 3)?)?,
albums: vec![],
});
}
Ok(artists)
}
fn insert_album<'a>(&self, artist_name: &str, album: &SerializeAlbum<'a>) -> Result<(), Error> { fn insert_album<'a>(&self, artist_name: &str, album: &SerializeAlbum<'a>) -> Result<(), Error> {
let mut stmt = self.prepare_cached( let mut stmt = self.prepare_cached(
"INSERT INTO albums (title, lib_id, mbid, artist_name, "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<Vec<DeserializeAlbum>, 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::<String>(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::<String>(row, 6)?)?,
primary_type: serde_json::from_str(&Self::get_value::<String>(row, 7)?)?,
secondary_types: serde_json::from_str(&Self::get_value::<String>(row, 8)?)?,
});
}
Ok(albums)
}
} }

View File

@ -12,7 +12,10 @@ use crate::{
collection::Collection, collection::Collection,
interface::database::{IDatabase, LoadError, SaveError}, 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. /// Trait for the SQL database backend.
@ -50,11 +53,20 @@ pub trait ISqlTransactionBackend {
/// Set the database version. /// Set the database version.
fn insert_database_version<'a>(&self, version: &SerializeDatabase<'a>) -> Result<(), Error>; fn insert_database_version<'a>(&self, version: &SerializeDatabase<'a>) -> Result<(), Error>;
/// Get the database version.
fn select_database_version<'a>(&self) -> Result<DeserializeDatabase, Error>;
/// Insert an artist into the artist table. /// Insert an artist into the artist table.
fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<(), Error>; fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<(), Error>;
/// Get all artists from the artist table.
fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, Error>;
/// Insert an artist into the artist table. /// Insert an artist into the artist table.
fn insert_album<'a>(&self, artist_name: &str, album: &SerializeAlbum<'a>) -> Result<(), Error>; 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<Vec<DeserializeAlbum>, Error>;
} }
/// Errors for SQL database backend. /// Errors for SQL database backend.
@ -95,6 +107,15 @@ impl From<Error> for SaveError {
} }
} }
impl From<Error> for LoadError {
fn from(value: Error) -> Self {
match value {
Error::SerDeError(s) => LoadError::SerDeError(s),
_ => LoadError::IoError(value.to_string()),
}
}
}
/// SQL database. /// SQL database.
pub struct SqlDatabase<SDB> { pub struct SqlDatabase<SDB> {
backend: SDB, backend: SDB,
@ -127,8 +148,21 @@ impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> SqlDatabase<SDB> {
} }
impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase<SDB> { impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase<SDB> {
fn load(&self) -> Result<Collection, LoadError> { fn load(&mut self) -> Result<Collection, LoadError> {
Ok(vec![]) 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> { fn save(&mut self, collection: &Collection) -> Result<(), SaveError> {

View File

@ -227,7 +227,7 @@ impl<'de> Deserialize<'de> for SerdeMbid {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SerdeAlbumDate(AlbumDate); pub struct SerdeAlbumDate(pub AlbumDate);
impl From<SerdeAlbumDate> for AlbumDate { impl From<SerdeAlbumDate> for AlbumDate {
fn from(value: SerdeAlbumDate) -> Self { fn from(value: SerdeAlbumDate) -> Self {

View File

@ -43,7 +43,7 @@ fn save() {
#[test] #[test]
fn load() { fn load() {
let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE); let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE);
let database = JsonDatabase::new(backend); let mut database = JsonDatabase::new(backend);
let read_data: Vec<Artist> = database.load().unwrap(); let read_data: Vec<Artist> = database.load().unwrap();

View File

@ -3,64 +3,61 @@ use std::{fs, path::PathBuf};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use musichoard::{ use musichoard::{
collection::{artist::Artist, Collection},
external::database::sql::{backend::SqlDatabaseSqliteBackend, SqlDatabase}, external::database::sql::{backend::SqlDatabaseSqliteBackend, SqlDatabase},
interface::database::IDatabase, interface::database::IDatabase,
}; };
use tempfile::NamedTempFile;
use crate::testlib::COLLECTION; use crate::testlib::COLLECTION;
pub static DATABASE_TEST_FILE: Lazy<PathBuf> = pub static DATABASE_TEST_FILE: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/database/database.db").unwrap()); Lazy::new(|| fs::canonicalize("./tests/files/database/database.db").unwrap());
// fn expected() -> Collection { fn expected() -> Collection {
// let mut expected = COLLECTION.to_owned(); let mut expected = COLLECTION.to_owned();
// for artist in expected.iter_mut() { for artist in expected.iter_mut() {
// for album in artist.albums.iter_mut() { for album in artist.albums.iter_mut() {
// album.tracks.clear(); album.tracks.clear();
// } }
// } }
// expected expected
// } }
#[test] #[test]
fn save() { 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 mut database = SqlDatabase::new(backend).unwrap();
let write_data = COLLECTION.to_owned(); let write_data = COLLECTION.to_owned();
database.save(&write_data).unwrap(); 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] #[test]
// fn load() { fn load() {
// let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE); let backend = SqlDatabaseSqliteBackend::new(&*DATABASE_TEST_FILE).unwrap();
// let database = JsonDatabase::new(backend); let mut database = SqlDatabase::new(backend).unwrap();
// let read_data: Vec<Artist> = database.load().unwrap(); let read_data: Vec<Artist> = database.load().unwrap();
// let expected = expected(); let expected = expected();
// assert_eq!(read_data, expected); assert_eq!(read_data, expected);
// } }
// #[test] #[test]
// fn reverse() { fn reverse() {
// let file = NamedTempFile::new().unwrap(); let file = NamedTempFile::new().unwrap();
// let backend = JsonDatabaseFileBackend::new(file.path()); let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
// let mut database = JsonDatabase::new(backend); let mut database = SqlDatabase::new(backend).unwrap();
// let write_data = COLLECTION.to_owned(); let write_data = COLLECTION.to_owned();
// database.save(&write_data).unwrap(); database.save(&write_data).unwrap();
// let read_data: Vec<Artist> = database.load().unwrap(); let read_data: Vec<Artist> = database.load().unwrap();
// // Album data is not saved into database. // Not all data is saved into database.
// let expected = expected(); let expected = expected();
// assert_eq!(read_data, expected); assert_eq!(read_data, expected);
// } }

Binary file not shown.