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 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<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()))?,
})
}
}
impl<'conn> SqlTransactionSqliteBackend<'conn> {
fn prepare(&self, sql: &str) -> Result<Statement, Error> {
self.conn
self.tx
.prepare(sql)
.map_err(|err| Error::StmtError(err.to_string()))
}
fn prepare_cached(&self, sql: &str) -> Result<CachedStatement, Error> {
self.conn
self.tx
.prepare_cached(sql)
.map_err(|err| Error::StmtError(err.to_string()))
}
@ -47,15 +53,22 @@ impl From<serde_json::Error> 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::Tx, Error> {
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, ())
}

View File

@ -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<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>;
@ -95,65 +100,57 @@ pub struct SqlDatabase<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.
/// [`backend::SqlDatabaseSqliteBackend`].
pub fn new(backend: SDB) -> Result<Self, Error> {
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: ISqlTransactionBackend>(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: ISqlTransactionBackend>(tx: &Tx) -> Result<(), Error> {
tx.drop_database_metadata_table()?;
tx.drop_artists_table()?;
tx.drop_albums_table()?;
Ok(())
}
}
impl<SDB: ISqlDatabaseBackend> IDatabase for SqlDatabase<SDB> {
impl<SDB: for<'c> ISqlDatabaseBackend<'c>> 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()?;
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(())
}
}