diff --git a/src/external/database/sql/backend.rs b/src/external/database/sql/backend.rs index 763d267..ecd4510 100644 --- a/src/external/database/sql/backend.rs +++ b/src/external/database/sql/backend.rs @@ -2,11 +2,11 @@ use std::path::PathBuf; -use rusqlite::{self, CachedStatement, Connection, Params, Statement}; +use rusqlite::{self, CachedStatement, Connection, Params, Statement, Transaction}; use crate::external::database::{ serde::serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase}, - sql::{Error, ISqlDatabaseBackend}, + sql::{Error, ISqlDatabaseBackend, ISqlTransactionBackend}, }; /// SQLite database backend that uses SQLite as the implementation. @@ -14,6 +14,10 @@ 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>(path: P) -> Result { @@ -21,15 +25,17 @@ impl SqlDatabaseSqliteBackend { conn: Connection::open(path.into()).map_err(|err| Error::OpenError(err.to_string()))?, }) } +} +impl<'conn> SqlTransactionSqliteBackend<'conn> { fn prepare(&self, sql: &str) -> Result { - self.conn + self.tx .prepare(sql) .map_err(|err| Error::StmtError(err.to_string())) } fn prepare_cached(&self, sql: &str) -> Result { - self.conn + self.tx .prepare_cached(sql) .map_err(|err| Error::StmtError(err.to_string())) } @@ -47,15 +53,22 @@ impl From for Error { } } -impl ISqlDatabaseBackend for SqlDatabaseSqliteBackend { - fn begin_transaction(&self) -> Result<(), Error> { - let mut stmt = self.prepare_cached("BEGIN TRANSACTION;")?; - Self::execute(&mut stmt, ()) - } +impl<'conn> ISqlDatabaseBackend<'conn> for SqlDatabaseSqliteBackend { + type Tx = SqlTransactionSqliteBackend<'conn>; - fn commit_transaction(&self) -> Result<(), Error> { - let mut stmt = self.prepare_cached("COMMIT;")?; - Self::execute(&mut stmt, ()) + fn transaction(&'conn mut self) -> Result { + self.conn + .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> { @@ -85,7 +98,7 @@ impl ISqlDatabaseBackend for SqlDatabaseSqliteBackend { 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")?; Self::execute(&mut stmt, ()) } @@ -109,7 +122,7 @@ impl ISqlDatabaseBackend for SqlDatabaseSqliteBackend { 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")?; Self::execute(&mut stmt, ()) } diff --git a/src/external/database/sql/mod.rs b/src/external/database/sql/mod.rs index 932c180..8a02eda 100644 --- a/src/external/database/sql/mod.rs +++ b/src/external/database/sql/mod.rs @@ -16,13 +16,18 @@ use crate::{ }; /// Trait for the SQL database backend. -#[cfg_attr(test, automock)] -pub trait ISqlDatabaseBackend { - /// Begin an SQL transaction. - fn begin_transaction(&self) -> Result<(), Error>; +pub trait ISqlDatabaseBackend<'conn> { + type Tx: ISqlTransactionBackend + 'conn; - /// Commit ongoing transaction. - fn commit_transaction(&self) -> Result<(), Error>; + /// Begin an SQL transaction. + fn transaction(&'conn mut self) -> Result; +} + +/// 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>; @@ -95,65 +100,57 @@ pub struct SqlDatabase { backend: SDB, } -impl SqlDatabase { +impl ISqlDatabaseBackend<'c>> SqlDatabase { /// Create a new SQL database with the provided backend, e.g. /// [`backend::SqlDatabaseSqliteBackend`]. pub fn new(backend: SDB) -> Result { - let db = SqlDatabase { backend }; - db.begin_transaction()?; - db.create_tables()?; - db.commit_transaction()?; + let mut db = SqlDatabase { backend }; + let tx = db.backend.transaction()?; + Self::create_tables(&tx)?; + tx.commit()?; 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()?; + fn create_tables(tx: &Tx) -> Result<(), Error> { + tx.create_database_metadata_table()?; + tx.create_artists_table()?; + tx.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()?; + fn drop_tables(tx: &Tx) -> Result<(), Error> { + tx.drop_database_metadata_table()?; + tx.drop_artists_table()?; + tx.drop_albums_table()?; Ok(()) } } -impl IDatabase for SqlDatabase { +impl ISqlDatabaseBackend<'c>> IDatabase for SqlDatabase { fn load(&self) -> Result { Ok(vec![]) } fn save(&mut self, collection: &Collection) -> Result<(), SaveError> { let database: SerializeDatabase = collection.into(); - self.begin_transaction()?; + let tx = self.backend.transaction()?; - self.drop_tables()?; - self.create_tables()?; + Self::drop_tables(&tx)?; + Self::create_tables(&tx)?; - self.backend.insert_database_version(&database)?; + tx.insert_database_version(&database)?; match database { SerializeDatabase::V20250103(artists) => { for artist in artists.iter() { - self.backend.insert_artist(artist)?; + tx.insert_artist(artist)?; 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(()) } }