Draft code to write database
This commit is contained in:
parent
3c6c093f1e
commit
02fdef444f
@ -1,4 +1,7 @@
|
||||
#[cfg(feature = "database-json")]
|
||||
pub mod json;
|
||||
#[cfg(feature = "database-sqlite")]
|
||||
pub mod sql;
|
||||
|
||||
#[cfg(feature = "database-json")]
|
||||
mod serde;
|
||||
|
@ -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 {
|
||||
|
@ -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)]
|
||||
|
166
src/external/database/sql/backend.rs
Normal file
166
src/external/database/sql/backend.rs
Normal 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)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
263
src/external/database/sql/mod.rs
Normal file
263
src/external/database/sql/mod.rs
Normal 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());
|
||||
// }
|
||||
// }
|
Loading…
x
Reference in New Issue
Block a user