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 in read-only mode.
63    ///
64    /// Uses `immutable=1` to skip all locking and WAL handling, which avoids
65    /// needing to create `-shm`/`-wal` files in root-owned directories.
66    /// Skips WAL mode, migrations, and JSON-to-JSONB conversion since
67    /// all of those require write access. Suitable for reading system
68    /// databases owned by root.
69    pub fn open_readonly<P: AsRef<Path>>(path: P) -> Result<Self, ConnectionError> {
70        let path_str = format!(
71            "file:{}?mode=ro&immutable=1",
72            path.as_ref().to_string_lossy()
73        );
74        debug!(path = %path_str, "opening database in read-only mode");
75
76        let conn = SqliteConnection::establish(&path_str)?;
77        trace!("read-only database connection established");
78
79        debug!(path = %path_str, "read-only database opened successfully");
80        Ok(Self {
81            conn,
82        })
83    }
84
85    /// Opens a database connection without running migrations.
86    ///
87    /// Use this when you know the database is already migrated.
88    pub fn open_without_migrations<P: AsRef<Path>>(path: P) -> Result<Self, ConnectionError> {
89        let path_str = path.as_ref().to_string_lossy();
90        debug!(path = %path_str, "opening database without migrations");
91
92        let mut conn = SqliteConnection::establish(&path_str)?;
93        trace!("database connection established");
94
95        // WAL mode for better concurrent access
96        sql_query("PRAGMA journal_mode = WAL;")
97            .execute(&mut conn)
98            .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
99        trace!("WAL journal mode enabled");
100
101        debug!(path = %path_str, "database opened successfully");
102        Ok(Self {
103            conn,
104        })
105    }
106
107    /// Opens a metadata database and migrates JSON text columns to JSONB.
108    ///
109    /// This is used for metadata databases that are generated externally (e.g., by rusqlite)
110    /// and may contain JSON stored as text instead of JSONB binary format.
111    ///
112    /// Does NOT run schema migrations since the schema is managed externally.
113    pub fn open_metadata<P: AsRef<Path>>(path: P) -> Result<Self, ConnectionError> {
114        let path_str = path.as_ref().to_string_lossy();
115        debug!(path = %path_str, "opening metadata database");
116
117        let mut conn = SqliteConnection::establish(&path_str)?;
118        trace!("metadata database connection established");
119
120        // WAL mode for better concurrent access
121        sql_query("PRAGMA journal_mode = WAL;")
122            .execute(&mut conn)
123            .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
124        trace!("WAL journal mode enabled");
125
126        // Migrate text JSON to JSONB binary format
127        migrate_json_to_jsonb(&mut conn, DbType::Metadata)
128            .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
129        trace!("JSON to JSONB migration completed");
130
131        debug!(path = %path_str, "metadata database opened successfully");
132        Ok(Self {
133            conn,
134        })
135    }
136
137    /// Gets a mutable reference to the underlying connection.
138    pub fn conn(&mut self) -> &mut SqliteConnection {
139        &mut self.conn
140    }
141}
142
143impl std::ops::Deref for DbConnection {
144    type Target = SqliteConnection;
145
146    fn deref(&self) -> &Self::Target {
147        &self.conn
148    }
149}
150
151impl std::ops::DerefMut for DbConnection {
152    fn deref_mut(&mut self) -> &mut Self::Target {
153        &mut self.conn
154    }
155}
156
157/// Manages database connections for the soar package manager.
158///
159/// This struct manages separate connections for:
160/// - The core database (installed packages)
161/// - Multiple metadata databases (one per repository)
162///
163/// # Example
164///
165/// ```ignore
166/// use soar_db::connection::DatabaseManager;
167///
168/// let manager = DatabaseManager::new("/path/to/db")?;
169///
170/// // Access installed packages
171/// let installed = manager.core().list_installed()?;
172///
173/// // Access repository metadata
174/// if let Some(metadata_conn) = manager.metadata("pkgforge") {
175///     let packages = metadata_conn.search("firefox")?;
176/// }
177/// ```
178pub struct DatabaseManager {
179    /// Core database connection (installed packages).
180    core: DbConnection,
181    /// Metadata database connections, keyed by repository name.
182    metadata: HashMap<String, DbConnection>,
183}
184
185impl DatabaseManager {
186    /// Creates a new database manager with the given base directory.
187    ///
188    /// # Arguments
189    ///
190    /// * `base_dir` - Base directory for database files
191    ///
192    /// The following databases will be created/opened:
193    /// - `{base_dir}/core.db` - Installed packages
194    ///
195    /// Metadata databases are added separately via `add_metadata_db`.
196    pub fn new<P: AsRef<Path>>(base_dir: P) -> Result<Self, ConnectionError> {
197        let base = base_dir.as_ref();
198        debug!(base_dir = %base.display(), "initializing database manager");
199
200        let core_path = base.join("core.db");
201
202        let core = DbConnection::open(&core_path, DbType::Core)?;
203
204        debug!("database manager initialized");
205        Ok(Self {
206            core,
207            metadata: HashMap::new(),
208        })
209    }
210
211    /// Adds or opens a metadata database for a repository.
212    ///
213    /// This method opens the metadata database and migrates any JSON text columns
214    /// to JSONB binary format. It does NOT run schema migrations since metadata
215    /// databases are generated externally (e.g., by rusqlite).
216    ///
217    /// # Arguments
218    ///
219    /// * `repo_name` - Name of the repository
220    /// * `path` - Path to the metadata database file
221    pub fn add_metadata_db<P: AsRef<Path>>(
222        &mut self,
223        repo_name: &str,
224        path: P,
225    ) -> Result<(), ConnectionError> {
226        debug!(repo_name = repo_name, "adding metadata database");
227        let conn = DbConnection::open_metadata(path)?;
228        self.metadata.insert(repo_name.to_string(), conn);
229        trace!(repo_name = repo_name, "metadata database added to manager");
230        Ok(())
231    }
232
233    /// Gets a mutable reference to the core database connection.
234    pub fn core(&mut self) -> &mut DbConnection {
235        &mut self.core
236    }
237
238    /// Gets a mutable reference to a metadata database connection.
239    ///
240    /// Returns `None` if no metadata database exists for the given repository.
241    pub fn metadata(&mut self, repo_name: &str) -> Option<&mut DbConnection> {
242        self.metadata.get_mut(repo_name)
243    }
244
245    /// Gets an iterator over all metadata database connections.
246    pub fn all_metadata(&mut self) -> impl Iterator<Item = (&String, &mut DbConnection)> {
247        self.metadata.iter_mut()
248    }
249
250    /// Returns the names of all loaded metadata databases.
251    pub fn metadata_names(&self) -> impl Iterator<Item = &String> {
252        self.metadata.keys()
253    }
254
255    /// Removes a metadata database connection.
256    pub fn remove_metadata_db(&mut self, repo_name: &str) -> Option<DbConnection> {
257        debug!(repo_name = repo_name, "removing metadata database");
258        self.metadata.remove(repo_name)
259    }
260}