1use std::collections::{HashMap, HashSet};
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, Condvar, Mutex, MutexGuard};
13use std::time::Instant;
14
15use mentedb::{CognitiveConfig, MenteDb};
16use mentedb_core::error::MenteResult;
17use mentedb_embedding::provider::EmbeddingProvider;
18
19use crate::error::ApiError;
20
21pub const DEFAULT_DB: &str = "default";
23
24const MAX_NAME_LEN: usize = 128;
26
27#[derive(Clone, Debug)]
29pub struct DatabasesConfig {
30 pub max_open: usize,
32 pub auto_create: bool,
34}
35
36impl Default for DatabasesConfig {
37 fn default() -> Self {
38 Self {
39 max_open: 256,
40 auto_create: true,
41 }
42 }
43}
44
45struct SharedEmbedder(Arc<dyn EmbeddingProvider>);
47
48impl EmbeddingProvider for SharedEmbedder {
49 fn embed(&self, text: &str) -> MenteResult<Vec<f32>> {
50 self.0.embed(text)
51 }
52 fn embed_batch(&self, texts: &[&str]) -> MenteResult<Vec<Vec<f32>>> {
53 self.0.embed_batch(texts)
54 }
55 fn dimensions(&self) -> usize {
56 self.0.dimensions()
57 }
58 fn model_name(&self) -> &str {
59 self.0.model_name()
60 }
61}
62
63struct OpenDb {
64 db: Arc<MenteDb>,
65 last_used: Instant,
66}
67
68#[derive(Default)]
72struct Registry {
73 open: HashMap<String, OpenDb>,
74 closing: HashSet<String>,
75}
76
77pub struct Databases {
79 root: PathBuf,
80 default: Arc<MenteDb>,
81 embedder: Option<Arc<dyn EmbeddingProvider>>,
82 cognitive: CognitiveConfig,
83 config: DatabasesConfig,
84 registry: Mutex<Registry>,
85 closed: Condvar,
87}
88
89impl Databases {
90 pub fn open(
92 root: &Path,
93 embedder: Option<Arc<dyn EmbeddingProvider>>,
94 cognitive: CognitiveConfig,
95 config: DatabasesConfig,
96 ) -> MenteResult<Self> {
97 std::fs::create_dir_all(root)?;
98 let default = Arc::new(open_at(root, embedder.clone(), cognitive.clone())?);
99 Ok(Self {
100 root: root.to_path_buf(),
101 default,
102 embedder,
103 cognitive,
104 config,
105 registry: Mutex::new(Registry::default()),
106 closed: Condvar::new(),
107 })
108 }
109
110 pub fn default_db(&self) -> Arc<MenteDb> {
112 self.default.clone()
113 }
114
115 pub(crate) fn root(&self) -> &Path {
117 &self.root
118 }
119
120 pub fn embedder(&self) -> Option<Arc<dyn EmbeddingProvider>> {
122 self.embedder.clone()
123 }
124
125 fn lock(&self) -> MutexGuard<'_, Registry> {
126 self.registry.lock().unwrap_or_else(|e| e.into_inner())
127 }
128
129 fn wait_until_closed<'a>(
132 &'a self,
133 mut reg: MutexGuard<'a, Registry>,
134 name: &str,
135 ) -> MutexGuard<'a, Registry> {
136 while reg.closing.contains(name) {
137 reg = self.closed.wait(reg).unwrap_or_else(|e| e.into_inner());
138 }
139 reg
140 }
141
142 fn finish_closing(&self, handles: Vec<(String, Arc<MenteDb>)>) {
146 let mut names = Vec::with_capacity(handles.len());
147 for (name, handle) in handles {
148 if let Err(e) = handle.close() {
149 tracing::warn!(db = %name, error = %e, "closing idle database failed");
150 }
151 drop(handle);
152 names.push(name);
153 }
154 let mut reg = self.lock();
155 for name in names {
156 reg.closing.remove(&name);
157 }
158 drop(reg);
159 self.closed.notify_all();
160 }
161
162 fn dir(&self, name: &str) -> PathBuf {
163 self.root.join("dbs").join(name)
164 }
165
166 pub fn exists(&self, name: &str) -> bool {
168 name == DEFAULT_DB || self.dir(name).join(".mentedb-db").exists()
169 }
170
171 pub fn get(&self, name: &str, create: bool) -> Result<Arc<MenteDb>, ApiError> {
174 if name == DEFAULT_DB {
175 return Ok(self.default.clone());
176 }
177 validate_name(name)?;
178 let reg = self.lock();
179 let mut reg = self.wait_until_closed(reg, name);
180 if let Some(entry) = reg.open.get_mut(name) {
181 entry.last_used = Instant::now();
182 return Ok(entry.db.clone());
183 }
184 let dir = self.dir(name);
185 let marker = dir.join(".mentedb-db");
186 if !marker.exists() {
187 if !(create || self.config.auto_create) {
188 return Err(ApiError::NotFound(format!("database '{name}' not found")));
189 }
190 std::fs::create_dir_all(&dir)
191 .map_err(|e| ApiError::Internal(format!("create database '{name}': {e}")))?;
192 std::fs::write(&marker, name.as_bytes())
193 .map_err(|e| ApiError::Internal(format!("create database '{name}': {e}")))?;
194 }
195 let db = Arc::new(
198 open_at(&dir, self.embedder.clone(), self.cognitive.clone())
199 .map_err(|e| ApiError::Internal(format!("open database '{name}': {e}")))?,
200 );
201 reg.open.insert(
202 name.to_string(),
203 OpenDb {
204 db: db.clone(),
205 last_used: Instant::now(),
206 },
207 );
208 let evicted = take_idle_overflow(&mut reg.open, self.config.max_open);
209 for (evicted_name, _) in &evicted {
210 reg.closing.insert(evicted_name.clone());
211 }
212 drop(reg);
213 if !evicted.is_empty() {
214 self.finish_closing(evicted);
215 }
216 Ok(db)
217 }
218
219 pub fn list(&self) -> Vec<String> {
221 let mut names: Vec<String> = std::fs::read_dir(self.root.join("dbs"))
222 .into_iter()
223 .flatten()
224 .flatten()
225 .filter(|e| e.path().join(".mentedb-db").exists())
226 .filter_map(|e| e.file_name().into_string().ok())
227 .collect();
228 names.sort();
229 names
230 }
231
232 pub fn delete(&self, name: &str) -> Result<(), ApiError> {
234 if name == DEFAULT_DB {
235 return Err(ApiError::BadRequest(
236 "the default database cannot be deleted".into(),
237 ));
238 }
239 validate_name(name)?;
240 let reg = self.lock();
241 let mut reg = self.wait_until_closed(reg, name);
242 if let Some(entry) = reg.open.get(name)
243 && Arc::strong_count(&entry.db) > 1
244 {
245 return Err(ApiError::Conflict(format!(
246 "database '{name}' is in use; retry"
247 )));
248 }
249 if let Some(entry) = reg.open.remove(name) {
252 let _ = entry.db.close();
253 }
254 let dir = self.dir(name);
255 if !dir.join(".mentedb-db").exists() {
256 return Err(ApiError::NotFound(format!("database '{name}' not found")));
257 }
258 std::fs::remove_dir_all(&dir)
259 .map_err(|e| ApiError::Internal(format!("delete database '{name}': {e}")))
260 }
261
262 pub fn path_of(&self, name: &str) -> Result<PathBuf, ApiError> {
264 if name == DEFAULT_DB {
265 return Ok(self.root.clone());
266 }
267 validate_name(name)?;
268 Ok(self.dir(name))
269 }
270
271 pub fn open_handles(&self) -> Vec<(String, Arc<MenteDb>)> {
273 let mut all = vec![(DEFAULT_DB.to_string(), self.default.clone())];
274 let reg = self.lock();
275 all.extend(reg.open.iter().map(|(n, e)| (n.clone(), e.db.clone())));
276 all
277 }
278
279 pub fn close_all(&self) {
281 let handles: Vec<(String, Arc<MenteDb>)> = {
282 let mut reg = self.lock();
283 let drained: Vec<_> = reg.open.drain().map(|(n, e)| (n, e.db)).collect();
284 for (n, _) in &drained {
285 reg.closing.insert(n.clone());
286 }
287 drained
288 };
289 self.finish_closing(handles);
290 if let Err(e) = self.default.close() {
291 tracing::warn!(error = %e, "closing default database failed");
292 }
293 }
294}
295
296fn open_at(
297 dir: &Path,
298 embedder: Option<Arc<dyn EmbeddingProvider>>,
299 cognitive: CognitiveConfig,
300) -> MenteResult<MenteDb> {
301 match embedder {
302 Some(e) => {
303 MenteDb::open_with_embedder_and_config(dir, Box::new(SharedEmbedder(e)), cognitive)
304 }
305 None => MenteDb::open_with_config(dir, cognitive),
306 }
307}
308
309fn take_idle_overflow(
312 open: &mut HashMap<String, OpenDb>,
313 max_open: usize,
314) -> Vec<(String, Arc<MenteDb>)> {
315 let mut evicted = Vec::new();
316 while open.len() > max_open {
317 let victim = open
318 .iter()
319 .filter(|(_, e)| Arc::strong_count(&e.db) == 1)
320 .min_by_key(|(_, e)| e.last_used)
321 .map(|(n, _)| n.clone());
322 match victim {
323 Some(name) => {
324 if let Some(entry) = open.remove(&name) {
325 evicted.push((name, entry.db));
326 }
327 }
328 None => break,
330 }
331 }
332 evicted
333}
334
335pub fn validate_name(name: &str) -> Result<(), ApiError> {
338 let ok = !name.is_empty()
339 && name.len() <= MAX_NAME_LEN
340 && !name.starts_with('.')
341 && name
342 .chars()
343 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'));
344 if ok {
345 Ok(())
346 } else {
347 Err(ApiError::BadRequest(format!(
348 "invalid database name '{name}': use letters, digits, '-', '_' or '.', at most \
349 {MAX_NAME_LEN} characters, not starting with '.'"
350 )))
351 }
352}
353
354const READ_ONLY_POSTS: &[&str] = &[
356 "/recall",
357 "/search",
358 "/search/hybrid",
359 "/context",
360 "/query",
361 "/recall_for_action",
362];
363
364pub struct Db {
367 pub name: String,
368 pub handle: Arc<MenteDb>,
369}
370
371impl std::ops::Deref for Db {
372 type Target = MenteDb;
373 fn deref(&self) -> &MenteDb {
374 &self.handle
375 }
376}
377
378impl axum::extract::FromRequestParts<Arc<crate::state::AppState>> for Db {
379 type Rejection = ApiError;
380
381 async fn from_request_parts(
382 parts: &mut axum::http::request::Parts,
383 state: &Arc<crate::state::AppState>,
384 ) -> Result<Self, ApiError> {
385 use axum::extract::RawPathParams;
386 let params = RawPathParams::from_request_parts(parts, state)
387 .await
388 .map_err(|e| ApiError::BadRequest(e.to_string()))?;
389 let name = params
390 .iter()
391 .find(|(k, _)| *k == "db")
392 .map(|(_, v)| v.to_string())
393 .unwrap_or_else(|| DEFAULT_DB.to_string());
394 let path = parts.uri.path();
395 let write = match parts.method {
396 axum::http::Method::GET | axum::http::Method::HEAD => false,
397 axum::http::Method::POST => !READ_ONLY_POSTS.iter().any(|p| path.ends_with(p)),
398 _ => true,
399 };
400 if let Some(agent) = parts.extensions.get::<crate::auth::AuthenticatedAgent>() {
401 agent.authorize_db(&name, write)?;
402 }
403 let dbs = state.databases.clone();
404 let lookup = name.clone();
405 let handle = tokio::task::spawn_blocking(move || dbs.get(&lookup, false))
406 .await
407 .map_err(|e| ApiError::Internal(format!("open database task failed: {e}")))??;
408 Ok(Self { name, handle })
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 fn registry(dir: &Path, max_open: usize) -> Databases {
417 Databases::open(
418 dir,
419 None,
420 CognitiveConfig::default(),
421 DatabasesConfig {
422 max_open,
423 auto_create: true,
424 },
425 )
426 .unwrap()
427 }
428
429 #[test]
433 fn names_are_safe_path_segments() {
434 for ok in [
435 "alice",
436 "team-7",
437 "org_1",
438 "u.123",
439 "a.b.c",
440 "tenant.alice.notes",
441 "a..b",
442 &"a".repeat(128),
443 ] {
444 assert!(validate_name(ok).is_ok(), "{ok}");
445 }
446 for bad in [
447 "",
448 ".",
449 "..",
450 ".hidden",
451 "a/b",
452 "/abs",
453 "a/../b",
454 "../x",
455 "a\\b",
456 "x y",
457 "a\0b",
458 "caf\u{e9}",
459 &"a".repeat(129),
460 ] {
461 assert!(validate_name(bad).is_err(), "{bad}");
462 }
463 }
464
465 #[test]
466 fn dotted_names_open_as_one_directory() {
467 let tmp = tempfile::tempdir().unwrap();
468 let dbs = registry(tmp.path(), 4);
469 drop(dbs.get("tenant.alice.notes", false).unwrap());
470 assert!(
471 tmp.path()
472 .join("dbs/tenant.alice.notes/.mentedb-db")
473 .exists()
474 );
475 assert!(
476 !tmp.path().join("dbs/tenant").exists(),
477 "not nested directories"
478 );
479 }
480
481 #[test]
482 fn named_databases_are_isolated_directories() {
483 let tmp = tempfile::tempdir().unwrap();
484 let dbs = registry(tmp.path(), 8);
485 let a = dbs.get("alice", false).unwrap();
486 let b = dbs.get("bob", false).unwrap();
487 assert!(!Arc::ptr_eq(&a, &b));
488 assert!(tmp.path().join("dbs/alice/.mentedb-db").exists());
489 assert!(tmp.path().join("dbs/bob/.mentedb-db").exists());
490 assert_eq!(dbs.list(), vec!["alice".to_string(), "bob".to_string()]);
491 assert!(
492 Arc::ptr_eq(&a, &dbs.get("alice", false).unwrap()),
493 "cached handle"
494 );
495 }
496
497 #[test]
498 fn idle_databases_beyond_the_limit_are_closed() {
499 let tmp = tempfile::tempdir().unwrap();
500 let dbs = registry(tmp.path(), 2);
501 drop(dbs.get("a", false).unwrap());
502 drop(dbs.get("b", false).unwrap());
503 drop(dbs.get("c", false).unwrap());
504 let open: Vec<String> = dbs.open_handles().into_iter().map(|(n, _)| n).collect();
505 assert_eq!(open.len(), 3, "default plus two named: {open:?}");
506 assert!(
507 !open.contains(&"a".to_string()),
508 "least recently used closed"
509 );
510 assert!(dbs.get("a", false).is_ok());
512 }
513
514 #[test]
515 fn missing_database_is_404_without_auto_create() {
516 let tmp = tempfile::tempdir().unwrap();
517 let dbs = Databases::open(
518 tmp.path(),
519 None,
520 CognitiveConfig::default(),
521 DatabasesConfig {
522 max_open: 4,
523 auto_create: false,
524 },
525 )
526 .unwrap();
527 assert!(matches!(
528 dbs.get("nobody", false),
529 Err(ApiError::NotFound(_))
530 ));
531 assert!(dbs.get("nobody", true).is_ok(), "explicit create works");
532 }
533
534 #[test]
539 fn reopening_a_database_while_it_is_being_closed_waits_for_the_close() {
540 let tmp = tempfile::tempdir().unwrap();
541 let dbs = Arc::new(registry(tmp.path(), 2));
542 let names: Vec<String> = (0..8).map(|i| format!("t{i}")).collect();
543 let errors = Arc::new(std::sync::atomic::AtomicUsize::new(0));
544 let threads: Vec<_> = (0..16)
545 .map(|t| {
546 let dbs = dbs.clone();
547 let names = names.clone();
548 let errors = errors.clone();
549 std::thread::spawn(move || {
550 for i in 0..40 {
551 let name = &names[(t * 7 + i) % names.len()];
552 match dbs.get(name, false) {
553 Ok(db) => {
554 let _ = db.memory_count();
555 }
556 Err(e) => {
557 eprintln!("{name}: {e}");
558 errors.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
559 }
560 }
561 }
562 })
563 })
564 .collect();
565 for t in threads {
566 t.join().unwrap();
567 }
568 assert_eq!(errors.load(std::sync::atomic::Ordering::SeqCst), 0);
569 }
570
571 #[test]
572 fn delete_removes_the_directory() {
573 let tmp = tempfile::tempdir().unwrap();
574 let dbs = registry(tmp.path(), 4);
575 drop(dbs.get("gone", false).unwrap());
576 dbs.delete("gone").unwrap();
577 assert!(!tmp.path().join("dbs/gone").exists());
578 assert!(dbs.delete(DEFAULT_DB).is_err());
579 }
580}