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}