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
5 changed files with 445 additions and 13 deletions
Showing only changes of commit 02fdef444f - Show all commits

View File

@ -1,4 +1,7 @@
#[cfg(feature = "database-json")]
pub mod json;
#[cfg(feature = "database-sqlite")]
pub mod sql;
#[cfg(feature = "database-json")]
mod serde;

View File

@ -37,7 +37,7 @@ pub struct AlbumDateDef {
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SerdeAlbumDate(#[serde(with = "AlbumDateDef")] AlbumDate);
pub struct SerdeAlbumDate(#[serde(with = "AlbumDateDef")] pub AlbumDate);
impl From<SerdeAlbumDate> for AlbumDate {
fn from(value: SerdeAlbumDate) -> Self {

View File

@ -24,22 +24,22 @@ impl<'a> From<&'a Collection> for SerializeDatabase<'a> {
#[derive(Debug, Serialize)]
pub struct SerializeArtist<'a> {
name: &'a str,
sort: Option<&'a str>,
musicbrainz: SerializeMbRefOption<'a>,
properties: BTreeMap<&'a str, &'a Vec<String>>,
albums: Vec<SerializeAlbum<'a>>,
pub name: &'a str,
pub sort: Option<&'a str>,
pub musicbrainz: SerializeMbRefOption<'a>,
pub properties: BTreeMap<&'a str, &'a Vec<String>>,
pub albums: Vec<SerializeAlbum<'a>>,
}
#[derive(Debug, Serialize)]
pub struct SerializeAlbum<'a> {
title: &'a str,
lib_id: SerdeAlbumLibId,
date: SerdeAlbumDate,
seq: u8,
musicbrainz: SerializeMbRefOption<'a>,
primary_type: Option<SerdeAlbumPrimaryType>,
secondary_types: Vec<SerdeAlbumSecondaryType>,
pub title: &'a str,
pub lib_id: SerdeAlbumLibId,
pub date: SerdeAlbumDate,
pub seq: u8,
pub musicbrainz: SerializeMbRefOption<'a>,
pub primary_type: Option<SerdeAlbumPrimaryType>,
pub secondary_types: Vec<SerdeAlbumSecondaryType>,
}
#[derive(Debug, Serialize)]

View File

@ -0,0 +1,166 @@
//! Module for storing MusicHoard data in a JSON file database.
use std::path::PathBuf;
use rusqlite::{self, CachedStatement, Connection, Params, Statement};
use crate::external::database::{
serde::serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase},
sql::{Error, ISqlDatabaseBackend},
};
/// SQLite database backend that uses SQLite as the implementation.
pub struct SqlDatabaseSqliteBackend {
conn: Connection,
}
impl SqlDatabaseSqliteBackend {
/// Create a [`SqlDatabaseSqliteBackend`] that will read/write to the provided database.
pub fn new<P: Into<PathBuf>>(path: P) -> Result<Self, Error> {
Ok(SqlDatabaseSqliteBackend {
conn: Connection::open(path.into()).map_err(|err| Error::OpenError(err.to_string()))?,
})
}
fn prepare(&self, sql: &str) -> Result<Statement, Error> {
self.conn
.prepare(sql)
.map_err(|err| Error::StmtError(err.to_string()))
}
fn prepare_cached(&self, sql: &str) -> Result<CachedStatement, Error> {
self.conn
.prepare_cached(sql)
.map_err(|err| Error::StmtError(err.to_string()))
}
fn execute<P: Params>(stmt: &mut Statement, params: P) -> Result<(), Error> {
stmt.execute(params)
.map(|_| ())
.map_err(|err| Error::ExecError(err.to_string()))
}
}
impl From<serde_json::Error> for Error {
fn from(value: serde_json::Error) -> Self {
Error::SerDeError(value.to_string())
}
}
impl ISqlDatabaseBackend for SqlDatabaseSqliteBackend {
fn begin_transaction(&self) -> Result<(), Error> {
let mut stmt = self.prepare_cached("BEGIN TRANSACTION;")?;
Self::execute(&mut stmt, ())
}
fn commit_transaction(&self) -> Result<(), Error> {
let mut stmt = self.prepare_cached("COMMIT;")?;
Self::execute(&mut stmt, ())
}
fn create_database_metadata_table(&self) -> Result<(), Error> {
let mut stmt = self.prepare(
"CREATE TABLE IF NOT EXISTS database_metadata (
name TEXT NOT NULL PRIMARY KEY,
value TEXT NOT NULL
)",
)?;
Self::execute(&mut stmt, ())
}
fn drop_database_metadata_table(&self) -> Result<(), Error> {
let mut stmt = self.prepare_cached("DROP TABLE database_metadata")?;
Self::execute(&mut stmt, ())
}
fn create_artists_table(&self) -> Result<(), Error> {
let mut stmt = self.prepare(
"CREATE TABLE IF NOT EXISTS artists (
name TEXT NOT NULL PRIMARY KEY,
sort TEXT NULL,
mbid JSON NOT NULL DEFAULT '\"None\"',
properties JSON NOT NULL DEFAULT '{}'
)",
)?;
Self::execute(&mut stmt, ())
}
fn drop_artists_table(&self) -> Result<(),Error> {
let mut stmt = self.prepare_cached("DROP TABLE artists")?;
Self::execute(&mut stmt, ())
}
fn create_albums_table(&self) -> Result<(), Error> {
let mut stmt = self.prepare(
"CREATE TABLE IF NOT EXISTS albums (
title TEXT NOT NULL PRIMARY KEY,
lib_id JSON NOT NULL DEFAULT '\"None\"',
mbid JSON NOT NULL DEFAULT '\"None\"',
artist_name TEXT NOT NULL,
year INT NULL,
month INT NULL,
day INT NULL,
seq INT NOT NULL,
primary_type JSON NOT NULL DEFAULT 'null',
secondary_types JSON NOT NULL DEFAULT '[]',
FOREIGN KEY (artist_name) REFERENCES artists(name)
)",
)?;
Self::execute(&mut stmt, ())
}
fn drop_albums_table(&self) -> Result<(),Error> {
let mut stmt = self.prepare_cached("DROP TABLE albums")?;
Self::execute(&mut stmt, ())
}
fn insert_database_version<'a>(&self, version: &SerializeDatabase<'a>) -> Result<(), Error> {
let mut stmt = self.prepare_cached(
"INSERT INTO database_metadata (name, value)
VALUES (?1, ?2)",
)?;
let version = match version {
SerializeDatabase::V20250103(_) => "V20250103",
};
Self::execute(&mut stmt, ("version", version))
}
fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<(), Error> {
let mut stmt = self.prepare_cached(
"INSERT INTO artists (name, sort, mbid, properties)
VALUES (?1, ?2, ?3, ?4)",
)?;
Self::execute(
&mut stmt,
(
artist.name,
artist.sort,
serde_json::to_string(&artist.musicbrainz)?,
serde_json::to_string(&artist.properties)?,
),
)
}
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,
year, month, day, seq, primary_type, secondary_types)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
)?;
Self::execute(
&mut stmt,
(
album.title,
serde_json::to_string(&album.lib_id)?,
serde_json::to_string(&album.musicbrainz)?,
artist_name,
album.date.0.year,
album.date.0.month,
album.date.0.day,
album.seq,
serde_json::to_string(&album.primary_type)?,
serde_json::to_string(&album.secondary_types)?,
),
)
}
}

