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::{Surreal, engine::local::Db};
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::{RecordId, Surreal, engine::local::Mem, method::Stats};
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", "0"));
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", "1"));
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 const NUMBER_OF_USERS: usize = 100;
223 for i in 2..NUMBER_OF_USERS {
225 let user_id = RecordId::from(("users", i.to_string()));
226 let user = User {
227 id: user_id.clone(),
228 name: format!("User {}", i),
229 age: i as i32,
230 favorite_numbers: [i as i32; 7],
231 };
232 let _: Option<User> = db.create(user_id.clone()).content(user).await.unwrap();
233 }
234
235 let mut resp_new = db
236 .query("SELECT count() FROM users GROUP ALL")
238 .with_stats()
239 .await
240 .unwrap();
241 dbg!(&resp_new);
242 let res = resp_new.take(0).unwrap();
243 let cnt: Option<Count> = res.1.unwrap();
244 assert_eq!(cnt, Some(Count::new(NUMBER_OF_USERS)));
245 let stats_new: Stats = res.0;
246
247 let mut resp_old = db
248 .query("RETURN array::len((SELECT * FROM users))")
250 .with_stats()
251 .await
252 .unwrap();
253 dbg!(&resp_old);
254 let res = resp_old.take(0).unwrap();
255 let cnt: Option<usize> = res.1.unwrap();
256 assert_eq!(cnt, Some(NUMBER_OF_USERS));
257 let stats_old: Stats = res.0;
258
259 assert!(stats_new.execution_time.unwrap() < stats_old.execution_time.unwrap());
261
262 let result: Vec<User> = db.delete("users").await.unwrap();
263
264 assert_eq!(result.len(), NUMBER_OF_USERS);
265 assert!(result.contains(&john), "Result does not contain 'john'");
266 assert!(result.contains(&sally), "Result does not contain 'sally'");
267 }
268}