Skip to main content

execution_engine_core/framework/
database.rs

1// Copyright 2024 Vincents AI
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Temporary database management for test environments
5//!
6//! This module provides utilities for creating isolated, temporary SQLite databases
7//! for testing purposes. Databases are automatically cleaned up when dropped.
8//!
9//! # Example
10//! ```ignore
11//! use framework::database::TemporaryDatabase;
12//!
13//! let temp_db = TemporaryDatabase::new("my_test").unwrap();
14//! let conn_str = temp_db.connection_string(); // "sqlite:/path/to/my_test.db"
15//! // Database is cleaned up when temp_db goes out of scope
16//! ```
17
18use anyhow::Result;
19use std::path::{Path, PathBuf};
20use tempfile::TempDir;
21
22/// A temporary SQLite database that is automatically cleaned up on drop.
23///
24/// The database file is created in a temporary directory that is removed
25/// when this struct is dropped, ensuring no test artifacts remain.
26pub struct TemporaryDatabase {
27    /// The temporary directory containing the database
28    temp_dir: TempDir,
29    /// Path to the database file
30    db_path: PathBuf,
31    /// Name of the database
32    name: String,
33}
34
35impl TemporaryDatabase {
36    /// Create a new temporary database with the given name.
37    ///
38    /// The database file will be created as `{name}.db` in a temporary directory.
39    /// The file is touch()ed to ensure it exists on disk.
40    ///
41    /// # Arguments
42    /// * `name` - A name for the database (used in the filename)
43    ///
44    /// # Returns
45    /// A new TemporaryDatabase instance, or an error if creation fails.
46    pub fn new(name: &str) -> Result<Self> {
47        let temp_dir = TempDir::new()?;
48        let db_path = temp_dir.path().join(format!("{}.db", name));
49
50        // Create the database file
51        std::fs::File::create(&db_path)?;
52
53        Ok(Self {
54            temp_dir,
55            db_path,
56            name: name.to_string(),
57        })
58    }
59
60    /// Create a new temporary database with an initial schema.
61    ///
62    /// The schema SQL will be executed against the database after creation.
63    ///
64    /// # Arguments
65    /// * `name` - A name for the database
66    /// * `schema` - SQL statements to initialize the database
67    ///
68    /// # Returns
69    /// A new TemporaryDatabase with the schema applied.
70    pub fn with_schema(name: &str, schema: &str) -> Result<Self> {
71        let temp_db = Self::new(name)?;
72
73        // Apply the schema using rusqlite
74        let conn = rusqlite::Connection::open(temp_db.path())?;
75        conn.execute_batch(schema)?;
76
77        Ok(temp_db)
78    }
79
80    /// Get the path to the database file.
81    pub fn path(&self) -> &Path {
82        &self.db_path
83    }
84
85    /// Get a connection string suitable for SQLx or other SQLite clients.
86    ///
87    /// Returns a string in the format `sqlite:/path/to/database.db`
88    pub fn connection_string(&self) -> String {
89        format!("sqlite:{}", self.db_path.display())
90    }
91
92    /// Get the name of the database.
93    pub fn name(&self) -> &str {
94        &self.name
95    }
96
97    /// Get the directory containing the database.
98    pub fn directory(&self) -> &Path {
99        self.temp_dir.path()
100    }
101}
102
103/// A manager for multiple temporary databases.
104///
105/// Useful when tests need multiple isolated databases that should all
106/// be cleaned up together.
107pub struct TemporaryDatabaseManager {
108    databases: Vec<TemporaryDatabase>,
109}
110
111impl TemporaryDatabaseManager {
112    /// Create a new empty database manager.
113    pub fn new() -> Self {
114        Self {
115            databases: Vec::new(),
116        }
117    }
118
119    /// Create a new temporary database and track it for cleanup.
120    pub fn create_database(&mut self, name: &str) -> Result<&TemporaryDatabase> {
121        let db = TemporaryDatabase::new(name)?;
122        self.databases.push(db);
123        Ok(self.databases.last().unwrap())
124    }
125
126    /// Create a database with schema and track it for cleanup.
127    pub fn create_database_with_schema(
128        &mut self,
129        name: &str,
130        schema: &str,
131    ) -> Result<&TemporaryDatabase> {
132        let db = TemporaryDatabase::with_schema(name, schema)?;
133        self.databases.push(db);
134        Ok(self.databases.last().unwrap())
135    }
136
137    /// Get the number of managed databases.
138    pub fn count(&self) -> usize {
139        self.databases.len()
140    }
141
142    /// Get a database by name.
143    pub fn get(&self, name: &str) -> Option<&TemporaryDatabase> {
144        self.databases.iter().find(|db| db.name() == name)
145    }
146
147    /// Remove and drop a specific database by name.
148    pub fn remove(&mut self, name: &str) -> bool {
149        if let Some(pos) = self.databases.iter().position(|db| db.name() == name) {
150            self.databases.remove(pos);
151            true
152        } else {
153            false
154        }
155    }
156
157    /// Clear all databases (they will be dropped and cleaned up).
158    pub fn clear(&mut self) {
159        self.databases.clear();
160    }
161}
162
163impl Default for TemporaryDatabaseManager {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_temporary_database_exists() {
175        let db = TemporaryDatabase::new("test").unwrap();
176        assert!(db.path().exists());
177    }
178
179    #[test]
180    fn test_temporary_database_connection_string_format() {
181        let db = TemporaryDatabase::new("mydb").unwrap();
182        let conn_str = db.connection_string();
183        assert!(conn_str.starts_with("sqlite:"));
184        assert!(conn_str.contains("mydb"));
185    }
186
187    #[test]
188    fn test_temporary_database_manager() {
189        let mut manager = TemporaryDatabaseManager::new();
190
191        manager.create_database("db1").unwrap();
192        manager.create_database("db2").unwrap();
193
194        assert_eq!(manager.count(), 2);
195        assert!(manager.get("db1").is_some());
196        assert!(manager.get("db2").is_some());
197        assert!(manager.get("db3").is_none());
198
199        manager.remove("db1");
200        assert_eq!(manager.count(), 1);
201        assert!(manager.get("db1").is_none());
202    }
203}