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}