View File

@ -0,0 +1,263 @@
//! Module for storing MusicHoard data in a SQLdatabase.
pub mod backend;
use std::fmt;
#[cfg(test)]
use mockall::automock;
use crate::{
core::{
collection::Collection,
interface::database::{IDatabase, LoadError, SaveError},
},
external::database::serde::serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase},
};
/// Trait for the SQL database backend.
#[cfg_attr(test, automock)]
pub trait ISqlDatabaseBackend {
/// Begin an SQL transaction.
fn begin_transaction(&self) -> Result<(), Error>;
/// Commit ongoing transaction.
fn commit_transaction(&self) -> Result<(), Error>;
/// Create the database metadata table (if needed).
fn create_database_metadata_table(&self) -> Result<(), Error>;
/// Drop the database metadata table.
fn drop_database_metadata_table(&self) -> Result<(), Error>;
/// Create the artists table (if needed).
fn create_artists_table(&self) -> Result<(), Error>;
/// Drop the artists table.
fn drop_artists_table(&self) -> Result<(), Error>;
/// Create the albums table (if needed).
fn create_albums_table(&self) -> Result<(), Error>;
/// Drop the albums table.
fn drop_albums_table(&self) -> Result<(), Error>;
/// Set the database version.
fn insert_database_version<'a>(&self, version: &SerializeDatabase<'a>) -> Result<(), Error>;
/// Insert an artist into the artist table.
fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<(), Error>;
/// Insert an artist into the artist table.
fn insert_album<'a>(&self, artist_name: &str, album: &SerializeAlbum<'a>) -> Result<(), Error>;
}
/// Errors for SQL database backend.
#[derive(Debug)]
pub enum Error {
/// An error occurred when connecting to the database.
OpenError(String),
/// An error occurred when preparing a statement for execution.
StmtError(String),
/// An error occurred during serialisation.
SerDeError(String),
/// An error occurred during execution.
ExecError(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::OpenError(ref s) => {
write!(f, "an error occurred when connecting to the database: {s}")
}
Self::StmtError(ref s) => write!(
f,
"an error occurred when preparing a statement for execution: {s}"
),
Self::SerDeError(ref s) => write!(f, "an error occurred during serialisation : {s}"),
Self::ExecError(ref s) => write!(f, "an error occurred during execution: {s}"),
}
}
}
impl From<Error> for SaveError {
fn from(value: Error) -> Self {
match value {
Error::SerDeError(s) => SaveError::SerDeError(s),
_ => SaveError::IoError(value.to_string()),
}
}
}
/// SQL database.
pub struct SqlDatabase<SDB> {
backend: SDB,
}
impl<SDB: ISqlDatabaseBackend> SqlDatabase<SDB> {
/// Create a new SQL database with the provided backend, e.g.
/// [`backend::SqlDatabaseSqliteBackend`].
pub fn new(backend: SDB) -> Result<Self, Error> {
let db = SqlDatabase { backend };
db.begin_transaction()?;
db.create_tables()?;
db.commit_transaction()?;
Ok(db)
}
fn begin_transaction(&self) -> Result<(), Error> {
self.backend.begin_transaction()
}
fn commit_transaction(&self) -> Result<(), Error> {
self.backend.commit_transaction()
}
fn create_tables(&self) -> Result<(), Error> {
self.backend.create_database_metadata_table()?;
self.backend.create_artists_table()?;
self.backend.create_albums_table()?;
Ok(())
}
fn drop_tables(&self) -> Result<(), Error> {
self.backend.drop_database_metadata_table()?;
self.backend.drop_artists_table()?;
self.backend.drop_albums_table()?;
Ok(())
}
}
impl<SDB: ISqlDatabaseBackend> IDatabase for SqlDatabase<SDB> {
fn load(&self) -> Result<Collection, LoadError> {
Ok(vec![])
}
fn save(&mut self, collection: &Collection) -> Result<(), SaveError> {
let database: SerializeDatabase = collection.into();
self.begin_transaction()?;
self.drop_tables()?;
self.create_tables()?;
self.backend.insert_database_version(&database)?;
match database {
SerializeDatabase::V20250103(artists) => {
for artist in artists.iter() {
self.backend.insert_artist(artist)?;
for album in artist.albums.iter() {
self.backend.insert_album(artist.name, album)?;
}
}
}
}
self.commit_transaction()?;
Ok(())
}
}
// #[cfg(test)]
// pub mod testmod;
// #[cfg(test)]
// mod tests {
// use std::collections::HashMap;
// use mockall::predicate;
// use crate::core::{
// collection::{artist::Artist, Collection},
// testmod::FULL_COLLECTION,
// };
// use super::*;
// use testmod::DATABASE_JSON;
// fn expected() -> Collection {
// let mut expected = FULL_COLLECTION.to_owned();
// for artist in expected.iter_mut() {
// for album in artist.albums.iter_mut() {
// album.tracks.clear();
// }
// }
// expected
// }
// #[test]
// fn save() {
// let write_data = FULL_COLLECTION.to_owned();
// let input = DATABASE_JSON.to_owned();
// let mut backend = MockISqlDatabaseBackend::new();
// backend
// .expect_write()
// .with(predicate::eq(input))
// .times(1)
// .return_once(|_| Ok(()));
// SqlDatabase::new(backend).save(&write_data).unwrap();
// }
// #[test]
// fn load() {
// let expected = expected();
// let result = Ok(DATABASE_JSON.to_owned());
// eprintln!("{DATABASE_JSON}");
// let mut backend = MockISqlDatabaseBackend::new();
// backend.expect_read().times(1).return_once(|| result);
// let read_data: Vec<Artist> = SqlDatabase::new(backend).load().unwrap();
// 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]
// fn load_errors() {
// let json = String::from("");
// let serde_err = serde_json::from_str::<DeserializeDatabase>(&json);
// assert!(serde_err.is_err());
// let serde_err: LoadError = serde_err.unwrap_err().into();
// assert!(!serde_err.to_string().is_empty());
// assert!(!format!("{:?}", serde_err).is_empty());
// }
// #[test]
// fn save_errors() {
// // serde_json will raise an error as it has certain requirements on keys.
// let mut object = HashMap::<Result<(), ()>, String>::new();
// object.insert(Ok(()), String::from("string"));
// let serde_err = serde_json::to_string(&object);
// assert!(serde_err.is_err());
// let serde_err: SaveError = serde_err.unwrap_err().into();
// assert!(!serde_err.to_string().is_empty());
// assert!(!format!("{:?}", serde_err).is_empty());
// }
// }