soar_db/
connection.rs

1//! Database connection management.
2//!
3//! This module provides connection management for the soar database system.
4//! It supports multiple database types:
5//!
6//! - **Core database**: Tracks installed packages
7//! - **Metadata databases**: One per repository, contains package metadata
8//! - **Nests database**: Tracks nest configurations
9
10use std::{collections::HashMap, path::Path};
11
12use diesel::{sql_query, Connection, ConnectionError, RunQueryDsl, SqliteConnection};
13use tracing::{debug, trace};
14
15use crate::migration::{apply_migrations, migrate_json_to_jsonb, DbType};
16
17/// Database connection wrapper with migration support.
18pub struct DbConnection {
19    conn: SqliteConnection,
20}
21
22impl DbConnection {
23    /// Opens a database connection and runs migrations.
24    ///
25    /// # Arguments
26    ///
27    /// * `path` - Path to the SQLite database file
28    /// * `db_type` - Type of database for selecting correct migrations
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if the connection fails or migrations fail.
33    pub fn open<P: AsRef<Path>>(path: P, db_type: DbType) -> Result<Self, ConnectionError> {
34        let path_str = path.as_ref().to_string_lossy();
35        debug!(path = %path_str, db_type = ?db_type, "opening database connection");
36
37        let mut conn = SqliteConnection::establish(&path_str)?;
38        trace!("database connection established");
39
40        sql_query("PRAGMA journal_mode = WAL;")
41            .execute(&mut conn)
42            .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
43        trace!("WAL journal mode enabled");
44
45        apply_migrations(&mut conn, &db_type)
46            .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
47        trace!("migrations applied");
48
49        // Migrate text JSON to JSONB for databases we manage (Core, Nest)
50        // Metadata databases are generated externally and migrated on fetch
51        if matches!(db_type, DbType::Core | DbType::Nest) {
52            migrate_json_to_jsonb(&mut conn, db_type)
53                .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
54            trace!("JSON to JSONB migration completed");
55        }
56
57        debug!(path = %path_str, "database opened successfully");
58        Ok(Self {
59            conn,
60        })
61    }
62
63    /// Opens a database connection without running migrations.
64    ///
65    /// Use this when you know the database is already migrated.
66    pub fn open_without_migrations<P: AsRef<Path>>(path: P) -> Result<Self, ConnectionError> {
67        let path_str = path.as_ref().to_string_lossy();
68        debug!(path = %path_str, "opening database without migrations");
69
70        let mut conn = SqliteConnection::establish(&path_str)?;
71        trace!("database connection established");
72
73        // WAL mode for better concurrent access
74        sql_query("PRAGMA journal_mode = WAL;")
75            .execute(&mut conn)
76            .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
77        trace!("WAL journal mode enabled");
78
79        debug!(path = %path_str, "database opened successfully");
80        Ok(Self {
81            conn,
82        })
83    }
84
85    /// Opens a metadata database and migrates JSON text columns to JSONB.
86    ///
87    /// This is used for metadata databases that are generated externally (e.g., by rusqlite)
88    /// and may contain JSON stored as text instead of JSONB binary format.
89    ///
90    /// Does NOT run schema migrations since the schema is managed externally.
91    pub fn open_metadata<P: AsRef<Path>>(path: P) -> Result<Self, ConnectionError> {
92        let path_str = path.as_ref().to_string_lossy();
93        debug!(path = %path_str, "opening metadata database");
94
95        let mut conn = SqliteConnection::establish(&path_str)?;
96        trace!("metadata database connection established");
97
98        // WAL mode for better concurrent access
99        sql_query("PRAGMA journal_mode = WAL;")
100            .execute(&mut conn)
101            .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
102        trace!("WAL journal mode enabled");
103
104        // Migrate text JSON to JSONB binary format
105        migrate_json_to_jsonb(&mut conn, DbType::Metadata)
106            .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
107        trace!("JSON to JSONB migration completed");
108
109        debug!(path = %path_str, "metadata database opened successfully");
110        Ok(Self {
111            conn,
112        })
113    }
114
115    /// Gets a mutable reference to the underlying connection.
116    pub fn conn(&mut self) -> &mut SqliteConnection {
117        &mut self.conn
118    }
119}
120
121impl std::ops::Deref for DbConnection {
122    type Target = SqliteConnection;
123
124    fn deref(&self) -> &Self::Target {
125        &self.conn
126    }
127}
128
129impl std::ops::DerefMut for DbConnection {
130    fn deref_mut(&mut self) -> &mut Self::Target {
131        &mut self.conn
132    }
133}
134
135/// Manages database connections for the soar package manager.
136///
137/// This struct manages separate connections for:
138/// - The core database (installed packages)
139/// - Multiple metadata databases (one per repository)
140/// - The nests database (nest configurations)
141///
142/// # Example
143///
144/// ```ignore
145/// use soar_db::connection::DatabaseManager;
146///
147/// let manager = DatabaseManager::new("/path/to/db")?;
148///
149/// // Access installed packages
150/// let installed = manager.core().list_installed()?;
151///
152/// // Access repository metadata
153/// if let Some(metadata_conn) = manager.metadata("pkgforge") {
154///     let packages = metadata_conn.search("firefox")?;
155/// }
156/// ```
157pub struct DatabaseManager {
158    /// Core database connection (installed packages).
159    core: DbConnection,
160    /// Metadata database connections, keyed by repository name.
161    metadata: HashMap<String, DbConnection>,
162    /// Nests database connection.
163    nests: DbConnection,
164}
165
166impl DatabaseManager {
167    /// Creates a new database manager with the given base directory.
168    ///
169    /// # Arguments
170    ///
171    /// * `base_dir` - Base directory for database files
172    ///
173    /// The following databases will be created/opened:
174    /// - `{base_dir}/core.db` - Installed packages
175    /// - `{base_dir}/nests.db` - Nest configurations
176    ///
177    /// Metadata databases are added separately via `add_metadata_db`.
178    pub fn new<P: AsRef<Path>>(base_dir: P) -> Result<Self, ConnectionError> {
179        let base = base_dir.as_ref();
180        debug!(base_dir = %base.display(), "initializing database manager");
181
182        let core_path = base.join("core.db");
183        let nests_path = base.join("nests.db");
184
185        let core = DbConnection::open(&core_path, DbType::Core)?;
186        let nests = DbConnection::open(&nests_path, DbType::Nest)?;
187
188        debug!("database manager initialized with core and nests databases");
189        Ok(Self {
190            core,
191            metadata: HashMap::new(),
192            nests,
193        })
194    }
195
196    /// Adds or opens a metadata database for a repository.
197    ///
198    /// This method opens the metadata database and migrates any JSON text columns
199    /// to JSONB binary format. It does NOT run schema migrations since metadata
200    /// databases are generated externally (e.g., by rusqlite).
201    ///
202    /// # Arguments
203    ///
204    /// * `repo_name` - Name of the repository
205    /// * `path` - Path to the metadata database file
206    pub fn add_metadata_db<P: AsRef<Path>>(
207        &mut self,
208        repo_name: &str,
209        path: P,
210    ) -> Result<(), ConnectionError> {
211        debug!(repo_name = repo_name, "adding metadata database");
212        let conn = DbConnection::open_metadata(path)?;
213        self.metadata.insert(repo_name.to_string(), conn);
214        trace!(repo_name = repo_name, "metadata database added to manager");
215        Ok(())
216    }
217
218    /// Gets a mutable reference to the core database connection.
219    pub fn core(&mut self) -> &mut DbConnection {
220        &mut self.core
221    }
222
223    /// Gets a mutable reference to a metadata database connection.
224    ///
225    /// Returns `None` if no metadata database exists for the given repository.
226    pub fn metadata(&mut self, repo_name: &str) -> Option<&mut DbConnection> {
227        self.metadata.get_mut(repo_name)
228    }
229
230    /// Gets an iterator over all metadata database connections.
231    pub fn all_metadata(&mut self) -> impl Iterator<Item = (&String, &mut DbConnection)> {
232        self.metadata.iter_mut()
233    }
234
235    /// Gets a mutable reference to the nests database connection.
236    pub fn nests(&mut self) -> &mut DbConnection {
237        &mut self.nests
238    }
239
240    /// Returns the names of all loaded metadata databases.
241    pub fn metadata_names(&self) -> impl Iterator<Item = &String> {
242        self.metadata.keys()
243    }
244
245    /// Removes a metadata database connection.
246    pub fn remove_metadata_db(&mut self, repo_name: &str) -> Option<DbConnection> {
247        debug!(repo_name = repo_name, "removing metadata database");
248        self.metadata.remove(repo_name)
249    }
250}