Skip to main content

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