vibesql_storage/persistence/binary/
mod.rs

1// ============================================================================
2// Binary Persistence Format
3// ============================================================================
4//
5// Efficient binary serialization for vibesql databases.
6//
7// File Format:
8// - Header (16 bytes): Magic number, version, flags
9// - Catalog section: Schemas, tables, indexes, roles
10// - Data section: Table data with type tags
11//
12// Uses little-endian byte order for cross-platform compatibility.
13
14use std::{
15    fs::File,
16    io::{BufReader, BufWriter, Write},
17    path::Path,
18};
19
20use crate::{Database, StorageError};
21
22// Public submodules
23pub mod catalog;
24pub mod data;
25pub mod expression;
26pub mod format;
27pub mod io;
28pub mod value;
29
30// Re-export public API
31pub use catalog::{read_catalog, read_catalog_v, write_catalog};
32pub use data::{read_data, write_data};
33pub use expression::{read_expression, write_expression};
34pub use format::{read_header, write_header};
35
36impl Database {
37    /// Save database in efficient binary format
38    ///
39    /// Binary format is faster and more compact than SQL dumps.
40    /// Use `.vbsql` extension to indicate binary format.
41    ///
42    /// # Example
43    /// ```no_run
44    /// # use vibesql_storage::Database;
45    /// let db = Database::new();
46    /// db.save_binary("database.vbsql").unwrap();
47    /// ```
48    pub fn save_binary<P: AsRef<Path>>(&self, path: P) -> Result<(), StorageError> {
49        let file = File::create(path)
50            .map_err(|e| StorageError::NotImplemented(format!("Failed to create file: {}", e)))?;
51
52        let mut writer = BufWriter::new(file);
53
54        // Write header
55        write_header(&mut writer)?;
56
57        // Write catalog section
58        write_catalog(&mut writer, self)?;
59
60        // Write data section
61        write_data(&mut writer, self)?;
62
63        writer
64            .flush()
65            .map_err(|e| StorageError::NotImplemented(format!("Failed to flush: {}", e)))?;
66
67        Ok(())
68    }
69
70    /// Load database from binary format
71    ///
72    /// Reads a binary `.vbsql` file and reconstructs the database.
73    ///
74    /// # Example
75    /// ```no_run
76    /// # use vibesql_storage::Database;
77    /// let db = Database::load_binary("database.vbsql").unwrap();
78    /// ```
79    pub fn load_binary<P: AsRef<Path>>(path: P) -> Result<Self, StorageError> {
80        let file = File::open(path.as_ref()).map_err(|e| {
81            StorageError::NotImplemented(format!("Failed to open file {:?}: {}", path.as_ref(), e))
82        })?;
83
84        let mut reader = BufReader::new(file);
85
86        // Read and validate header, get version for backward compatibility
87        let version = read_header(&mut reader)?;
88
89        // Read catalog section with version awareness
90        let mut db = read_catalog_v(&mut reader, version)?;
91
92        // Read data section
93        read_data(&mut reader, &mut db)?;
94
95        Ok(db)
96    }
97
98    /// Save database in default format
99    ///
100    /// Uses compressed format when `compression` feature is enabled (default),
101    /// otherwise falls back to uncompressed binary format.
102    ///
103    /// # Example
104    /// ```no_run
105    /// # use vibesql_storage::Database;
106    /// let db = Database::new();
107    /// db.save("database.vbsql").unwrap();
108    /// ```
109    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), StorageError> {
110        #[cfg(feature = "compression")]
111        {
112            self.save_compressed(path)
113        }
114        #[cfg(not(feature = "compression"))]
115        {
116            self.save_binary(path)
117        }
118    }
119
120    /// Save database in uncompressed binary format
121    ///
122    /// Use this if you need uncompressed `.vbsql` files (e.g., for debugging
123    /// or when compression overhead is not desired).
124    ///
125    /// # Example
126    /// ```no_run
127    /// # use vibesql_storage::Database;
128    /// let db = Database::new();
129    /// db.save_uncompressed("database.vbsql").unwrap();
130    /// ```
131    pub fn save_uncompressed<P: AsRef<Path>>(&self, path: P) -> Result<(), StorageError> {
132        self.save_binary(path)
133    }
134
135    /// Save database in compressed binary format (zstd compression)
136    ///
137    /// Creates a `.vbsqlz` file containing zstd-compressed binary data.
138    /// Typically 50-70% smaller than uncompressed `.vbsql` files.
139    ///
140    /// Note: This method requires the `compression` feature to be enabled.
141    ///
142    /// # Example
143    /// ```no_run
144    /// # use vibesql_storage::Database;
145    /// let db = Database::new();
146    /// db.save_compressed("database.vbsqlz").unwrap();
147    /// ```
148    #[cfg(feature = "compression")]
149    pub fn save_compressed<P: AsRef<Path>>(&self, path: P) -> Result<(), StorageError> {
150        // First, save to temporary in-memory buffer
151        let mut uncompressed_data = Vec::new();
152        {
153            let mut writer = BufWriter::new(&mut uncompressed_data);
154
155            // Write header
156            write_header(&mut writer)?;
157
158            // Write catalog section
159            write_catalog(&mut writer, self)?;
160
161            // Write data section
162            write_data(&mut writer, self)?;
163
164            writer
165                .flush()
166                .map_err(|e| StorageError::NotImplemented(format!("Failed to flush: {}", e)))?;
167        }
168
169        // Compress the data using zstd (level 3 - good balance of speed and compression)
170        let compressed_data = zstd::encode_all(&uncompressed_data[..], 3)
171            .map_err(|e| StorageError::NotImplemented(format!("Compression failed: {}", e)))?;
172
173        // Write compressed data to file
174        std::fs::write(path.as_ref(), compressed_data)
175            .map_err(|e| StorageError::NotImplemented(format!("Failed to write file: {}", e)))?;
176
177        Ok(())
178    }
179
180    /// Load database from compressed binary format
181    ///
182    /// Reads a zstd-compressed `.vbsqlz` file and reconstructs the database.
183    ///
184    /// Note: This method requires the `compression` feature to be enabled.
185    ///
186    /// # Example
187    /// ```no_run
188    /// # use vibesql_storage::Database;
189    /// let db = Database::load_compressed("database.vbsqlz").unwrap();
190    /// ```
191    #[cfg(feature = "compression")]
192    pub fn load_compressed<P: AsRef<Path>>(path: P) -> Result<Self, StorageError> {
193        // Read compressed file
194        let compressed_data = std::fs::read(path.as_ref()).map_err(|e| {
195            StorageError::NotImplemented(format!("Failed to read file {:?}: {}", path.as_ref(), e))
196        })?;
197
198        // Decompress using zstd
199        let uncompressed_data = zstd::decode_all(&compressed_data[..])
200            .map_err(|e| StorageError::NotImplemented(format!("Decompression failed: {}", e)))?;
201
202        // Parse the uncompressed binary data
203        let mut reader = BufReader::new(&uncompressed_data[..]);
204
205        // Read and validate header, get version for backward compatibility
206        let version = read_header(&mut reader)?;
207
208        // Read catalog section with version awareness
209        let mut db = read_catalog_v(&mut reader, version)?;
210
211        // Read data section
212        read_data(&mut reader, &mut db)?;
213
214        Ok(db)
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::{
221        format::{MAGIC, VERSION},
222        io::*,
223        *,
224    };
225
226    #[test]
227    fn test_header_roundtrip() {
228        let mut buf = Vec::new();
229        write_header(&mut buf).unwrap();
230
231        assert_eq!(buf.len(), 16);
232        assert_eq!(&buf[0..5], MAGIC);
233        assert_eq!(buf[5], VERSION);
234
235        let mut reader = &buf[..];
236        let version = read_header(&mut reader).unwrap();
237        assert_eq!(version, VERSION);
238    }
239
240    #[test]
241    fn test_primitives() {
242        let mut buf = Vec::new();
243
244        write_u32(&mut buf, 12345).unwrap();
245        write_i64(&mut buf, -9876543210).unwrap();
246        write_f64(&mut buf, 3.14159).unwrap();
247        write_bool(&mut buf, true).unwrap();
248        write_string(&mut buf, "Hello, VBSQL!").unwrap();
249
250        let mut reader = &buf[..];
251        assert_eq!(read_u32(&mut reader).unwrap(), 12345);
252        assert_eq!(read_i64(&mut reader).unwrap(), -9876543210);
253        assert!((read_f64(&mut reader).unwrap() - 3.14159).abs() < 1e-10);
254        assert!(read_bool(&mut reader).unwrap());
255        assert_eq!(read_string(&mut reader).unwrap(), "Hello, VBSQL!");
256    }
257}