vibesql_storage/persistence/
mod.rs

1// ============================================================================
2// Database Persistence Module
3// ============================================================================
4//
5// Provides multiple formats for database persistence:
6//
7// 1. SQL dump format (human-readable, portable SQL statements):
8//    - `save`: SQL dump generation and serialization
9//    - `load`: SQL dump parsing and deserialization utilities
10//
11// 2. Binary format (fast, efficient):
12//    - `binary`: Binary serialization/deserialization
13//
14// 3. Compressed binary format (zstd-compressed, smallest size):
15//    - `binary`: Compressed binary serialization/deserialization
16//
17// 4. JSON format (structured, tool-friendly):
18//    - `json`: JSON serialization/deserialization
19//
20// The `save_sql_dump`, `save_binary`, `save_compressed`, `save_json`, and
21// `load_json` methods are implemented directly on the `Database` type via
22// impl blocks in their respective modules.
23//
24// Load utilities are exported for use by the CLI layer to parse and execute
25// dump files.
26
27use std::{fs, io::Read, path::Path};
28
29pub mod binary;
30pub mod json;
31pub mod load;
32mod save;
33
34#[cfg(test)]
35mod tests;
36
37/// Persistence format detection and auto-loading
38impl crate::Database {
39    /// Load database from file with automatic format detection
40    ///
41    /// Detects format based on:
42    /// 1. File extension (.vbsql for binary, .vbsqlz for compressed, .json for JSON, .sql for SQL
43    ///    dump)
44    /// 2. Magic number in file header (if extension is ambiguous)
45    ///
46    /// # Example
47    /// ```no_run
48    /// # use vibesql_storage::Database;
49    /// // Auto-detects format from extension and content
50    /// let db = Database::load("database.vbsql").unwrap();
51    /// let db2 = Database::load("database.vbsqlz").unwrap();
52    /// let db3 = Database::load("database.json").unwrap();
53    /// let db4 = Database::load("database.sql").unwrap();
54    /// ```
55    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, crate::StorageError> {
56        let path_ref = path.as_ref();
57        let format = detect_format(path_ref)?;
58
59        match format {
60            PersistenceFormat::Binary => Self::load_binary(path),
61            PersistenceFormat::BinaryCompressed => Self::load_compressed(path),
62            PersistenceFormat::Json => Self::load_json(path),
63            PersistenceFormat::Sql => {
64                // SQL dump requires executor for parsing, so we return an error
65                // directing users to use the executor layer's load_sql_dump function
66                Err(crate::StorageError::NotImplemented(
67                    "SQL dump loading requires the executor layer. \
68                     Use vibesql_executor::load_sql_dump() instead, or use binary format (.vbsql/.vbsqlz) or JSON format (.json)"
69                        .to_string(),
70                ))
71            }
72        }
73    }
74}
75
76/// Persistence format
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum PersistenceFormat {
79    /// SQL dump format (.sql) - human-readable text
80    Sql,
81    /// Binary format (.vbsql) - efficient binary
82    Binary,
83    /// Compressed binary format (.vbsqlz) - zstd-compressed binary
84    BinaryCompressed,
85    /// JSON format (.json) - structured, tool-friendly
86    Json,
87}
88
89/// Detect persistence format from file
90fn detect_format<P: AsRef<Path>>(path: P) -> Result<PersistenceFormat, crate::StorageError> {
91    let path_ref = path.as_ref();
92
93    // For .vbsqlz, we know it's compressed
94    if let Some(ext) = path_ref.extension() {
95        match ext.to_str() {
96            Some("vbsqlz") => return Ok(PersistenceFormat::BinaryCompressed),
97            Some("json") => return Ok(PersistenceFormat::Json),
98            Some("sql") => return Ok(PersistenceFormat::Sql),
99            _ => {}
100        }
101    }
102
103    // For .vbsql or unknown extensions, check magic number to distinguish
104    // between compressed and uncompressed binary formats
105    // (save() with compression feature creates compressed content even with .vbsql extension)
106    let mut file = fs::File::open(path_ref).map_err(|e| {
107        crate::StorageError::NotImplemented(format!("Failed to open file {:?}: {}", path_ref, e))
108    })?;
109
110    let mut magic = [0u8; 5];
111    if file.read_exact(&mut magic).is_ok() && &magic == b"VBSQL" {
112        return Ok(PersistenceFormat::Binary);
113    }
114
115    // Try reading as zstd-compressed (check for zstd magic number 0x28, 0xB5, 0x2F, 0xFD)
116    file = fs::File::open(path_ref).map_err(|e| {
117        crate::StorageError::NotImplemented(format!("Failed to open file {:?}: {}", path_ref, e))
118    })?;
119    let mut zstd_magic = [0u8; 4];
120    if file.read_exact(&mut zstd_magic).is_ok() && &zstd_magic == b"\x28\xB5\x2F\xFD" {
121        return Ok(PersistenceFormat::BinaryCompressed);
122    }
123
124    // Check for JSON by looking for opening brace
125    file = fs::File::open(path_ref).map_err(|e| {
126        crate::StorageError::NotImplemented(format!("Failed to open file {:?}: {}", path_ref, e))
127    })?;
128    let mut first_byte = [0u8; 1];
129    if file.read_exact(&mut first_byte).is_ok() && first_byte[0] == b'{' {
130        return Ok(PersistenceFormat::Json);
131    }
132
133    // Default to SQL if we can't determine
134    Ok(PersistenceFormat::Sql)
135}