diff --git a/inc/liblektor-rs/database.h b/inc/liblektor-rs/database.h index b398c88f7452822301f8b3e1f8d1f418218cbe3d..23bab61ffc3d11dd96644f61aaa175106e6d540b 100644 --- a/inc/liblektor-rs/database.h +++ b/inc/liblektor-rs/database.h @@ -5,12 +5,17 @@ extern "C" { #endif +#include <stdint.h> + struct lkt_sqlite_connection; typedef struct lkt_sqlite_connection lkt_sqlite_connection; lkt_sqlite_connection *lkt_database_establish_connection(const char *); void lkt_database_close_connection(lkt_sqlite_connection *const); +bool lkt_database_delete_kara_by_repo(lkt_sqlite_connection *const, int64_t, int64_t); +bool lkt_database_delete_kara_by_local_id(lkt_sqlite_connection *const, int64_t); + #if defined(__cplusplus) } #endif diff --git a/src/rust/liblektor-rs/src/database/error.rs b/src/rust/liblektor-rs/src/database/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..d0fd2eb7d42e821e43a1a2c2bb38b3777fb35e03 --- /dev/null +++ b/src/rust/liblektor-rs/src/database/error.rs @@ -0,0 +1,40 @@ +use super::*; + +pub enum LktDatabaseError { + DieselConnection(diesel::ConnectionError), + DieselResult(diesel::result::Error), + IntegerOverflow, + String(String), +} + +pub type LktDatabaseResult<T> = Result<T, LktDatabaseError>; + +impl From<diesel::ConnectionError> for LktDatabaseError { + fn from(err: diesel::ConnectionError) -> Self { + LktDatabaseError::DieselConnection(err) + } +} + +impl From<diesel::result::Error> for LktDatabaseError { + fn from(err: diesel::result::Error) -> Self { + LktDatabaseError::DieselResult(err) + } +} + +impl From<std::num::TryFromIntError> for LktDatabaseError { + fn from(_: std::num::TryFromIntError) -> Self { + LktDatabaseError::IntegerOverflow + } +} + +impl std::fmt::Display for LktDatabaseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use LktDatabaseError::*; + match self { + DieselConnection(err) => write!(f, "diesel connection error: {err}"), + DieselResult(err) => write!(f, "diesel result error: {err}"), + IntegerOverflow => f.write_str("integer overflow when casting to i32"), + String(str) => f.write_str(str), + } + } +} diff --git a/src/rust/liblektor-rs/src/database/mod.rs b/src/rust/liblektor-rs/src/database/mod.rs index 9b910e96103b7ab917dc03508637e7864351fbe7..28c83cbf62cd8033deb0cb21282394829b5ce337 100644 --- a/src/rust/liblektor-rs/src/database/mod.rs +++ b/src/rust/liblektor-rs/src/database/mod.rs @@ -2,15 +2,15 @@ pub(self) use diesel::prelude::*; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +pub(self) use error::*; pub(self) use log::*; +pub mod error; pub mod models; pub mod schema; pub mod unsafe_interface; -use self::models::*; - -use crate::{database::schema::kara_tags, kurisu_api::v1 as api_v1}; +use crate::{database::models::*, kurisu_api::v1 as api_v1}; /// The migrations! const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); @@ -32,10 +32,10 @@ pub fn establish_connection(path: impl AsRef<str>) -> Result<SqliteConnection, S /// All the information needed to add a kara recieved from a repo! pub type NewKaraRequest<'a> = ( - models::NewKaraId, + models::KaraId, models::NewKara<'a>, - models::NewLanguage<'a>, - Vec<models::AddKaraMaker<'a>>, + models::Language<'a>, + Vec<models::KaraMaker<'a>>, Vec<models::AddKaraTag>, ); @@ -43,47 +43,119 @@ pub struct LktDatabaseConnection { sqlite: SqliteConnection, } +/// Load the diesel DSL for a given table. With the loaded dsl execute the +/// expression... +macro_rules! with_dsl { + ($table: ident => $expr: expr) => {{ + use self::schema::$table::dsl::*; + use diesel::dsl::*; + $expr + }}; +} + impl LktDatabaseConnection { /// Get a tag id by its name. - pub fn get_tag_id_by_name(&mut self, tag_name: impl AsRef<str>) -> i32 { - use self::schema::tag::dsl::*; - tag.filter(name.is(tag_name.as_ref())) - .first::<Tag>(&mut self.sqlite) - .unwrap() - .id + pub fn get_tag_id_by_name(&mut self, tag_name: impl AsRef<str>) -> LktDatabaseResult<i32> { + Ok(with_dsl!(tag => tag.filter(name.is(tag_name.as_ref())) + .first::<Tag>(&mut self.sqlite)?.id + )) } - /// Get a free local id for all karas. - pub fn get_kara_new_local_id(&mut self) -> i32 { - use self::schema::kara::dsl::*; - use diesel::dsl::*; - kara.select(max(id)) - .first::<Option<i32>>(&mut self.sqlite) - .unwrap() + /// Get a free local id for all karas. Note that using this function might + /// be unsafe because there is no guarenties that the returned ID will be + /// free by the time a kara is inserted with the said id... + pub fn get_kara_new_local_id(&mut self) -> LktDatabaseResult<i32> { + let max = with_dsl!(kara => kara.select(max(id)) + .first::<Option<i32>>(&mut self.sqlite)? .unwrap_or(0) + ); + Ok(max + 1) + } + + /// Delete a kara with its id in a repo. + pub fn delete_kara_by_repo( + &mut self, + arg_repo_id: u64, + arg_kara_id: u64, + ) -> LktDatabaseResult<()> { + let arg_repo_id = i32::try_from(arg_repo_id)?; + let arg_kara_id = i32::try_from(arg_kara_id)?; + self.sqlite.exclusive_transaction(|c| { + let local_id = with_dsl!(repo_kara => repo_kara + .filter(repo_id.eq(arg_repo_id)) + .filter(repo_kara_id.eq(arg_kara_id)) + .first::<KaraId>(c)? + .local_kara_id + ); + with_dsl!(repo_kara => diesel::delete(repo_kara.filter(local_kara_id.eq(local_id))).execute(c)?); + with_dsl!(kara => diesel::delete(kara.filter(id.eq(local_id))).execute(c)?); + Ok(()) + }) + } + + /// Delete a kara by its local ID. + pub fn delete_kara_by_local_id(&mut self, kara_id: u64) -> LktDatabaseResult<()> { + let local_id = i32::try_from(kara_id)?; + self.sqlite.exclusive_transaction(|c| { + with_dsl!(repo_kara => diesel::delete(repo_kara.filter(local_kara_id.eq(local_id))).execute(c)?); + with_dsl!(kara => diesel::delete(kara.filter(id.eq(local_id))).execute(c)?); + Ok(()) + }) + } + + /// Ensure that a given language is present in the database. If it's not + /// insert it. Existence test is done on the code of the language. + pub fn ensure_language_exists(&mut self, lang: &Language) -> LktDatabaseResult<()> { + self.sqlite.exclusive_transaction(|c| { + with_dsl!(iso_639_1 => match iso_639_1.filter(code.eq(lang.code)).count().get_result(c)? { + 1 => Ok(()), + 0 => { diesel::insert_into(iso_639_1).values(lang).execute(c)?; Ok(()) } + count => Err(LktDatabaseError::String(format!("language `{lang:?}` has {count} occurences in the database..."))), + }) + }) + } + + /// Add a kara with a request. + pub fn add_kara_from_request<'a>(&mut self, kara: NewKaraRequest<'a>) -> LktDatabaseResult<()> { + let (id, new_kara, lang, karamakers, tags) = kara; + self.ensure_language_exists(&lang)?; + self.sqlite.exclusive_transaction(|c| { + with_dsl!(kara => diesel::insert_into(kara).values(&new_kara).execute(c)?); + with_dsl!(repo_kara => diesel::insert_into(repo_kara).values(id).execute(c)?); + with_dsl!(kara_makers => diesel::insert_or_ignore_into(kara_makers).values(karamakers).execute(c)?); + with_dsl!(kara_tags => { + diesel::delete(kara_tags.filter(kara_id.eq(new_kara.id))).execute(c)?; + diesel::insert_into(kara_tags).values(tags).execute(c)?; + }); + Ok(()) + }) } /// Create a series of models from a kara signature from Kurisu's V1 API. - pub fn new_kara<'a>(&mut self, repo_id: u64, kara: api_v1::Kara<'a>) -> NewKaraRequest<'a> { - error!("todo: query the database for a new local id"); - let local_id = self.get_kara_new_local_id(); - let id = NewKaraId { - repo_id: repo_id as i32, - local_kara_id: local_id as i32, - repo_kara_id: kara.id as i32, + pub fn new_kara_v1<'a>( + &mut self, + repo_id: u64, + kara: api_v1::Kara<'a>, + ) -> LktDatabaseResult<NewKaraRequest<'a>> { + let local_id = self.get_kara_new_local_id()?; + let repo_id = i32::try_from(repo_id)?; + let id = KaraId { + repo_id, + local_kara_id: local_id, + repo_kara_id: i32::try_from(kara.id)?, }; - let lang = NewLanguage::from(kara.get_language()); - let kara_makers = vec![AddKaraMaker { - id: local_id as i32, + let lang = Language::from(kara.get_language()); + let kara_makers = vec![KaraMaker { + id: local_id, name: kara.author_name, }]; let tags = vec![AddKaraTag { - kara_id: local_id as i32, - tag_id: self.get_tag_id_by_name("number"), + kara_id: local_id, + tag_id: self.get_tag_id_by_name("number")?, value: Some(format!("{}", kara.song_number)), }]; let kara = NewKara { - id: local_id as i32, + id: local_id, song_title: kara.song_name, song_type: kara.song_type, song_origin: kara.category, @@ -91,6 +163,6 @@ impl LktDatabaseConnection { language: lang.code, file_hash: format!("{}", kara.unix_timestamp), }; - (id, kara, lang, kara_makers, tags) + Ok((id, kara, lang, kara_makers, tags)) } } diff --git a/src/rust/liblektor-rs/src/database/models.rs b/src/rust/liblektor-rs/src/database/models.rs index 63914a8daefb3807def36e2997d147d9c8dfb004..b22a1be9756f5daa531e9cd4f386a09dde9c4f20 100644 --- a/src/rust/liblektor-rs/src/database/models.rs +++ b/src/rust/liblektor-rs/src/database/models.rs @@ -14,15 +14,15 @@ pub struct NewRepo<'a> { pub name: &'a str, } -#[derive(Insertable)] +#[derive(Debug, Insertable, Queryable, Selectable)] #[diesel(table_name = repo_kara)] -pub struct NewKaraId { +pub struct KaraId { pub repo_id: i32, pub repo_kara_id: i32, pub local_kara_id: i32, } -#[derive(Insertable)] +#[derive(Debug, Insertable)] #[diesel(table_name = kara)] pub struct NewKara<'a> { pub id: i32, @@ -34,16 +34,16 @@ pub struct NewKara<'a> { pub file_hash: String, } -#[derive(Insertable)] +#[derive(Debug, Insertable, Queryable, Selectable)] #[diesel(table_name = kara_makers)] -pub struct AddKaraMaker<'a> { +pub struct KaraMaker<'a> { pub id: i32, pub name: &'a str, } -#[derive(Insertable)] +#[derive(Debug, Insertable, Queryable, Selectable)] #[diesel(table_name = iso_639_1)] -pub struct NewLanguage<'a> { +pub struct Language<'a> { pub code: &'a str, pub name_en: &'a str, pub is_iso: bool, @@ -58,7 +58,7 @@ pub struct AddKaraTag { pub value: Option<String>, } -impl<'a> From<api_v1::Language<'a>> for NewLanguage<'a> { +impl<'a> From<api_v1::Language<'a>> for Language<'a> { fn from(lang: api_v1::Language<'a>) -> Self { Self { code: lang.code, @@ -70,7 +70,8 @@ impl<'a> From<api_v1::Language<'a>> for NewLanguage<'a> { } // Then the queriable things -#[derive(Queryable)] + +#[derive(Queryable, Selectable)] #[diesel(table_name = tag)] pub struct Tag { pub id: i32, diff --git a/src/rust/liblektor-rs/src/database/unsafe_interface.rs b/src/rust/liblektor-rs/src/database/unsafe_interface.rs index ebd5e1af6c7801fd31b1c69e6eb3c629972fe214..7b744d412cadee89b94c60ab6a3cca42b31df88b 100644 --- a/src/rust/liblektor-rs/src/database/unsafe_interface.rs +++ b/src/rust/liblektor-rs/src/database/unsafe_interface.rs @@ -41,3 +41,33 @@ pub unsafe extern "C" fn lkt_database_close_connection(db: *mut LktDatabaseConne drop(db); } } + +/// Delete a kara by its repo and id in the repo ids. The passed database +/// pointer must be allocated by the [lkt_database_establish_connection] +/// function. The function will return `true` on success, `false` otherwise. Any +/// error will be logged. +#[no_mangle] +pub unsafe extern "C" fn lkt_database_delete_kara_by_repo( + db: *mut LktDatabaseConnection, + repo: u64, + kara: u64, +) -> bool { + let db = db.as_mut().expect("passing a nullptr as db handle"); + db.delete_kara_by_repo(repo, kara) + .map_err(|err| error!(target: "DB", "delete_kara_by_repo failed: {err}")) + .is_ok() +} + +/// Delete a kara by its local id. The passed database pointer must be allocated +/// by the [lkt_database_establish_connection] function. The function will +/// return `true` on success, `false` otherwise. Any error will be logged. +#[no_mangle] +pub unsafe extern "C" fn lkt_database_delete_kara_by_local_id( + db: *mut LktDatabaseConnection, + local_id: u64, +) -> bool { + let db = db.as_mut().expect("passing a nullptr as db handle"); + db.delete_kara_by_local_id(local_id) + .map_err(|err| error!(target: "DB", "delete_kara_by_local_id failed: {err}")) + .is_ok() +}