Skip to content

Commit 448fee3

Browse files
author
Christopher Skene
committed
Generalise database
1 parent e54f331 commit 448fee3

4 files changed

Lines changed: 688 additions & 171 deletions

File tree

src/models/middleware/types/jmix_index.rs

Lines changed: 119 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use once_cell::sync::OnceCell;
1+
use crate::storage::{DatabaseBackend, DatabaseManager, DatabaseOperation};
22
use redb::{Database, ReadableTable, TableDefinition};
33
use serde::{Deserialize, Serialize};
44
use std::path::{Path, PathBuf};
@@ -19,69 +19,65 @@ const PACKAGES_BY_ID: TableDefinition<&str, &str> = TableDefinition::new("packag
1919
const PACKAGES_BY_STUDY_UID: TableDefinition<&str, &str> =
2020
TableDefinition::new("packages_by_study_uid");
2121

22-
/// Global shared database instances mapped by database path
23-
/// This allows multiple storage locations in tests while sharing instances per path
24-
static GLOBAL_JMIX_DB: OnceCell<std::sync::Mutex<std::collections::HashMap<PathBuf, Arc<Database>>>> = OnceCell::new();
25-
2622
/// JMIX package index for fast lookups without filesystem walks
27-
/// Now uses a shared database instance to prevent concurrent access issues
23+
/// Now uses the generalized database manager to prevent concurrent access issues
2824
pub struct JmixIndex {
2925
db: Arc<Database>,
26+
db_path: PathBuf,
3027
}
3128

3229
impl JmixIndex {
33-
/// Open or create the index database using the shared global instance
30+
/// Open or create the index database using the generalized database manager
3431
pub fn open(db_path: &Path) -> Result<Self, String> {
35-
let db = get_or_create_shared_database(db_path)?;
36-
Ok(Self { db })
32+
let db_path_buf = db_path.to_path_buf();
33+
let db = DatabaseManager::global().get_or_create_database(&db_path_buf)?;
34+
35+
// Initialize tables if this is a new database
36+
let instance = Self { db: db.clone(), db_path: db_path_buf };
37+
instance.initialize_tables(&db)?;
38+
39+
Ok(instance)
3740
}
3841

3942
/// Create a new JmixIndex with the provided shared database (used for testing)
4043
#[cfg(test)]
41-
pub fn with_shared_db(db: Arc<Database>) -> Self {
42-
Self { db }
44+
pub fn with_shared_db(db: Arc<Database>, db_path: PathBuf) -> Self {
45+
Self { db, db_path }
4346
}
4447

4548
/// Index a new JMIX package
4649
pub fn index_package(&self, info: &JmixPackageInfo) -> Result<(), String> {
47-
let write_txn = self
48-
.db
49-
.begin_write()
50-
.map_err(|e| format!("Failed to begin write transaction: {}", e))?;
51-
52-
{
50+
let info_clone = info.clone();
51+
DatabaseOperation::write(&self.db, |write_txn| {
5352
// Serialize the package info
54-
let json = serde_json::to_string(info)
53+
let json = serde_json::to_string(&info_clone)
5554
.map_err(|e| format!("Failed to serialize package info: {}", e))?;
5655

57-
// Store by ID
58-
let mut table = write_txn
59-
.open_table(PACKAGES_BY_ID)
60-
.map_err(|e| format!("Failed to open packages_by_id table: {}", e))?;
61-
table
62-
.insert(info.id.as_str(), json.as_str())
63-
.map_err(|e| format!("Failed to insert package by ID: {}", e))?;
64-
}
65-
66-
{
67-
// Store by study UID (for fast queries)
68-
let json = serde_json::to_string(info)
69-
.map_err(|e| format!("Failed to serialize package info: {}", e))?;
70-
71-
let mut table = write_txn
72-
.open_table(PACKAGES_BY_STUDY_UID)
73-
.map_err(|e| format!("Failed to open packages_by_study_uid table: {}", e))?;
56+
{
57+
// Store by ID
58+
let mut table = write_txn
59+
.open_table(PACKAGES_BY_ID)
60+
.map_err(|e| format!("Failed to open packages_by_id table: {}", e))?;
61+
table
62+
.insert(info_clone.id.as_str(), json.as_str())
63+
.map_err(|e| format!("Failed to insert package by ID: {}", e))?;
64+
}
7465

75-
// Key format: "study_uid:id" to support multiple packages per study
76-
let key = format!("{}:{}", info.study_uid, info.id);
77-
table
78-
.insert(key.as_str(), json.as_str())
79-
.map_err(|e| format!("Failed to insert package by study UID: {}", e))?;
80-
}
66+
{
67+
// Store by study UID (for fast queries)
68+
let mut table = write_txn
69+
.open_table(PACKAGES_BY_STUDY_UID)
70+
.map_err(|e| format!("Failed to open packages_by_study_uid table: {}", e))?;
71+
72+
// Key format: "study_uid:id" to support multiple packages per study
73+
let key = format!("{}:{}", info_clone.study_uid, info_clone.id);
74+
table
75+
.insert(key.as_str(), json.as_str())
76+
.map_err(|e| format!("Failed to insert package by study UID: {}", e))?;
77+
}
8178

82-
write_txn
83-
.commit()
84-
.map_err(|e| format!("Failed to commit package index: {}", e))?;
79+
Ok(())
80+
})?;
8581

8682
tracing::debug!(
8783
"📇 Indexed JMIX package: id={}, study_uid={}",
@@ -93,57 +89,55 @@ impl JmixIndex {
9389

9490
/// Lookup a package by ID
9591
pub fn get_by_id(&self, id: &str) -> Result<Option<JmixPackageInfo>, String> {
96-
let read_txn = self
97-
.db
98-
.begin_read()
99-
.map_err(|e| format!("Failed to begin read transaction: {}", e))?;
100-
101-
let table = read_txn
102-
.open_table(PACKAGES_BY_ID)
103-
.map_err(|e| format!("Failed to open packages_by_id table: {}", e))?;
104-
105-
match table.get(id) {
106-
Ok(Some(value)) => {
107-
let json = value.value();
108-
let info: JmixPackageInfo = serde_json::from_str(json)
109-
.map_err(|e| format!("Failed to deserialize package info: {}", e))?;
110-
Ok(Some(info))
92+
let id_owned = id.to_string();
93+
DatabaseOperation::read(&self.db, |read_txn| {
94+
let table = read_txn
95+
.open_table(PACKAGES_BY_ID)
96+
.map_err(|e| format!("Failed to open packages_by_id table: {}", e))?;
97+
98+
match table.get(id_owned.as_str()) {
99+
Ok(Some(value)) => {
100+
let json = value.value();
101+
let info: JmixPackageInfo = serde_json::from_str(json)
102+
.map_err(|e| format!("Failed to deserialize package info: {}", e))?;
103+
Ok(Some(info))
104+
}
105+
Ok(None) => Ok(None),
106+
Err(e) => Err(format!("Failed to get package by ID: {}", e)),
111107
}
112-
Ok(None) => Ok(None),
113-
Err(e) => Err(format!("Failed to get package by ID: {}", e)),
114-
}
108+
})
115109
}
116110

117111
/// Query packages by study UID
118112
pub fn query_by_study_uid(&self, study_uid: &str) -> Result<Vec<JmixPackageInfo>, String> {
119-
let read_txn = self
120-
.db
121-
.begin_read()
122-
.map_err(|e| format!("Failed to begin read transaction: {}", e))?;
123-
124-
let table = read_txn
125-
.open_table(PACKAGES_BY_STUDY_UID)
126-
.map_err(|e| format!("Failed to open packages_by_study_uid table: {}", e))?;
127-
128-
let mut results = Vec::new();
129-
let prefix = format!("{}:", study_uid);
130-
131-
// Iterate over all entries and filter by prefix
132-
let iter = table
133-
.iter()
134-
.map_err(|e| format!("Failed to iterate packages_by_study_uid: {}", e))?;
135-
136-
for entry in iter {
137-
let (key, value) = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
138-
let key_str = key.value();
139-
140-
if key_str.starts_with(&prefix) {
141-
let json = value.value();
142-
let info: JmixPackageInfo = serde_json::from_str(json)
143-
.map_err(|e| format!("Failed to deserialize package info: {}", e))?;
144-
results.push(info);
113+
let study_uid_owned = study_uid.to_string();
114+
let results = DatabaseOperation::read(&self.db, |read_txn| {
115+
let table = read_txn
116+
.open_table(PACKAGES_BY_STUDY_UID)
117+
.map_err(|e| format!("Failed to open packages_by_study_uid table: {}", e))?;
118+
119+
let mut results = Vec::new();
120+
let prefix = format!("{}:", study_uid_owned);
121+
122+
// Iterate over all entries and filter by prefix
123+
let iter = table
124+
.iter()
125+
.map_err(|e| format!("Failed to iterate packages_by_study_uid: {}", e))?;
126+
127+
for entry in iter {
128+
let (key, value) = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
129+
let key_str = key.value();
130+
131+
if key_str.starts_with(&prefix) {
132+
let json = value.value();
133+
let info: JmixPackageInfo = serde_json::from_str(json)
134+
.map_err(|e| format!("Failed to deserialize package info: {}", e))?;
135+
results.push(info);
136+
}
145137
}
146-
}
138+
139+
Ok(results)
140+
})?;
147141

148142
tracing::debug!(
149143
"📇 Found {} packages for study_uid={}",
@@ -155,35 +149,33 @@ impl JmixIndex {
155149

156150
/// Remove a package from the index
157151
pub fn remove_package(&self, id: &str, study_uid: &str) -> Result<(), String> {
158-
let write_txn = self
159-
.db
160-
.begin_write()
161-
.map_err(|e| format!("Failed to begin write transaction: {}", e))?;
162-
163-
{
164-
// Remove from ID index
165-
let mut table = write_txn
166-
.open_table(PACKAGES_BY_ID)
167-
.map_err(|e| format!("Failed to open packages_by_id table: {}", e))?;
168-
table
169-
.remove(id)
170-
.map_err(|e| format!("Failed to remove package by ID: {}", e))?;
171-
}
152+
let id_owned = id.to_string();
153+
let study_uid_owned = study_uid.to_string();
154+
155+
DatabaseOperation::write(&self.db, |write_txn| {
156+
{
157+
// Remove from ID index
158+
let mut table = write_txn
159+
.open_table(PACKAGES_BY_ID)
160+
.map_err(|e| format!("Failed to open packages_by_id table: {}", e))?;
161+
table
162+
.remove(id_owned.as_str())
163+
.map_err(|e| format!("Failed to remove package by ID: {}", e))?;
164+
}
172165

173-
{
174-
// Remove from study UID index
175-
let mut table = write_txn
176-
.open_table(PACKAGES_BY_STUDY_UID)
177-
.map_err(|e| format!("Failed to open packages_by_study_uid table: {}", e))?;
178-
let key = format!("{}:{}", study_uid, id);
179-
table
180-
.remove(key.as_str())
181-
.map_err(|e| format!("Failed to remove package by study UID: {}", e))?;
182-
}
166+
{
167+
// Remove from study UID index
168+
let mut table = write_txn
169+
.open_table(PACKAGES_BY_STUDY_UID)
170+
.map_err(|e| format!("Failed to open packages_by_study_uid table: {}", e))?;
171+
let key = format!("{}:{}", study_uid_owned, id_owned);
172+
table
173+
.remove(key.as_str())
174+
.map_err(|e| format!("Failed to remove package by study UID: {}", e))?;
175+
}
183176

184-
write_txn
185-
.commit()
186-
.map_err(|e| format!("Failed to commit package removal: {}", e))?;
177+
Ok(())
178+
})?;
187179

188180
tracing::debug!("📇 Removed JMIX package from index: id={}", id);
189181
Ok(())
@@ -195,62 +187,16 @@ impl JmixIndex {
195187
}
196188
}
197189

198-
/// Get or create the shared database instance for a specific path
199-
fn get_or_create_shared_database(db_path: &Path) -> Result<Arc<Database>, String> {
200-
let db_path_buf = db_path.to_path_buf();
201-
202-
// Get or initialize the global database map
203-
let db_map = GLOBAL_JMIX_DB.get_or_init(|| {
204-
std::sync::Mutex::new(std::collections::HashMap::new())
205-
});
206-
207-
// Lock the map and check if we already have a database for this path
208-
let mut map = db_map.lock().map_err(|e| format!("Failed to lock database map: {}", e))?;
209-
210-
if let Some(existing_db) = map.get(&db_path_buf) {
211-
// Return existing database instance for this path
212-
tracing::debug!("🔄 Reusing existing database instance for: {}", db_path_buf.display());
213-
Ok(existing_db.clone())
214-
} else {
215-
// Create new database instance for this path
216-
tracing::debug!("🆕 Creating new database instance for: {}", db_path_buf.display());
217-
let db = init_database(&db_path_buf)?;
218-
map.insert(db_path_buf.clone(), db.clone());
219-
Ok(db)
220-
}
221-
}
222-
223-
/// Initialize a new database instance
224-
fn init_database(db_path: &Path) -> Result<Arc<Database>, String> {
225-
// Ensure parent directory exists
226-
if let Some(parent) = db_path.parent() {
227-
std::fs::create_dir_all(parent)
228-
.map_err(|e| format!("Failed to create index directory: {}", e))?;
190+
/// Implement DatabaseBackend trait for JmixIndex
191+
impl DatabaseBackend for JmixIndex {
192+
fn database_path(&self) -> PathBuf {
193+
self.db_path.clone()
229194
}
230195

231-
tracing::info!("🗄️ Initializing shared JMIX index database: {}", db_path.display());
232-
233-
let db = Database::create(db_path)
234-
.map_err(|e| format!("Failed to open JMIX index database: {}", e))?;
235-
236-
// Initialize tables
237-
let write_txn = db
238-
.begin_write()
239-
.map_err(|e| format!("Failed to begin write transaction: {}", e))?;
240-
{
241-
let _ = write_txn
242-
.open_table(PACKAGES_BY_ID)
243-
.map_err(|e| format!("Failed to open packages_by_id table: {}", e))?;
244-
let _ = write_txn
245-
.open_table(PACKAGES_BY_STUDY_UID)
246-
.map_err(|e| format!("Failed to open packages_by_study_uid table: {}", e))?;
196+
fn initialize_tables(&self, db: &Database) -> Result<(), String> {
197+
let table_definitions = &[&PACKAGES_BY_ID, &PACKAGES_BY_STUDY_UID];
198+
DatabaseManager::global().initialize_tables(db, table_definitions)
247199
}
248-
write_txn
249-
.commit()
250-
.map_err(|e| format!("Failed to commit table initialization: {}", e))?;
251-
252-
tracing::info!("✅ JMIX index database initialized successfully");
253-
Ok(Arc::new(db))
254200
}
255201

256202
/// Get or create the global JMIX index
@@ -263,8 +209,10 @@ pub fn get_jmix_index(store_root: &Path) -> Result<JmixIndex, String> {
263209
/// Create a new database instance directly (for testing)
264210
#[cfg(test)]
265211
pub fn create_test_index(db_path: &Path) -> Result<JmixIndex, String> {
266-
let db = init_database(db_path)?;
267-
Ok(JmixIndex::with_shared_db(db))
212+
let db = DatabaseManager::global().get_or_create_database(db_path)?;
213+
let instance = JmixIndex::with_shared_db(db.clone(), db_path.to_path_buf());
214+
instance.initialize_tables(&db)?;
215+
Ok(instance)
268216
}
269217

270218
/// Helper to get current Unix timestamp

0 commit comments

Comments
 (0)