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
2 changed files with 59 additions and 49 deletions
Showing only changes of commit eccc8a880f - Show all commits

View File

@ -2,11 +2,11 @@
use std::path::PathBuf; use std::path::PathBuf;
use rusqlite::{self, CachedStatement, Connection, Params, Statement}; use rusqlite::{self, CachedStatement, Connection, Params, Statement, Transaction};
use crate::external::database::{ use crate::external::database::{
serde::serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase}, serde::serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase},
sql::{Error, ISqlDatabaseBackend}, sql::{Error, ISqlDatabaseBackend, ISqlTransactionBackend},
}; };
/// SQLite database backend that uses SQLite as the implementation. /// SQLite database backend that uses SQLite as the implementation.
@ -14,6 +14,10 @@ pub struct SqlDatabaseSqliteBackend {
conn: Connection, conn: Connection,
} }
pub struct SqlTransactionSqliteBackend<'conn> {
tx: Transaction<'conn>,
}
impl SqlDatabaseSqliteBackend { impl SqlDatabaseSqliteBackend {
/// Create a [`SqlDatabaseSqliteBackend`] that will read/write to the provided database. /// Create a [`SqlDatabaseSqliteBackend`] that will read/write to the provided database.
pub fn new<P: Into<PathBuf>>(path: P) -> Result<Self, Error> { pub fn new<P: Into<PathBuf>>(path: P) -> Result<Self, Error> {
@ -21,15 +25,17 @@ impl SqlDatabaseSqliteBackend {
conn: Connection::open(path.into()).map_err(|err| Error::OpenError(err.to_string()))?, conn: Connection::open(path.into()).map_err(|err| Error::OpenError(err.to_string()))?,
}) })
} }
}
impl<'conn> SqlTransactionSqliteBackend<'conn> {
fn prepare(&self, sql: &str) -> Result<Statement, Error> { fn prepare(&self, sql: &str) -> Result<Statement, Error> {
self.conn self.tx
.prepare(sql) .prepare(sql)
.map_err(|err| Error::StmtError(err.to_string())) .map_err(|err| Error::StmtError(err.to_string()))
} }
fn prepare_cached(&self, sql: &str) -> Result<CachedStatement, Error> { fn prepare_cached(&self, sql: &str) -> Result<CachedStatement, Error> {
self.conn self.tx
.prepare_cached(sql) .prepare_cached(sql)
.map_err(|err| Error::StmtError(err.to_string())) .map_err(|err| Error::StmtError(err.to_string()))
} }
@ -47,15 +53,22 @@ impl From<serde_json::Error> for Error {
} }
} }
impl ISqlDatabaseBackend for SqlDatabaseSqliteBackend { impl<'conn> ISqlDatabaseBackend<'conn> for SqlDatabaseSqliteBackend {
fn begin_transaction(&self) -> Result<(), Error> { type Tx = SqlTransactionSqliteBackend<'conn>;
let mut stmt = self.prepare_cached("BEGIN TRANSACTION;")?;
Self::execute(&mut stmt, ())
}
fn commit_transaction(&self) -> Result<(), Error> { fn transaction(&'conn mut self) -> Result<Self::Tx, Error> {
let mut stmt = self.prepare_cached("COMMIT;")?; self.conn
Self::execute(&mut stmt, ()) .transaction()
.map(|tx| SqlTransactionSqliteBackend { tx })
.map_err(|err| Error::OpenError(err.to_string()))
}
}
impl<'conn> ISqlTransactionBackend for SqlTransactionSqliteBackend<'conn> {
fn commit(self) -> Result<(), Error> {
self.tx
.commit()
.map_err(|err| Error::ExecError(err.to_string()))
} }
fn create_database_metadata_table(&self) -> Result<(), Error> { fn create_database_metadata_table(&self) -> Result<(), Error> {
@ -85,7 +98,7 @@ impl ISqlDatabaseBackend for SqlDatabaseSqliteBackend {
Self::execute(&mut stmt, ()) Self::execute(&mut stmt, ())
} }
fn drop_artists_table(&self) -> Result<(),Error> { fn drop_artists_table(&self) -> Result<(), Error> {
let mut stmt = self.prepare_cached("DROP TABLE artists")?; let mut stmt = self.prepare_cached("DROP TABLE artists")?;
Self::execute(&mut stmt, ()) Self::execute(&mut stmt, ())
} }
@ -109,7 +122,7 @@ impl ISqlDatabaseBackend for SqlDatabaseSqliteBackend {
Self::execute(&mut stmt, ()) Self::execute(&mut stmt, ())
} }
fn drop_albums_table(&self) -> Result<(),Error> { fn drop_albums_table(&self) -> Result<(), Error> {
let mut stmt = self.prepare_cached("DROP TABLE albums")?; let mut stmt = self.prepare_cached("DROP TABLE albums")?;
Self::execute(&mut stmt, ()) Self::execute(&mut stmt, ())
} }

View File

@ -16,13 +16,18 @@ use crate::{
}; };
/// Trait for the SQL database backend. /// Trait for the SQL database backend.
#[cfg_attr(test, automock)] pub trait ISqlDatabaseBackend<'conn> {
pub trait ISqlDatabaseBackend { type Tx: ISqlTransactionBackend + 'conn;
/// Begin an SQL transaction.
fn begin_transaction(&self) -> Result<(), Error>;
/// Commit ongoing transaction. /// Begin an SQL transaction.
fn commit_transaction(&self) -> Result<(), Error>; 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). /// Create the database metadata table (if needed).
fn create_database_metadata_table(&self) -> Result<(), Error>; fn create_database_metadata_table(&self) -> Result<(), Error>;
@ -95,65 +100,57 @@ pub struct SqlDatabase<SDB> {
backend: SDB, backend: SDB,
} }
impl<SDB: ISqlDatabaseBackend> SqlDatabase<SDB> { impl<SDB: for<'c> ISqlDatabaseBackend<'c>> SqlDatabase<SDB> {
/// Create a new SQL database with the provided backend, e.g. /// Create a new SQL database with the provided backend, e.g.
/// [`backend::SqlDatabaseSqliteBackend`]. /// [`backend::SqlDatabaseSqliteBackend`].
pub fn new(backend: SDB) -> Result<Self, Error> { pub fn new(backend: SDB) -> Result<Self, Error> {
let db = SqlDatabase { backend }; let mut db = SqlDatabase { backend };
db.begin_transaction()?; let tx = db.backend.transaction()?;
db.create_tables()?; Self::create_tables(&tx)?;
db.commit_transaction()?; tx.commit()?;
Ok(db) Ok(db)
} }
fn begin_transaction(&self) -> Result<(), Error> { fn create_tables<Tx: ISqlTransactionBackend>(tx: &Tx) -> Result<(), Error> {
self.backend.begin_transaction() tx.create_database_metadata_table()?;
} tx.create_artists_table()?;
tx.create_albums_table()?;
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(()) Ok(())
} }
fn drop_tables(&self) -> Result<(), Error> { fn drop_tables<Tx: ISqlTransactionBackend>(tx: &Tx) -> Result<(), Error> {
self.backend.drop_database_metadata_table()?; tx.drop_database_metadata_table()?;
self.backend.drop_artists_table()?; tx.drop_artists_table()?;
self.backend.drop_albums_table()?; tx.drop_albums_table()?;
Ok(()) Ok(())
} }
} }
impl<SDB: ISqlDatabaseBackend> IDatabase for SqlDatabase<SDB> { impl<SDB: for<'c> ISqlDatabaseBackend<'c>> IDatabase for SqlDatabase<SDB> {
fn load(&self) -> Result<Collection, LoadError> { fn load(&self) -> Result<Collection, LoadError> {
Ok(vec![]) Ok(vec![])
} }
fn save(&mut self, collection: &Collection) -> Result<(), SaveError> { fn save(&mut self, collection: &Collection) -> Result<(), SaveError> {
let database: SerializeDatabase = collection.into(); let database: SerializeDatabase = collection.into();
self.begin_transaction()?; let tx = self.backend.transaction()?;
self.drop_tables()?; Self::drop_tables(&tx)?;
self.create_tables()?; Self::create_tables(&tx)?;
self.backend.insert_database_version(&database)?; tx.insert_database_version(&database)?;
match database { match database {
SerializeDatabase::V20250103(artists) => { SerializeDatabase::V20250103(artists) => {
for artist in artists.iter() { for artist in artists.iter() {
self.backend.insert_artist(artist)?; tx.insert_artist(artist)?;
for album in artist.albums.iter() { for album in artist.albums.iter() {
self.backend.insert_album(artist.name, album)?; tx.insert_album(artist.name, album)?;
} }
} }
} }
} }
self.commit_transaction()?; tx.commit()?;
Ok(()) Ok(())
} }
} }