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}