1- use once_cell :: sync :: OnceCell ;
1+ use crate :: storage :: { DatabaseBackend , DatabaseManager , DatabaseOperation } ;
22use redb:: { Database , ReadableTable , TableDefinition } ;
33use serde:: { Deserialize , Serialize } ;
44use std:: path:: { Path , PathBuf } ;
@@ -19,69 +19,65 @@ const PACKAGES_BY_ID: TableDefinition<&str, &str> = TableDefinition::new("packag
1919const 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
2824pub struct JmixIndex {
2925 db : Arc < Database > ,
26+ db_path : PathBuf ,
3027}
3128
3229impl 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) ]
265211pub 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