sqlitegraph/backend/native/v2/migration/
detect.rs1use crate::backend::native::{
7 constants::MAGIC_BYTES,
8 types::{NativeBackendError, NativeResult},
9};
10use std::fs::File;
11use std::io::Read;
12use std::path::Path;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum FormatVersion {
20 V1,
22 V2,
25 V3,
27 Unknown(u32),
29}
30
31impl FormatVersion {
32 pub fn is_supported(&self) -> bool {
36 matches!(self, FormatVersion::V2 | FormatVersion::V3)
37 }
38
39 pub fn needs_migration_to_current(&self) -> bool {
43 matches!(self, FormatVersion::V2)
44 }
45
46 pub fn as_u32(&self) -> u32 {
48 match self {
49 FormatVersion::V1 => 1,
50 FormatVersion::V2 => 2,
51 FormatVersion::V3 => 3,
52 FormatVersion::Unknown(v) => *v,
53 }
54 }
55}
56
57pub fn detect_format_version(path: &Path) -> NativeResult<FormatVersion> {
74 let mut file = File::open(path).map_err(|e| NativeBackendError::Io(e))?;
76
77 let mut header = [0u8; 80];
79 file.read_exact(&mut header)
80 .map_err(|e| NativeBackendError::Io(e))?;
81
82 let magic = &header[0..8];
84 if magic != MAGIC_BYTES {
85 return Err(NativeBackendError::InvalidMagic {
86 expected: u64::from_be_bytes(MAGIC_BYTES),
87 found: u64::from_be_bytes(magic.try_into().unwrap_or([0u8; 8])),
88 });
89 }
90
91 let version_bytes = [header[8], header[9], header[10], header[11]];
93 let version = u32::from_be_bytes(version_bytes);
94
95 Ok(match version {
96 1 => FormatVersion::V1,
97 2 => FormatVersion::V2,
98 3 => FormatVersion::V3,
99 v => FormatVersion::Unknown(v),
100 })
101}
102
103pub fn needs_migration(path: &Path) -> NativeResult<bool> {
122 let version = detect_format_version(path)?;
123
124 match version {
125 FormatVersion::V1 => Err(NativeBackendError::UnsupportedVersion {
126 version: 1,
127 supported_version: 3,
128 }),
129 FormatVersion::V2 => Ok(true), FormatVersion::V3 => Ok(false), FormatVersion::Unknown(v) => Err(NativeBackendError::UnsupportedVersion {
132 version: v,
133 supported_version: 3,
134 }),
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::backend::native::{
142 constants::DEFAULT_FEATURE_FLAGS,
143 v2::{V2_FORMAT_VERSION, V2_MAGIC},
144 };
145 use std::io::Write;
146 use tempfile::NamedTempFile;
147
148 fn create_test_file(version: u32) -> NamedTempFile {
150 let mut file = NamedTempFile::new().unwrap();
151
152 let mut header = [0u8; 80];
154
155 header[0..8].copy_from_slice(&V2_MAGIC);
157
158 header[8..12].copy_from_slice(&version.to_be_bytes());
160
161 header[12..16].copy_from_slice(&DEFAULT_FEATURE_FLAGS.to_be_bytes());
163
164 file.as_file_mut().write_all(&header).unwrap();
166 file.as_file_mut().flush().unwrap();
167 file.as_file_mut().sync_all().unwrap();
168
169 file
170 }
171
172 #[test]
173 fn test_detect_format_version_v2() {
174 let file = create_test_file(2);
175 let version = detect_format_version(file.path()).unwrap();
176 assert_eq!(version, FormatVersion::V2);
177 assert!(version.is_supported());
178 assert!(version.needs_migration_to_current());
179 }
180
181 #[test]
182 fn test_detect_format_version_v3() {
183 let file = create_test_file(3);
184 let version = detect_format_version(file.path()).unwrap();
185 assert_eq!(version, FormatVersion::V3);
186 assert!(version.is_supported());
187 assert!(!version.needs_migration_to_current());
188 }
189
190 #[test]
191 fn test_detect_format_version_v1_unsupported() {
192 let file = create_test_file(1);
193 let version = detect_format_version(file.path()).unwrap();
194 assert_eq!(version, FormatVersion::V1);
195 assert!(!version.is_supported());
196 }
198
199 #[test]
200 fn test_detect_format_version_unknown() {
201 let file = create_test_file(99);
202 let version = detect_format_version(file.path()).unwrap();
203 assert_eq!(version, FormatVersion::Unknown(99));
204 assert!(!version.is_supported());
205 }
206
207 #[test]
208 fn test_needs_migration_v2_returns_true() {
209 let file = create_test_file(2);
210 let needs = needs_migration(file.path()).unwrap();
211 assert!(needs);
212 }
213
214 #[test]
215 fn test_needs_migration_v3_returns_false() {
216 let file = create_test_file(3);
217 let needs = needs_migration(file.path()).unwrap();
218 assert!(!needs);
219 }
220
221 #[test]
222 fn test_needs_migration_v1_returns_error() {
223 let file = create_test_file(1);
224 let result = needs_migration(file.path());
225 assert!(result.is_err());
226 match result.unwrap_err() {
227 NativeBackendError::UnsupportedVersion { version, .. } => {
228 assert_eq!(version, 1);
229 }
230 _ => panic!("Expected UnsupportedVersion error"),
231 }
232 }
233
234 #[test]
235 fn test_detect_format_version_invalid_magic() {
236 let mut file = NamedTempFile::new().unwrap();
237
238 let mut header = [0u8; 80];
240 header[0..7].copy_from_slice(b"INVALID");
241 file.as_file_mut().write_all(&header).unwrap();
242
243 let result = detect_format_version(file.path());
244 assert!(result.is_err());
245 }
246
247 #[test]
248 fn test_detect_format_version_missing_file() {
249 let result = detect_format_version(Path::new("/nonexistent/file.db"));
250 assert!(result.is_err());
251 }
252
253 #[test]
254 fn test_format_version_as_u32() {
255 assert_eq!(FormatVersion::V1.as_u32(), 1);
256 assert_eq!(FormatVersion::V2.as_u32(), 2);
257 assert_eq!(FormatVersion::V3.as_u32(), 3);
258 assert_eq!(FormatVersion::Unknown(99).as_u32(), 99);
259 }
260}