Add a SQLite database backend #265

Merged
wojtek merged 20 commits from 248---replace-json-file-as-a-database-with-sqlite into main 2025-01-12 10:24:53 +01:00
4 changed files with 222 additions and 57 deletions
Showing only changes of commit 50dd1e3730 - Show all commits

View File

@ -14,7 +14,7 @@ pub enum AlbumLibIdDef {
} }
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct SerdeAlbumLibId(#[serde(with = "AlbumLibIdDef")] AlbumLibId); pub struct SerdeAlbumLibId(#[serde(with = "AlbumLibIdDef")] pub AlbumLibId);
impl From<SerdeAlbumLibId> for AlbumLibId { impl From<SerdeAlbumLibId> for AlbumLibId {
fn from(value: SerdeAlbumLibId) -> Self { fn from(value: SerdeAlbumLibId) -> Self {
@ -70,7 +70,7 @@ pub enum AlbumPrimaryTypeDef {
} }
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct SerdeAlbumPrimaryType(#[serde(with = "AlbumPrimaryTypeDef")] AlbumPrimaryType); pub struct SerdeAlbumPrimaryType(#[serde(with = "AlbumPrimaryTypeDef")] pub AlbumPrimaryType);
impl From<SerdeAlbumPrimaryType> for AlbumPrimaryType { impl From<SerdeAlbumPrimaryType> for AlbumPrimaryType {
fn from(value: SerdeAlbumPrimaryType) -> Self { fn from(value: SerdeAlbumPrimaryType) -> Self {
@ -102,7 +102,7 @@ pub enum AlbumSecondaryTypeDef {
} }
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct SerdeAlbumSecondaryType(#[serde(with = "AlbumSecondaryTypeDef")] AlbumSecondaryType); pub struct SerdeAlbumSecondaryType(#[serde(with = "AlbumSecondaryTypeDef")] pub AlbumSecondaryType);
impl From<SerdeAlbumSecondaryType> for AlbumSecondaryType { impl From<SerdeAlbumSecondaryType> for AlbumSecondaryType {
fn from(value: SerdeAlbumSecondaryType) -> Self { fn from(value: SerdeAlbumSecondaryType) -> Self {

View File

@ -51,10 +51,12 @@ pub struct DeserializeAlbum {
} }
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct DeserializeMbRefOption(#[serde(with = "MbRefOptionDef")] MbRefOption<DeserializeMbid>); pub struct DeserializeMbRefOption(
#[serde(with = "MbRefOptionDef")] pub MbRefOption<DeserializeMbid>,
);
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DeserializeMbid(Mbid); pub struct DeserializeMbid(pub Mbid);
macro_rules! impl_from_for_mb_ref_option { macro_rules! impl_from_for_mb_ref_option {
($ref:ty) => { ($ref:ty) => {

View File

@ -198,19 +198,21 @@ impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase<SDB>
} }
} }
// #[cfg(test)] #[cfg(test)]
// pub mod testmod; pub mod testmod;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fs;
use std::collections::VecDeque; use std::collections::VecDeque;
use mockall::{predicate, Sequence}; use mockall::{predicate, Sequence};
use rusqlite::Connection;
use crate::core::{collection::Collection, testmod::FULL_COLLECTION}; use crate::{
core::{collection::Collection, testmod::FULL_COLLECTION},
external::database::sql::testmod::{
DATABASE_SQL_ALBUMS, DATABASE_SQL_ARTISTS, DATABASE_SQL_VERSION,
},
};
use super::*; use super::*;
@ -323,39 +325,21 @@ mod tests {
#[test] #[test]
fn load() { fn load() {
let path = fs::canonicalize("./src/external/database/sql/testmod.db").unwrap();
let db = Connection::open(path).unwrap();
let mut tx = MockISqlTransactionBackend::new(); let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new(); let mut seq = Sequence::new();
let version_query = "SELECT value FROM database_metadata WHERE name = 'version'"; then!(tx, seq, expect_select_database_version)
let version: String = db.query_row(&version_query, (), |row| row.get(0)).unwrap(); .return_once(|| Ok(Some(DATABASE_SQL_VERSION.to_string())));
then!(tx, seq, expect_select_database_version).return_once(|| Ok(Some(version)));
let artist_query = "SELECT name, sort, mbid, properties FROM artists"; let de_artists = DATABASE_SQL_ARTISTS.to_owned();
let mut artist_query = db.prepare(&artist_query).unwrap();
let mut artist_rows = artist_query.query(()).unwrap();
let mut de_artists = vec![];
while let Some(row) = artist_rows.next().unwrap() {
de_artists.push(row.try_into().unwrap());
}
let artists: Collection = de_artists.iter().cloned().map(Into::into).collect(); let artists: Collection = de_artists.iter().cloned().map(Into::into).collect();
then!(tx, seq, expect_select_all_artists).return_once(|| Ok(de_artists)); then!(tx, seq, expect_select_all_artists).return_once(|| Ok(de_artists));
for artist in artists.iter() { for artist in artists.iter() {
let album_query = let de_albums = DATABASE_SQL_ALBUMS.get(&artist.meta.id.name).unwrap();
"SELECT title, lib_id, year, month, day, seq, mbid, primary_type, secondary_types
FROM albums WHERE artist_name = ?1";
let mut album_query = db.prepare(&album_query).unwrap();
let mut album_rows = album_query.query([artist.meta.id.name.clone()]).unwrap();
let mut de_albums = vec![];
while let Some(row) = album_rows.next().unwrap() {
de_albums.push(row.try_into().unwrap());
}
then!(tx, seq, expect_select_artist_albums) then!(tx, seq, expect_select_artist_albums)
.with(predicate::eq(artist.meta.id.name.clone())) .with(predicate::eq(artist.meta.id.name.clone()))
.return_once(|_| Ok(de_albums)); .return_once(|_| Ok(de_albums.to_owned()));
} }
then0!(tx, seq, expect_commit); then0!(tx, seq, expect_commit);
@ -366,29 +350,6 @@ mod tests {
assert_eq!(read_data, expected); assert_eq!(read_data, expected);
} }
// #[test]
// fn reverse() {
// let input = DATABASE_JSON.to_owned();
// let result = Ok(input.clone());
// let mut backend = MockISqlDatabaseBackend::new();
// backend
// .expect_write()
// .with(predicate::eq(input))
// .times(1)
// .return_once(|_| Ok(()));
// backend.expect_read().times(1).return_once(|| result);
// let mut database = SqlDatabase::new(backend);
// let write_data = FULL_COLLECTION.to_owned();
// database.save(&write_data).unwrap();
// let read_data: Vec<Artist> = database.load().unwrap();
// // Album information is not saved to disk.
// let expected = expected();
// assert_eq!(read_data, expected);
// }
// #[test] // #[test]
// fn load_errors() { // fn load_errors() {
// let json = String::from(""); // let json = String::from("");

View File

@ -0,0 +1,202 @@
use std::collections::HashMap;
use once_cell::sync::Lazy;
use crate::{
collection::{
album::{AlbumDate, AlbumLibId, AlbumPrimaryType},
musicbrainz::MbRefOption,
},
external::database::serde::{
common::{SerdeAlbumDate, SerdeAlbumLibId, SerdeAlbumPrimaryType},
deserialize::{
DeserializeAlbum, DeserializeArtist, DeserializeMbRefOption, DeserializeMbid,
},
},
};
pub static DATABASE_SQL_VERSION: &str = "V20250103";
pub static DATABASE_SQL_ARTISTS: Lazy<Vec<DeserializeArtist>> = Lazy::new(|| {
vec![
DeserializeArtist {
name: String::from("Album_Artist A"),
sort: None,
musicbrainz: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"00000000-0000-0000-0000-000000000000".try_into().unwrap(),
))),
properties: HashMap::from([
(
String::from("MusicButler"),
vec![String::from(
"https://www.musicbutler.io/artist-page/000000000",
)],
),
(
String::from("Qobuz"),
vec![String::from(
"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums",
)],
),
]),
albums: vec![],
},
DeserializeArtist {
name: String::from("Album_Artist B"),
sort: None,
musicbrainz: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"11111111-1111-1111-1111-111111111111".try_into().unwrap(),
))),
properties: HashMap::from([
(String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/111111111"),
String::from("https://www.musicbutler.io/artist-page/111111112"),
]),
(String::from("Bandcamp"), vec![
String::from("https://artist-b.bandcamp.com/")
]),
(String::from("Qobuz"), vec![
String::from(
"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums",
)
]),
]),
albums: vec![],
},
DeserializeArtist {
name: String::from("The Album_Artist C"),
sort: Some(String::from("Album_Artist C, The")),
musicbrainz: DeserializeMbRefOption(MbRefOption::CannotHaveMbid),
properties: HashMap::new(),
albums: vec![],
},
DeserializeArtist {
name: String::from("Album_Artist D"),
sort: None,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
properties: HashMap::new(),
albums: vec![],
},
]
});
pub static DATABASE_SQL_ALBUMS: Lazy<HashMap<String, Vec<DeserializeAlbum>>> = Lazy::new(|| {
HashMap::from([
(
String::from("Album_Artist A"),
vec![
DeserializeAlbum {
title: String::from("album_title a.a"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(1)),
date: SerdeAlbumDate(AlbumDate::new(Some(1998), None, None)),
seq: 1,
musicbrainz: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"00000000-0000-0000-0000-000000000000".try_into().unwrap(),
))),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![],
},
DeserializeAlbum {
title: String::from("album_title a.b"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(2)),
date: SerdeAlbumDate(AlbumDate::new(Some(2015), Some(4), None)),
seq: 1,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![],
},
],
),
(
String::from("Album_Artist B"),
vec![
DeserializeAlbum {
title: String::from("album_title b.a"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(3)),
date: SerdeAlbumDate(AlbumDate::new(Some(2003), Some(6), Some(6))),
seq: 1,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![],
},
DeserializeAlbum {
title: String::from("album_title b.b"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(4)),
date: SerdeAlbumDate(AlbumDate::new(Some(2008), None, None)),
seq: 3,
musicbrainz: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"11111111-1111-1111-1111-111111111111".try_into().unwrap(),
))),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![],
},
DeserializeAlbum {
title: String::from("album_title b.c"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(5)),
date: SerdeAlbumDate(AlbumDate::new(Some(2009), None, None)),
seq: 2,
musicbrainz: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"11111111-1111-1111-1111-111111111112".try_into().unwrap(),
))),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![],
},
DeserializeAlbum {
title: String::from("album_title b.d"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(6)),
date: SerdeAlbumDate(AlbumDate::new(Some(2015), None, None)),
seq: 4,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![],
},
],
),
(
String::from("The Album_Artist C"),
vec![
DeserializeAlbum {
title: String::from("album_title c.a"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(7)),
date: SerdeAlbumDate(AlbumDate::new(Some(1985), None, None)),
seq: 0,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![],
},
DeserializeAlbum {
title: String::from("album_title c.b"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(8)),
date: SerdeAlbumDate(AlbumDate::new(Some(2018), None, None)),
seq: 0,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![],
},
],
),
(
String::from("Album_Artist D"),
vec![
DeserializeAlbum {
title: String::from("album_title d.a"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(9)),
date: SerdeAlbumDate(AlbumDate::new(Some(1995), None, None)),
seq: 0,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![],
},
DeserializeAlbum {
title: String::from("album_title d.b"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(10)),
date: SerdeAlbumDate(AlbumDate::new(Some(2028), None, None)),
seq: 0,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![],
},
],
),
])
});