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
21pub const FULL_TEXT_SEARCH_ANALYZER_NAME: &str = "custom_analyzer";
23
24#[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#[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 queries::relations::define_relation_tables(&db).await?;
71
72 Ok(db)
73}
74
75#[cfg(feature = "db")]
76pub(crate) async fn register_custom_analyzer<C>(db: &Surreal<C>) -> surrealdb::Result<()>
77where
78 C: surrealdb::Connection,
79{
80 use queries::define_analyzer;
81 use surrealdb::sql::Tokenizer;
82
83 db.query(define_analyzer(
84 FULL_TEXT_SEARCH_ANALYZER_NAME,
85 Some(Tokenizer::Class),
86 &[
87 "ascii",
88 "lowercase",
89 "edgengram(1, 10)",
90 "snowball(English)",
91 ],
92 ))
93 .await?;
94
95 Ok(())
96}
97
98#[cfg(test)]
99mod test {
100 use super::schemas::{
101 album::Album, artist::Artist, collection::Collection, dynamic::DynamicPlaylist,
102 playlist::Playlist, song::Song,
103 };
104 use super::*;
105
106 use surrealdb::engine::local::Mem;
107 use surrealqlx::traits::Table;
108
109 #[tokio::test]
110 async fn test_register_tables() -> anyhow::Result<()> {
111 let db = Surreal::new::<Mem>(()).await?;
113 db.use_ns("test").use_db("test").await?;
114
115 register_custom_analyzer(&db).await?;
117
118 <Album as Table>::init_table(&db).await?;
120 <Artist as Table>::init_table(&db).await?;
121 <Song as Table>::init_table(&db).await?;
122 <Collection as Table>::init_table(&db).await?;
123 <Playlist as Table>::init_table(&db).await?;
124 <DynamicPlaylist as Table>::init_table(&db).await?;
125
126 queries::relations::define_relation_tables(&db).await?;
128
129 <Album as Table>::init_table(&db).await?;
131
132 Ok(())
133 }
134}
135
136#[cfg(test)]
137mod minimal_reproduction {
138 use serde::{Deserialize, Serialize};
141 use surrealdb::{engine::local::Mem, method::Stats, RecordId, Surreal};
142
143 use crate::db::queries::generic::{count, Count};
144
145 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
146 struct User {
147 id: RecordId,
148 name: String,
149 age: i32,
150 favorite_numbers: [i32; 7],
151 }
152
153 static SCHEMA_SQL: &str = r"
154 BEGIN;
155 DEFINE TABLE users SCHEMAFULL;
156 COMMIT;
157 BEGIN;
158 DEFINE FIELD id ON users TYPE record;
159 DEFINE FIELD name ON users TYPE string;
160 DEFINE FIELD age ON users TYPE int;
161 DEFINE FIELD favorite_numbers ON users TYPE array<int>;
162 COMMIT;
163 BEGIN;
164 DEFINE INDEX users_name_unique_index ON users FIELDS name UNIQUE;
165 DEFINE INDEX users_age_normal_index ON users FIELDS age;
166 DEFINE INDEX users_favorite_numbers_vector_index ON users FIELDS favorite_numbers MTREE DIMENSION 7;
167 ";
168
169 #[tokio::test]
170 async fn minimal_reproduction() {
171 let db = Surreal::new::<Mem>(()).await.unwrap();
172 db.use_ns("test").use_db("test").await.unwrap();
173
174 db.query(SCHEMA_SQL).await.unwrap();
175
176 let cnt: Option<Count> = db
177 .query(count("users"))
179 .await
180 .unwrap()
181 .take(0)
182 .unwrap();
183
184 assert_eq!(cnt, Some(Count::new(0)));
185
186 let john_id = RecordId::from(("users", "1"));
187 let john = User {
188 id: john_id.clone(),
189 name: "John".to_string(),
190 age: 42,
191 favorite_numbers: [1, 2, 3, 4, 5, 6, 7],
192 };
193
194 let sally_id = RecordId::from(("users", "2"));
195 let sally = User {
196 id: sally_id.clone(),
197 name: "Sally".to_string(),
198 age: 24,
199 favorite_numbers: [8, 9, 10, 11, 12, 13, 14],
200 };
201
202 let result: Option<User> = db
203 .create(john_id.clone())
204 .content(john.clone())
205 .await
206 .unwrap();
207
208 assert_eq!(result, Some(john.clone()));
209
210 let result: Option<User> = db
211 .create(sally_id.clone())
212 .content(sally.clone())
213 .await
214 .unwrap();
215
216 assert_eq!(result, Some(sally.clone()));
217
218 let result: Option<User> = db.select(john_id).await.unwrap();
219
220 assert_eq!(result, Some(john.clone()));
221
222 let mut resp_new = db
223 .query("SELECT count() FROM users GROUP ALL")
225 .with_stats()
226 .await
227 .unwrap();
228 dbg!(&resp_new);
229 let res = resp_new.take(0).unwrap();
230 let cnt: Option<Count> = res.1.unwrap();
231 assert_eq!(cnt, Some(Count::new(2)));
232 let stats_new: Stats = res.0;
233
234 let mut resp_old = db
235 .query("RETURN array::len((SELECT * FROM users))")
237 .with_stats()
238 .await
239 .unwrap();
240 dbg!(&resp_old);
241 let res = resp_old.take(0).unwrap();
242 let cnt: Option<usize> = res.1.unwrap();
243 assert_eq!(cnt, Some(2));
244 let stats_old: Stats = res.0;
245
246 assert!(stats_new.execution_time.unwrap() < stats_old.execution_time.unwrap());
248
249 let result: Vec<User> = db.delete("users").await.unwrap();
250
251 assert_eq!(result, vec![john.clone(), sally.clone()]);
252 }
253}