Skip to main content

dbx_core/storage/backup/
snapshot.rs

1//! Database snapshot save/load implementation
2
3use crate::engine::metadata::SchemaMetadata;
4pub use crate::engine::snapshot::DatabaseSnapshot;
5use crate::engine::snapshot::TableData;
6use crate::engine::{Database, WosVariant};
7use crate::error::{DbxError, DbxResult};
8use arrow::datatypes::Schema;
9use std::path::Path;
10use std::sync::Arc;
11
12impl Database {
13    /// Save in-memory database to file
14    ///
15    /// Only works for in-memory databases. Returns error for file-based DBs.
16    ///
17    /// # Example
18    ///
19    /// ```no_run
20    /// use dbx_core::Database;
21    ///
22    /// # fn main() -> dbx_core::DbxResult<()> {
23    /// let db = Database::open_in_memory()?;
24    /// db.execute_sql("CREATE TABLE users (id INT, name TEXT)")?;
25    /// db.execute_sql("INSERT INTO users VALUES (1, 'Alice')")?;
26    ///
27    /// // Save to file
28    /// db.save_to_file("backup.json")?;
29    /// # Ok(())
30    /// # }
31    /// ```
32    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> DbxResult<()> {
33        // 1. Check if this is an in-memory DB
34        if !self.is_in_memory() {
35            return Err(DbxError::InvalidOperation {
36                message: "save_to_file only works for in-memory databases".to_string(),
37                context: "Use flush() for file-based databases".to_string(),
38            });
39        }
40
41        // 2. Create snapshot
42        let snapshot = self.create_snapshot()?;
43
44        // 3. Serialize to JSON
45        let json = serde_json::to_string_pretty(&snapshot)
46            .map_err(|e| DbxError::Serialization(e.to_string()))?;
47
48        // 4. Write to file
49        std::fs::write(path, json)?;
50
51        Ok(())
52    }
53
54    /// Load database from file into in-memory database
55    ///
56    /// Creates a new in-memory DB and loads all data from file.
57    ///
58    /// # Example
59    ///
60    /// ```no_run
61    /// use dbx_core::Database;
62    ///
63    /// # fn main() -> dbx_core::DbxResult<()> {
64    /// // Load from file
65    /// let db = Database::load_from_file("backup.json")?;
66    ///
67    /// // Query data
68    /// let results = db.execute_sql("SELECT * FROM users")?;
69    /// # Ok(())
70    /// # }
71    /// ```
72    pub fn load_from_file<P: AsRef<Path>>(path: P) -> DbxResult<Self> {
73        // 1. Read file
74        let json = std::fs::read_to_string(path)?;
75
76        // 2. Deserialize snapshot
77        let snapshot: DatabaseSnapshot =
78            serde_json::from_str(&json).map_err(|e| DbxError::Serialization(e.to_string()))?;
79
80        // 3. Create new in-memory DB
81        let db = Self::open_in_memory()?;
82
83        // 4. Restore snapshot
84        db.restore_snapshot(snapshot)?;
85
86        Ok(db)
87    }
88
89    /// Check if this is an in-memory database
90    fn is_in_memory(&self) -> bool {
91        matches!(self.wos, WosVariant::InMemory(_))
92    }
93
94    /// Create a snapshot of the current database state
95    fn create_snapshot(&self) -> DbxResult<DatabaseSnapshot> {
96        let mut snapshot = DatabaseSnapshot::new();
97
98        // 1. Capture schemas
99        let schemas = self.table_schemas.read().unwrap();
100        for (table_name, schema) in schemas.iter() {
101            let metadata = SchemaMetadata::from(schema.as_ref());
102            snapshot.schemas.insert(table_name.clone(), metadata);
103        }
104        drop(schemas);
105
106        // 2. Capture indexes
107        let indexes = self.index_registry.read().unwrap();
108        snapshot.indexes = indexes.clone();
109        drop(indexes);
110
111        // 3. Capture table data
112        // Use row_counters to get table list (more reliable than WOS table_names for in-memory)
113        let table_list: Vec<String> = self
114            .row_counters
115            .iter()
116            .map(|entry| entry.key().clone())
117            .collect();
118
119        for table_name in table_list {
120            // Skip metadata tables
121            if table_name.starts_with("__meta__") {
122                continue;
123            }
124
125            let entries = self.wos.scan(&table_name, ..)?;
126            snapshot.tables.insert(table_name, TableData { entries });
127        }
128
129        // 4. Capture row counters
130        for entry in self.row_counters.iter() {
131            let table = entry.key().clone();
132            let counter = entry.value().load(std::sync::atomic::Ordering::SeqCst);
133            snapshot.row_counters.insert(table, counter);
134        }
135
136        Ok(snapshot)
137    }
138
139    /// Restore database state from snapshot
140    fn restore_snapshot(&self, snapshot: DatabaseSnapshot) -> DbxResult<()> {
141        // 1. Validate version
142        if snapshot.version != DatabaseSnapshot::CURRENT_VERSION {
143            return Err(DbxError::InvalidOperation {
144                message: format!("Unsupported snapshot version: {}", snapshot.version),
145                context: format!("Expected version {}", DatabaseSnapshot::CURRENT_VERSION),
146            });
147        }
148
149        // 2. Restore schemas (both table_schemas and schemas for compatibility)
150        let mut table_schemas = self.table_schemas.write().unwrap();
151        let mut schemas = self.schemas.write().unwrap();
152        for (table_name, metadata) in snapshot.schemas {
153            let schema = Arc::new(
154                Schema::try_from(metadata)
155                    .map_err(|e| DbxError::Schema(format!("Failed to restore schema: {}", e)))?,
156            );
157            table_schemas.insert(table_name.clone(), schema.clone());
158            schemas.insert(table_name, schema);
159        }
160        drop(table_schemas);
161        drop(schemas);
162
163        // 3. Restore indexes
164        let mut indexes = self.index_registry.write().unwrap();
165        *indexes = snapshot.indexes;
166        drop(indexes);
167
168        // 4. Restore table data
169        for (table_name, table_data) in snapshot.tables {
170            for (key, value) in table_data.entries {
171                self.wos.insert(&table_name, &key, &value)?;
172            }
173        }
174
175        // 5. Restore row counters
176        for (table, count) in snapshot.row_counters {
177            self.row_counters
178                .insert(table, std::sync::atomic::AtomicUsize::new(count));
179        }
180
181        Ok(())
182    }
183}
184
185// ════════════════════════════════════════════
186// DatabaseSnapshot Trait Implementation
187// ════════════════════════════════════════════
188
189impl crate::traits::DatabaseSnapshot for Database {
190    fn save_to_file(&self, path: &str) -> DbxResult<()> {
191        // Reuse existing implementation
192        Database::save_to_file(self, path)
193    }
194
195    fn load_from_file(path: &str) -> DbxResult<Self> {
196        // Reuse existing implementation
197        Database::load_from_file(path)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_is_in_memory() {
207        let db = Database::open_in_memory().unwrap();
208        assert!(db.is_in_memory());
209    }
210
211    #[test]
212    fn test_file_based_db_rejects_save() {
213        let temp_dir = tempfile::tempdir().unwrap();
214        let db = Database::open(temp_dir.path()).unwrap();
215
216        let temp_file = tempfile::NamedTempFile::new().unwrap();
217        let result = db.save_to_file(temp_file.path());
218
219        assert!(result.is_err());
220        assert!(result.unwrap_err().to_string().contains("in-memory"));
221    }
222}