mecomp_storage/db/
mod.rs

1#[cfg(feature = "db")]
2pub mod crud;
3#[cfg(feature = "db")]
4pub mod health;
5#[cfg(feature = "db")]
6pub(crate) mod queries;
7pub mod schemas;
8
9#[cfg(feature = "db")]
10use surrealdb::{engine::local::Db, Surreal};
11
12#[cfg(feature = "db")]
13#[cfg(not(tarpaulin_include))]
14static DB_DIR: once_cell::sync::OnceCell<std::path::PathBuf> = once_cell::sync::OnceCell::new();
15#[cfg(feature = "db")]
16#[cfg(not(tarpaulin_include))]
17static TEMP_DB_DIR: once_cell::sync::Lazy<tempfile::TempDir> = once_cell::sync::Lazy::new(|| {
18    tempfile::tempdir().expect("Failed to create temporary directory")
19});
20
21/// NOTE: if you change this, you must go through the schemas and update the index analyzer names
22pub const FULL_TEXT_SEARCH_ANALYZER_NAME: &str = "custom_analyzer";
23
24/// Set the path to the database.
25///
26/// # Errors
27///
28/// This function will return an error if the path cannot be set.
29#[cfg(feature = "db")]
30#[allow(clippy::missing_inline_in_public_items)]
31pub fn set_database_path(path: std::path::PathBuf) -> Result<(), crate::errors::Error> {
32    DB_DIR
33        .set(path)
34        .map_err(crate::errors::Error::DbPathSetError)?;
35    log::info!("Primed database path");
36    Ok(())
37}
38
39/// Initialize the database with the necessary tables.
40///
41/// # Errors
42///
43/// This function will return an error if the database cannot be initialized.
44#[cfg(feature = "db")]
45#[allow(clippy::missing_inline_in_public_items)]
46pub async fn init_database() -> surrealdb::Result<Surreal<Db>> {
47    let db = Surreal::new(DB_DIR
48        .get().cloned()
49        .unwrap_or_else(|| {
50            log::warn!("DB_DIR not set, defaulting to a temporary directory `{}`, this is likely a bug because `init_database` should be called before `db`", TEMP_DB_DIR.path().display());
51            TEMP_DB_DIR.path()
52            .to_path_buf()
53        })).await?;
54
55    db.use_ns("mecomp").use_db("music").await?;
56
57    register_custom_analyzer(&db).await?;
58    surrealqlx::register_tables!(
59        &db,
60        schemas::album::Album,
61        schemas::artist::Artist,
62        schemas::song::Song,
63        schemas::collection::Collection,
64        schemas::playlist::Playlist,
65        schemas::dynamic::DynamicPlaylist
66    )?;
67    #[cfg(feature = "analysis")]
68    surrealqlx::register_tables!(&db, schemas::analysis::Analysis)?;
69
70    Ok(db)
71}
72
73#[cfg(feature = "db")]
74pub(crate) async fn register_custom_analyzer<C>(db: &Surreal<C>) -> surrealdb::Result<()>
75where
76    C: surrealdb::Connection,
77{
78    use queries::define_analyzer;
79    use surrealdb::sql::Tokenizer;
80
81    db.query(define_analyzer(
82        FULL_TEXT_SEARCH_ANALYZER_NAME,
83        Some(Tokenizer::Class),
84        &[
85            "ascii",
86            "lowercase",
87            "edgengram(1, 10)",
88            "snowball(English)",
89        ],
90    ))
91    .await?;
92
93    Ok(())
94}
95
96#[cfg(test)]
97mod test {
98    use super::schemas::{
99        album::Album, artist::Artist, collection::Collection, dynamic::DynamicPlaylist,
100        playlist::Playlist, song::Song,
101    };
102    use super::*;
103
104    use surrealdb::engine::local::Mem;
105    use surrealqlx::traits::Table;
106
107    #[tokio::test]
108    async fn test_register_tables() -> anyhow::Result<()> {
109        // use an in-memory db for testing
110        let db = Surreal::new::<Mem>(()).await?;
111        db.use_ns("test").use_db("test").await?;
112
113        // register the custom analyzer
114        register_custom_analyzer(&db).await?;
115
116        // first we init all the table to ensure that the queries made by the macro work without error
117        <Album as Table>::init_table(&db).await?;
118        <Artist as Table>::init_table(&db).await?;
119        <Song as Table>::init_table(&db).await?;
120        <Collection as Table>::init_table(&db).await?;
121        <Playlist as Table>::init_table(&db).await?;
122        <DynamicPlaylist as Table>::init_table(&db).await?;
123        // then we try initializing one of the tables again to ensure that initialization won't mess with existing tables/data
124        <Album as Table>::init_table(&db).await?;
125
126        Ok(())
127    }
128}