Skip to main content

dbx_core/storage/backup/
snapshot.rs

1//! Database snapshot save/load implementation
2
3use crate::engine::Database;
4use crate::engine::metadata::SchemaMetadata;
5pub use crate::engine::snapshot::DatabaseSnapshot;
6use crate::engine::snapshot::TableData;
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 (no file persistence)
90    fn is_in_memory(&self) -> bool {
91        self.file_wos.is_none()
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_for_table(&table_name).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_for_table(&table_name)
172                    .insert(&table_name, &key, &value)?;
173            }
174        }
175
176        // 5. Restore row counters
177        for (table, count) in snapshot.row_counters {
178            self.row_counters
179                .insert(table, std::sync::atomic::AtomicUsize::new(count));
180        }
181
182        Ok(())
183    }
184}
185
186// ════════════════════════════════════════════
187// DatabaseSnapshot Trait Implementation
188// ════════════════════════════════════════════
189
190impl crate::traits::DatabaseSnapshot for Database {
191    fn save_to_file(&self, path: &str) -> DbxResult<()> {
192        // Reuse existing implementation
193        Database::save_to_file(self, path)
194    }
195
196    fn load_from_file(path: &str) -> DbxResult<Self> {
197        // Reuse existing implementation
198        Database::load_from_file(path)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_is_in_memory() {
208        let db = Database::open_in_memory().unwrap();
209        assert!(db.is_in_memory());
210    }
211
212    #[test]
213    fn test_file_based_db_rejects_save() {
214        let temp_dir = tempfile::tempdir().unwrap();
215        let db = Database::open(temp_dir.path()).unwrap();
216
217        let temp_file = tempfile::NamedTempFile::new().unwrap();
218        let result = db.save_to_file(temp_file.path());
219
220        assert!(result.is_err());
221        assert!(result.unwrap_err().to_string().contains("in-memory"));
222    }
223}