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
17 changed files with 1154 additions and 64 deletions

101
Cargo.lock generated
View File

@ -17,6 +17,18 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -298,6 +310,18 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "2.3.0"
@ -432,6 +456,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
@ -443,6 +476,15 @@ dependencies = [
"foldhash",
]
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heck"
version = "0.3.3"
@ -731,7 +773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.15.2",
]
[[package]]
@ -796,6 +838,17 @@ version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
@ -830,7 +883,7 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown",
"hashbrown 0.15.2",
]
[[package]]
@ -868,14 +921,13 @@ dependencies = [
[[package]]
name = "mockall"
version = "0.12.1"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48"
checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2"
dependencies = [
"cfg-if",
"downcast",
"fragile",
"lazy_static",
"mockall_derive",
"predicates",
"predicates-tree",
@ -883,9 +935,9 @@ dependencies = [
[[package]]
name = "mockall_derive"
version = "0.12.1"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2"
checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898"
dependencies = [
"cfg-if",
"proc-macro2",
@ -905,6 +957,7 @@ dependencies = [
"paste",
"ratatui",
"reqwest",
"rusqlite",
"serde",
"serde_json",
"structopt",
@ -1255,6 +1308,20 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rusqlite"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
dependencies = [
"bitflags 2.6.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -2209,6 +2276,26 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
]
[[package]]
name = "zerofrom"
version = "0.1.5"

View File

@ -13,6 +13,7 @@ openssh = { version = "0.11.4", features = ["native-mux"], default-features = fa
paste = { version = "1.0.15", optional = true }
ratatui = { version = "0.29.0", optional = true}
reqwest = { version = "0.12.12", features = ["blocking", "json"], optional = true }
rusqlite = { version = "0.32.1", optional = true }
serde = { version = "1.0.217", features = ["derive"], optional = true }
serde_json = { version = "1.0.134", optional = true}
structopt = { version = "0.3.26", optional = true}
@ -23,16 +24,17 @@ url = { version = "2.5.4" }
uuid = { version = "1.11.0" }
[build-dependencies]
version_check = "0.9.4"
version_check = "0.9.5"
[dev-dependencies]
mockall = "0.12.1"
once_cell = "1.19.0"
tempfile = "3.10.0"
mockall = "0.13.1"
tempfile = "3.15.0"
[features]
default = ["database-json", "library-beets"]
bin = ["structopt"]
database-sqlite = ["rusqlite", "serde", "serde_json"]
database-sqlite-bundled = ["rusqlite/bundled"]
database-json = ["serde", "serde_json"]
library-beets = []
library-beets-ssh = ["openssh", "tokio"]

View File

@ -4,6 +4,19 @@
### Pre-requisites
#### database-sqlite
This feature requires the `sqlite` library.
Either install system libraries: with
On Fedora:
``` sh
sudo dnf install sqlite-devel
```
Or use a bundled version by enabling the `database-sqlite-bundled` feature.
#### musicbrainz-api
This feature requires the `openssl` system library.

View File

@ -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<Collection, LoadError>;
fn load(&mut self) -> Result<Collection, LoadError>;
/// 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<Collection, LoadError> {
fn load(&mut self) -> Result<Collection, LoadError> {
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());
}

View File

@ -49,7 +49,7 @@ impl<JDB: IJsonDatabaseBackend> 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 database: DeserializeDatabase = serde_json::from_str(&serialized)?;
Ok(database.into())

View File

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

View File

@ -13,8 +13,8 @@ pub enum AlbumLibIdDef {
None,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SerdeAlbumLibId(#[serde(with = "AlbumLibIdDef")] AlbumLibId);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct SerdeAlbumLibId(#[serde(with = "AlbumLibIdDef")] pub AlbumLibId);
impl From<SerdeAlbumLibId> for AlbumLibId {
fn from(value: SerdeAlbumLibId) -> Self {
@ -36,8 +36,8 @@ pub struct AlbumDateDef {
day: Option<u8>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SerdeAlbumDate(#[serde(with = "AlbumDateDef")] AlbumDate);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct SerdeAlbumDate(#[serde(with = "AlbumDateDef")] pub AlbumDate);
impl From<SerdeAlbumDate> for AlbumDate {
fn from(value: SerdeAlbumDate) -> Self {
@ -69,8 +69,8 @@ pub enum AlbumPrimaryTypeDef {
Other,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SerdeAlbumPrimaryType(#[serde(with = "AlbumPrimaryTypeDef")] AlbumPrimaryType);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct SerdeAlbumPrimaryType(#[serde(with = "AlbumPrimaryTypeDef")] pub AlbumPrimaryType);
impl From<SerdeAlbumPrimaryType> for AlbumPrimaryType {
fn from(value: SerdeAlbumPrimaryType) -> Self {
@ -101,8 +101,8 @@ pub enum AlbumSecondaryTypeDef {
FieldRecording,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SerdeAlbumSecondaryType(#[serde(with = "AlbumSecondaryTypeDef")] AlbumSecondaryType);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct SerdeAlbumSecondaryType(#[serde(with = "AlbumSecondaryTypeDef")] pub AlbumSecondaryType);
impl From<SerdeAlbumSecondaryType> for AlbumSecondaryType {
fn from(value: SerdeAlbumSecondaryType) -> Self {

View File

@ -23,38 +23,42 @@ pub enum DeserializeDatabase {
impl From<DeserializeDatabase> for Collection {
fn from(database: DeserializeDatabase) -> Self {
match database {
DeserializeDatabase::V20250103(collection) => {
collection.into_iter().map(Into::into).collect()
DeserializeDatabase::V20250103(db) => {
let mut collection: Collection = db.into_iter().map(Into::into).collect();
collection.sort_unstable();
collection
}
}
}
}
#[derive(Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
pub struct DeserializeArtist {
name: String,
sort: Option<String>,
musicbrainz: DeserializeMbRefOption,
properties: HashMap<String, Vec<String>>,
albums: Vec<DeserializeAlbum>,
pub name: String,
pub sort: Option<String>,
pub musicbrainz: DeserializeMbRefOption,
pub properties: HashMap<String, Vec<String>>,
pub albums: Vec<DeserializeAlbum>,
}
#[derive(Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
pub struct DeserializeAlbum {
title: String,
lib_id: SerdeAlbumLibId,
date: SerdeAlbumDate,
seq: u8,
musicbrainz: DeserializeMbRefOption,
primary_type: Option<SerdeAlbumPrimaryType>,
secondary_types: Vec<SerdeAlbumSecondaryType>,
pub title: String,
pub lib_id: SerdeAlbumLibId,
pub date: SerdeAlbumDate,
pub seq: u8,
pub musicbrainz: DeserializeMbRefOption,
pub primary_type: Option<SerdeAlbumPrimaryType>,
pub secondary_types: Vec<SerdeAlbumSecondaryType>,
}
#[derive(Debug, Deserialize)]
pub struct DeserializeMbRefOption(#[serde(with = "MbRefOptionDef")] MbRefOption<DeserializeMbid>);
#[derive(Clone, Debug, Deserialize)]
pub struct DeserializeMbRefOption(
#[serde(with = "MbRefOptionDef")] pub MbRefOption<DeserializeMbid>,
);
#[derive(Clone, Debug)]
pub struct DeserializeMbid(Mbid);
pub struct DeserializeMbid(pub Mbid);
macro_rules! impl_from_for_mb_ref_option {
($ref:ty) => {
@ -110,6 +114,8 @@ impl<'de> Deserialize<'de> for DeserializeMbid {
impl From<DeserializeArtist> for Artist {
fn from(artist: DeserializeArtist) -> Self {
let mut albums: Vec<Album> = artist.albums.into_iter().map(Into::into).collect();
albums.sort_unstable();
Artist {
meta: ArtistMeta {
id: ArtistId {
@ -121,7 +127,7 @@ impl From<DeserializeArtist> for Artist {
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.
mod common;
pub mod common;
pub mod deserialize;
pub mod serialize;

View File

@ -22,32 +22,32 @@ impl<'a> From<&'a Collection> for SerializeDatabase<'a> {
}
}
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, PartialEq, Eq)]
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)]
#[derive(Debug, Serialize, PartialEq, Eq)]
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)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct SerializeMbRefOption<'a>(
#[serde(with = "MbRefOptionDef")] MbRefOption<SerializeMbid<'a>>,
);
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerializeMbid<'a>(&'a Mbid);
impl<'a, T: IMusicBrainzRef> From<&'a MbRefOption<T>> for SerializeMbRefOption<'a> {

View File

@ -0,0 +1,265 @@
//! Module for storing MusicHoard data in a SQLite database.
use std::path::PathBuf;
use rusqlite::{
self, types::FromSql, CachedStatement, Connection, Params, Row, Rows, Statement, Transaction,
};
use crate::{
collection::album::AlbumDate,
external::database::{
serde::{
common::SerdeAlbumDate,
deserialize::{DeserializeAlbum, DeserializeArtist},
serialize::{SerializeAlbum, SerializeArtist},
},
sql::{Error, ISqlDatabaseBackend, ISqlTransactionBackend},
},
};
/// SQLite database backend that uses SQLite as the implementation.
pub struct SqlDatabaseSqliteBackend {
conn: Connection,
}
pub struct SqlTransactionSqliteBackend<'conn> {
tx: Transaction<'conn>,
}
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()))?,
})
}
}
impl SqlTransactionSqliteBackend<'_> {
// We only prepare strings known at compile time so errors in prep are bugs.
fn prepare(&self, sql: &'static str) -> Statement {
self.tx.prepare(sql).unwrap()
}
// We only prepare strings known at compile time so errors in prep are bugs.
fn prepare_cached(&self, sql: &'static str) -> CachedStatement {
self.tx.prepare_cached(sql).unwrap()
}
fn execute<P: Params>(stmt: &mut Statement, params: P) -> Result<(), Error> {
stmt.execute(params)
.map(|_| ())
.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<'conn> ISqlDatabaseBackend<'conn> for SqlDatabaseSqliteBackend {
type Tx = SqlTransactionSqliteBackend<'conn>;
fn transaction(&'conn mut self) -> Result<Self::Tx, Error> {
self.conn
.transaction()
.map(|tx| SqlTransactionSqliteBackend { tx })
.map_err(|err| Error::OpenError(err.to_string()))
}
}
impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
fn commit(self) -> Result<(), Error> {
self.tx
.commit()
.map_err(|err| Error::ExecError(err.to_string()))
}
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) ON DELETE CASCADE ON UPDATE NO ACTION
)",
);
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(&self, version: &str) -> Result<(), Error> {
let mut stmt = self.prepare_cached(
"INSERT INTO database_metadata (name, value)
VALUES (?1, ?2)",
);
Self::execute(&mut stmt, ("version", version))
}
fn select_database_version<'a>(&self) -> Result<Option<String>, Error> {
let mut stmt =
self.prepare_cached("SELECT value FROM database_metadata WHERE name = 'version'");
let mut rows = Self::query(&mut stmt, ())?;
Self::next_row(&mut rows)?
.map(|row| Self::get_value(row, 0))
.transpose()
}
fn insert_artist(&self, artist: &SerializeArtist<'_>) -> 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 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(row.try_into()?);
}
Ok(artists)
}
fn insert_album(&self, artist_name: &str, album: &SerializeAlbum<'_>) -> 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)?,
),
)
}
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(row.try_into()?);
}
Ok(albums)
}
}
impl TryFrom<&Row<'_>> for DeserializeArtist {
type Error = Error;
fn try_from(row: &Row<'_>) -> Result<Self, Self::Error> {
type Backend<'a> = SqlTransactionSqliteBackend<'a>;
Ok(DeserializeArtist {
name: Backend::get_value(row, 0)?,
sort: Backend::get_value(row, 1)?,
musicbrainz: serde_json::from_str(&Backend::get_value::<String>(row, 2)?)?,
properties: serde_json::from_str(&Backend::get_value::<String>(row, 3)?)?,
albums: vec![],
})
}
}
impl TryFrom<&Row<'_>> for DeserializeAlbum {
type Error = Error;
fn try_from(row: &Row<'_>) -> Result<Self, Self::Error> {
type Backend<'a> = SqlTransactionSqliteBackend<'a>;
Ok(DeserializeAlbum {
title: Backend::get_value(row, 0)?,
lib_id: serde_json::from_str(&Backend::get_value::<String>(row, 1)?)?,
date: SerdeAlbumDate(AlbumDate::new(
Backend::get_value(row, 2)?,
Backend::get_value(row, 3)?,
Backend::get_value(row, 4)?,
)),
seq: Backend::get_value(row, 5)?,
musicbrainz: serde_json::from_str(&Backend::get_value::<String>(row, 6)?)?,
primary_type: serde_json::from_str(&Backend::get_value::<String>(row, 7)?)?,
secondary_types: serde_json::from_str(&Backend::get_value::<String>(row, 8)?)?,
})
}
}

View File

@ -0,0 +1,448 @@
//! 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::{
deserialize::{DeserializeAlbum, DeserializeArtist, DeserializeDatabase},
serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase},
},
};
const V20250103: &str = "V20250103";
/// Trait for the SQL database backend.
pub trait ISqlDatabaseBackend<'conn> {
type Tx: ISqlTransactionBackend + 'conn;
/// Begin an SQL transaction.
fn transaction(&'conn mut self) -> Result<Self::Tx, Error>;
}
/// Trait for the SQL database backend.
#[cfg_attr(test, automock)]
pub trait ISqlTransactionBackend {
/// Commit transaction.
fn commit(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(&self, version: &str) -> Result<(), Error>;
/// Get the database version.
fn select_database_version(&self) -> Result<Option<String>, Error>;
/// Insert an artist into the artist table.
#[allow(clippy::needless_lifetimes)] // Conflicts with automock.
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.
#[allow(clippy::needless_lifetimes)] // Conflicts with automock.
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.
#[derive(Debug)]
pub enum Error {
/// An error occurred when connecting to the database.
OpenError(String),
/// An error occurred during serialisation.
SerDeError(String),
/// An error occurred during SQL execution.
ExecError(String),
}
impl From<serde_json::Error> for Error {
fn from(value: serde_json::Error) -> Self {
Error::SerDeError(value.to_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::SerDeError(ref s) => write!(f, "an error occurred during serialisation : {s}"),
Self::ExecError(ref s) => write!(f, "an error occurred during SQL 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()),
}
}
}
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.
pub struct SqlDatabase<SDB> {
backend: SDB,
}
impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> SqlDatabase<SDB> {
/// Create a new SQL database with the provided backend, e.g.
/// [`backend::SqlDatabaseSqliteBackend`].
pub fn new(backend: SDB) -> Result<Self, Error> {
let mut db = SqlDatabase { backend };
let tx = db.backend.transaction()?;
Self::create_tables(&tx)?;
tx.commit()?;
Ok(db)
}
fn create_tables<Tx: ISqlTransactionBackend>(tx: &Tx) -> Result<(), Error> {
tx.create_database_metadata_table()?;
tx.create_artists_table()?;
tx.create_albums_table()?;
Ok(())
}
fn drop_tables<Tx: ISqlTransactionBackend>(tx: &Tx) -> Result<(), Error> {
tx.drop_albums_table()?;
tx.drop_artists_table()?;
tx.drop_database_metadata_table()?;
Ok(())
}
}
impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase<SDB> {
fn load(&mut self) -> Result<Collection, LoadError> {
let tx = self.backend.transaction()?;
let version = tx
.select_database_version()?
.ok_or_else(|| Error::SerDeError(String::from("missing database version")))?;
let database = match version.as_str() {
V20250103 => {
let mut coll = tx.select_all_artists()?;
for artist in coll.iter_mut() {
artist.albums.extend(tx.select_artist_albums(&artist.name)?);
}
Ok(DeserializeDatabase::V20250103(coll))
}
s => Err(Error::SerDeError(format!("unknown database version: {s}"))),
}?;
tx.commit()?;
Ok(database.into())
}
fn save(&mut self, collection: &Collection) -> Result<(), SaveError> {
let database: SerializeDatabase = collection.into();
let tx = self.backend.transaction()?;
Self::drop_tables(&tx)?;
Self::create_tables(&tx)?;
match database {
SerializeDatabase::V20250103(artists) => {
tx.insert_database_version(V20250103)?;
for artist in artists.iter() {
tx.insert_artist(artist)?;
for album in artist.albums.iter() {
tx.insert_album(artist.name, album)?;
}
}
}
}
tx.commit()?;
Ok(())
}
}
#[cfg(test)]
pub mod testmod;
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use mockall::{predicate, Sequence};
use crate::{
core::{collection::Collection, testmod::FULL_COLLECTION},
external::database::sql::testmod::{
DATABASE_SQL_ALBUMS, DATABASE_SQL_ARTISTS, DATABASE_SQL_VERSION,
},
};
use super::*;
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
}
struct SqlDatabaseTestBackend {
pub txs: VecDeque<MockISqlTransactionBackend>,
}
impl SqlDatabaseTestBackend {
fn new(txs: VecDeque<MockISqlTransactionBackend>) -> Self {
SqlDatabaseTestBackend { txs }
}
}
impl ISqlDatabaseBackend<'_> for SqlDatabaseTestBackend {
type Tx = MockISqlTransactionBackend;
fn transaction(&mut self) -> Result<Self::Tx, Error> {
self.txs
.pop_front()
.ok_or_else(|| Error::OpenError(String::from("lol")))
}
}
macro_rules! then {
($tx:ident, $seq:ident, $expect:ident) => {
$tx.$expect().times(1).in_sequence(&mut $seq)
};
}
macro_rules! then0 {
($tx:ident, $seq:ident, $expect:ident) => {
then!($tx, $seq, $expect).return_once(|| Ok(()))
};
}
macro_rules! then1 {
($tx:ident, $seq:ident, $expect:ident) => {
then!($tx, $seq, $expect).return_once(|_| Ok(()))
};
}
macro_rules! then2 {
($tx:ident, $seq:ident, $expect:ident) => {
then!($tx, $seq, $expect).return_once(|_, _| Ok(()))
};
}
macro_rules! expect_create {
($tx:ident, $seq:ident) => {
let mut seq = Sequence::new();
then0!($tx, seq, expect_create_database_metadata_table);
then0!($tx, seq, expect_create_artists_table);
then0!($tx, seq, expect_create_albums_table);
};
}
macro_rules! expect_drop {
($tx:ident, $seq:ident) => {
let mut seq = Sequence::new();
then0!($tx, seq, expect_drop_albums_table);
then0!($tx, seq, expect_drop_artists_table);
then0!($tx, seq, expect_drop_database_metadata_table);
};
}
fn database(txs: VecDeque<MockISqlTransactionBackend>) -> SqlDatabase<SqlDatabaseTestBackend> {
let mut backend = SqlDatabaseTestBackend::new(txs);
let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new();
expect_create!(tx, seq);
then0!(tx, seq, expect_commit);
backend.txs.push_front(tx);
SqlDatabase::new(backend).unwrap()
}
#[test]
fn save() {
let write_data = FULL_COLLECTION.to_owned();
let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new();
expect_drop!(tx, seq);
expect_create!(tx, seq);
then1!(tx, seq, expect_insert_database_version).with(predicate::eq(V20250103));
for artist in write_data.iter() {
let ac = artist.clone();
then1!(tx, seq, expect_insert_artist)
.withf(move |a| a == &Into::<SerializeArtist>::into(&ac));
for album in artist.albums.iter() {
let (nc, ac) = (artist.meta.id.name.clone(), album.clone());
then2!(tx, seq, expect_insert_album)
.withf(move |n, a| n == nc && a == &Into::<SerializeAlbum>::into(&ac));
}
}
then0!(tx, seq, expect_commit);
assert!(database(VecDeque::from([tx])).save(&write_data).is_ok());
}
#[test]
fn load() {
let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new();
then!(tx, seq, expect_select_database_version)
.return_once(|| Ok(Some(DATABASE_SQL_VERSION.to_string())));
let de_artists = DATABASE_SQL_ARTISTS.to_owned();
let artists: Collection = de_artists.iter().cloned().map(Into::into).collect();
then!(tx, seq, expect_select_all_artists).return_once(|| Ok(de_artists));
for artist in artists.iter() {
let de_albums = DATABASE_SQL_ALBUMS.get(&artist.meta.id.name).unwrap();
then!(tx, seq, expect_select_artist_albums)
.with(predicate::eq(artist.meta.id.name.clone()))
.return_once(|_| Ok(de_albums.to_owned()));
}
then0!(tx, seq, expect_commit);
let read_data = database(VecDeque::from([tx])).load().unwrap();
let expected = expected();
assert_eq!(read_data, expected);
}
#[test]
fn load_missing_database_version() {
let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new();
then!(tx, seq, expect_select_database_version).return_once(|| Ok(None));
let error = database(VecDeque::from([tx])).load().unwrap_err();
assert!(matches!(error, LoadError::SerDeError(_)));
}
#[test]
fn load_unknown_database_version() {
let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new();
then!(tx, seq, expect_select_database_version)
.return_once(|| Ok(Some(String::from("no u"))));
let error = database(VecDeque::from([tx])).load().unwrap_err();
assert!(matches!(error, LoadError::SerDeError(_)));
}
#[test]
fn load_backend_open_error() {
let error = database(VecDeque::from([])).load().unwrap_err();
assert!(matches!(error, LoadError::IoError(_)));
}
#[test]
fn save_backend_open_error() {
let error = database(VecDeque::from([])).save(&vec![]).unwrap_err();
assert!(matches!(error, SaveError::IoError(_)));
}
#[test]
fn load_backend_exec_error() {
let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new();
then!(tx, seq, expect_select_database_version)
.return_once(|| Err(Error::ExecError(String::from("serde"))));
let error = database(VecDeque::from([tx])).load().unwrap_err();
assert!(matches!(error, LoadError::IoError(_)));
}
#[test]
fn save_backend_exec_error() {
let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new();
expect_drop!(tx, seq);
expect_create!(tx, seq);
then!(tx, seq, expect_insert_database_version)
.with(predicate::eq(V20250103))
.return_once(|_| Err(Error::ExecError(String::from("exec"))));
let error = database(VecDeque::from([tx])).save(&vec![]).unwrap_err();
assert!(matches!(error, SaveError::IoError(_)));
}
#[test]
fn load_backend_serde_error() {
let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new();
then!(tx, seq, expect_select_database_version)
.return_once(|| Ok(Some(DATABASE_SQL_VERSION.to_string())));
then!(tx, seq, expect_select_all_artists)
.return_once(|| Err(Error::SerDeError(String::from("serde"))));
let error = database(VecDeque::from([tx])).load().unwrap_err();
assert!(matches!(error, LoadError::SerDeError(_)));
}
#[test]
fn save_backend_serde_error() {
let write_data = FULL_COLLECTION.to_owned();
let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new();
expect_drop!(tx, seq);
expect_create!(tx, seq);
then1!(tx, seq, expect_insert_database_version).with(predicate::eq(V20250103));
then!(tx, seq, expect_insert_artist)
.return_once(|_| Err(Error::SerDeError(String::from("serde"))));
let error = database(VecDeque::from([tx]))
.save(&write_data)
.unwrap_err();
assert!(matches!(error, SaveError::SerDeError(_)));
}
#[test]
fn serde_json_error() {
let error = serde_json::from_str::<DeserializeArtist>("").unwrap_err();
let error: Error = error.into();
assert!(matches!(error, Error::SerDeError(_)));
assert!(!error.to_string().is_empty());
}
}

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![],
},
],
),
])
});

View File

@ -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<Artist> = database.load().unwrap();

View File

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

63
tests/database/sql.rs Normal file
View File

@ -0,0 +1,63 @@
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<PathBuf> =
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
}
#[test]
fn save() {
let file = NamedTempFile::new().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();
}
#[test]
fn load() {
let backend = SqlDatabaseSqliteBackend::new(&*DATABASE_TEST_FILE).unwrap();
let mut database = SqlDatabase::new(backend).unwrap();
let read_data: Vec<Artist> = database.load().unwrap();
let expected = expected();
assert_eq!(read_data, expected);
}
#[test]
fn reverse() {
let file = NamedTempFile::new().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 read_data: Vec<Artist> = database.load().unwrap();
// Not all data is saved into database.
let expected = expected();
assert_eq!(read_data, expected);
}

Binary file not shown.