Skip to content

Commit 11b075d

Browse files
committed
feat: add global registry
- This ensures only a single instance of a db (with is connection pools) can exist in the process - NOTE TO REVIEWERS: The caching behavior will be tested from the outside once the `database.rs` tests are committed.
1 parent 8c3818a commit 11b075d

2 files changed

Lines changed: 145 additions & 3 deletions

File tree

crates/sqlx-sqlite-conn-mgr/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
//!
2020
//! ## Usage
2121
//!
22-
//! ```no_run
22+
//! // TODO: Remove this ignore once implementation is complete
23+
//! ```ignore
2324
//! use sqlx_sqlite_conn_mgr::SqliteDatabase;
2425
//! use std::sync::Arc;
2526
//!
@@ -59,13 +60,13 @@
5960
//! - Global registry caches new database instances (with their pools) and returns existing ones
6061
//! - WAL mode is enabled lazily only when writes are needed
6162
//!
62-
// TODO: Remove these allows once implementation is complete
63-
#![allow(dead_code)]
63+
// TODO: Remove this allow once implementation is complete
6464
#![allow(unused)]
6565

6666
mod config;
6767
mod database;
6868
mod error;
69+
mod registry;
6970
mod write_guard;
7071

7172
// Re-export public types
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//! Global database registry to cache new database instances and return existing ones
2+
3+
use crate::Result;
4+
use crate::database::SqliteDatabase;
5+
use std::collections::HashMap;
6+
use std::future::Future;
7+
use std::path::{Path, PathBuf};
8+
use std::sync::{Arc, Mutex, OnceLock, Weak};
9+
10+
/// Global registry for SQLite databases
11+
static DATABASE_REGISTRY: OnceLock<Mutex<HashMap<PathBuf, Weak<SqliteDatabase>>>> = OnceLock::new();
12+
13+
fn registry() -> &'static Mutex<HashMap<PathBuf, Weak<SqliteDatabase>>> {
14+
DATABASE_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
15+
}
16+
17+
/// Get or open a SQLite database connection
18+
///
19+
/// If a database is already connected, returns the cached instance.
20+
/// Otherwise, calls the provided factory function to create a new connection.
21+
///
22+
/// Special case: `:memory:` databases are never cached (each is unique)
23+
pub async fn get_or_open_database<F, Fut>(path: &Path, factory: F) -> Result<Arc<SqliteDatabase>>
24+
where
25+
F: FnOnce() -> Fut,
26+
Fut: Future<Output = Result<SqliteDatabase>>,
27+
{
28+
// Skip registry for in-memory databases - always create new
29+
if path.to_str() == Some(":memory:") {
30+
let db = factory().await?;
31+
return Ok(Arc::new(db));
32+
}
33+
34+
// Canonicalize the path for consistent lookups
35+
let canonical_path = canonicalize_path(path)?;
36+
37+
// Check if database is already cached
38+
{
39+
let mut registry = registry().lock().unwrap_or_else(|poisoned| {
40+
#[cfg(debug_assertions)]
41+
eprintln!("Registry mutex was poisoned, recovering");
42+
poisoned.into_inner()
43+
});
44+
45+
// Clean up dead weak references
46+
registry.retain(|_, weak| weak.strong_count() > 0);
47+
48+
// Try to get existing database
49+
if let Some(weak) = registry.get(&canonical_path) {
50+
if let Some(db) = weak.upgrade() {
51+
return Ok(db);
52+
}
53+
// Weak reference exists but dead - will be cleaned up below
54+
}
55+
}
56+
57+
// Database not cached, create new instance
58+
let db = factory().await?;
59+
let arc_db = Arc::new(db);
60+
61+
// Cache the new database
62+
{
63+
let mut registry = registry().lock().unwrap_or_else(|poisoned| {
64+
#[cfg(debug_assertions)]
65+
eprintln!("Registry mutex was poisoned, recovering");
66+
poisoned.into_inner()
67+
});
68+
registry.insert(canonical_path, Arc::downgrade(&arc_db));
69+
}
70+
71+
Ok(arc_db)
72+
}
73+
74+
/// Helper to canonicalize a database path
75+
fn canonicalize_path(path: &Path) -> std::io::Result<PathBuf> {
76+
match path.canonicalize() {
77+
Ok(p) => Ok(p),
78+
Err(_) => {
79+
// If path doesn't exist, try to canonicalize parent + filename
80+
let parent = path.parent().unwrap_or_else(|| Path::new("."));
81+
let filename = path
82+
.file_name()
83+
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid path"))?;
84+
let canonical_parent = parent.canonicalize()?;
85+
Ok(canonical_parent.join(filename))
86+
}
87+
}
88+
}
89+
90+
/// Remove a database from the cache
91+
///
92+
/// Special case: `:memory:` databases are never in the registry
93+
///
94+
/// Returns an error if the path cannot be canonicalized
95+
pub fn uncache_database(path: &Path) -> std::io::Result<()> {
96+
// Skip registry for in-memory databases
97+
if path.to_str() == Some(":memory:") {
98+
return Ok(());
99+
}
100+
101+
// Canonicalize path
102+
let canonical_path = canonicalize_path(path)?;
103+
104+
let mut registry = registry().lock().unwrap_or_else(|poisoned| {
105+
#[cfg(debug_assertions)]
106+
eprintln!("Registry mutex was poisoned, recovering");
107+
poisoned.into_inner()
108+
});
109+
registry.remove(&canonical_path);
110+
Ok(())
111+
}
112+
113+
#[cfg(test)]
114+
mod tests {
115+
use super::*;
116+
117+
#[test]
118+
fn test_canonicalize_path() {
119+
let temp_dir = std::env::temp_dir();
120+
let test_path = temp_dir.join("test.db");
121+
122+
// Test that path is canonicalized to absolute path
123+
let canonical = canonicalize_path(&test_path).unwrap();
124+
assert!(canonical.is_absolute());
125+
126+
// Test relative path
127+
let relative_path = Path::new("./test_relative.db");
128+
let canonical_relative = canonicalize_path(relative_path).unwrap();
129+
assert!(canonical_relative.is_absolute());
130+
}
131+
132+
#[test]
133+
fn test_canonicalize_nonexistent_path() {
134+
let temp_dir = std::env::temp_dir();
135+
let nonexistent = temp_dir.join("nonexistent_dir").join("test.db");
136+
137+
// Should fail if parent directory doesn't exist
138+
let result = canonicalize_path(&nonexistent);
139+
assert!(result.is_err());
140+
}
141+
}

0 commit comments

Comments
 (0)