Skip to main content

sochdb_storage/
upgrade_contract.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! # Upgrade Compatibility Contract
19//!
20//! Manages versioned file formats and safe upgrade paths:
21//! - Versioned magic numbers for all persisted formats
22//! - Forward/backward compatibility policies
23//! - Migration orchestration (N → N+1 only)
24//! - Downgrade behavior specification
25//!
26//! ## Design Principles
27//!
28//! 1. **Explicit Versioning**: All formats have magic + version in header
29//! 2. **Safe Upgrades**: Migrations are atomic with rollback capability
30//! 3. **No Silent Corruption**: Incompatible formats fail loudly
31//! 4. **Document Downgrades**: Usually "not supported" but explicit
32
33use std::collections::HashMap;
34use std::fmt;
35
36/// Magic number for SochDB files (8 bytes)
37pub const SOCHDB_MAGIC: [u8; 8] = *b"SOCHDB\x00\x01";
38
39/// File format types
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum FormatType {
42    /// Write-Ahead Log segment
43    WalSegment,
44    /// Data page file
45    DataPage,
46    /// Manifest/catalog
47    Manifest,
48    /// HNSW vector index
49    HnswIndex,
50    /// SSTable (sorted string table)
51    Sstable,
52    /// Checkpoint file
53    Checkpoint,
54    /// Backup archive
55    BackupArchive,
56}
57
58impl FormatType {
59    /// Get unique identifier for format type
60    pub fn type_id(&self) -> u8 {
61        match self {
62            FormatType::WalSegment => 0x01,
63            FormatType::DataPage => 0x02,
64            FormatType::Manifest => 0x03,
65            FormatType::HnswIndex => 0x04,
66            FormatType::Sstable => 0x05,
67            FormatType::Checkpoint => 0x06,
68            FormatType::BackupArchive => 0x07,
69        }
70    }
71
72    /// Parse from type ID
73    pub fn from_type_id(id: u8) -> Option<Self> {
74        match id {
75            0x01 => Some(FormatType::WalSegment),
76            0x02 => Some(FormatType::DataPage),
77            0x03 => Some(FormatType::Manifest),
78            0x04 => Some(FormatType::HnswIndex),
79            0x05 => Some(FormatType::Sstable),
80            0x06 => Some(FormatType::Checkpoint),
81            0x07 => Some(FormatType::BackupArchive),
82            _ => None,
83        }
84    }
85
86    pub fn name(&self) -> &'static str {
87        match self {
88            FormatType::WalSegment => "WAL Segment",
89            FormatType::DataPage => "Data Page",
90            FormatType::Manifest => "Manifest",
91            FormatType::HnswIndex => "HNSW Index",
92            FormatType::Sstable => "SSTable",
93            FormatType::Checkpoint => "Checkpoint",
94            FormatType::BackupArchive => "Backup Archive",
95        }
96    }
97}
98
99/// Format version with major.minor
100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
101pub struct FormatVersion {
102    pub major: u16,
103    pub minor: u16,
104}
105
106impl FormatVersion {
107    pub const fn new(major: u16, minor: u16) -> Self {
108        Self { major, minor }
109    }
110
111    /// Check if this version is compatible with another
112    /// Same major version is backward compatible
113    pub fn is_compatible_with(&self, other: &FormatVersion) -> bool {
114        self.major == other.major && self.minor >= other.minor
115    }
116
117    /// Check if upgrade from other to self is supported
118    pub fn can_upgrade_from(&self, other: &FormatVersion) -> bool {
119        // Only N → N+1 upgrades supported (within same major)
120        if self.major == other.major {
121            return self.minor >= other.minor;
122        }
123        // Major version upgrade: only N.x → (N+1).0
124        if self.major == other.major + 1 && self.minor == 0 {
125            return true;
126        }
127        false
128    }
129
130    /// Serialize to bytes (4 bytes)
131    pub fn to_bytes(&self) -> [u8; 4] {
132        let mut buf = [0u8; 4];
133        buf[0..2].copy_from_slice(&self.major.to_le_bytes());
134        buf[2..4].copy_from_slice(&self.minor.to_le_bytes());
135        buf
136    }
137
138    /// Parse from bytes
139    pub fn from_bytes(buf: &[u8]) -> Option<Self> {
140        if buf.len() < 4 {
141            return None;
142        }
143        Some(Self {
144            major: u16::from_le_bytes([buf[0], buf[1]]),
145            minor: u16::from_le_bytes([buf[2], buf[3]]),
146        })
147    }
148}
149
150impl fmt::Display for FormatVersion {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(f, "{}.{}", self.major, self.minor)
153    }
154}
155
156/// Current format versions
157pub mod current_versions {
158    use super::*;
159
160    pub const WAL_SEGMENT: FormatVersion = FormatVersion::new(1, 0);
161    pub const DATA_PAGE: FormatVersion = FormatVersion::new(1, 0);
162    pub const MANIFEST: FormatVersion = FormatVersion::new(1, 0);
163    pub const HNSW_INDEX: FormatVersion = FormatVersion::new(1, 0);
164    pub const SSTABLE: FormatVersion = FormatVersion::new(1, 0);
165    pub const CHECKPOINT: FormatVersion = FormatVersion::new(1, 0);
166    pub const BACKUP_ARCHIVE: FormatVersion = FormatVersion::new(1, 0);
167}
168
169/// File header with magic and version
170#[derive(Debug, Clone)]
171pub struct FileHeader {
172    /// Magic bytes (8)
173    pub magic: [u8; 8],
174    /// Format type (1 byte)
175    pub format_type: FormatType,
176    /// Format version (4 bytes)
177    pub version: FormatVersion,
178    /// Feature flags (4 bytes)
179    pub feature_flags: u32,
180    /// Reserved for future use (15 bytes)
181    pub reserved: [u8; 15],
182}
183
184impl FileHeader {
185    /// Header size in bytes
186    pub const SIZE: usize = 32;
187
188    /// Create a new header for a format type
189    pub fn new(format_type: FormatType, version: FormatVersion) -> Self {
190        Self {
191            magic: SOCHDB_MAGIC,
192            format_type,
193            version,
194            feature_flags: 0,
195            reserved: [0; 15],
196        }
197    }
198
199    /// Serialize to bytes
200    pub fn to_bytes(&self) -> [u8; Self::SIZE] {
201        let mut buf = [0u8; Self::SIZE];
202        buf[0..8].copy_from_slice(&self.magic);
203        buf[8] = self.format_type.type_id();
204        buf[9..13].copy_from_slice(&self.version.to_bytes());
205        buf[13..17].copy_from_slice(&self.feature_flags.to_le_bytes());
206        // reserved stays zero
207        buf
208    }
209
210    /// Parse from bytes
211    pub fn from_bytes(buf: &[u8]) -> Result<Self, VersionError> {
212        if buf.len() < Self::SIZE {
213            return Err(VersionError::InvalidHeader("Header too short".to_string()));
214        }
215
216        let mut magic = [0u8; 8];
217        magic.copy_from_slice(&buf[0..8]);
218
219        if magic != SOCHDB_MAGIC {
220            return Err(VersionError::InvalidMagic {
221                expected: SOCHDB_MAGIC,
222                found: magic,
223            });
224        }
225
226        let format_type = FormatType::from_type_id(buf[8])
227            .ok_or_else(|| VersionError::UnknownFormatType(buf[8]))?;
228
229        let version = FormatVersion::from_bytes(&buf[9..13])
230            .ok_or_else(|| VersionError::InvalidHeader("Invalid version bytes".to_string()))?;
231
232        let feature_flags = u32::from_le_bytes([buf[13], buf[14], buf[15], buf[16]]);
233
234        Ok(Self {
235            magic,
236            format_type,
237            version,
238            feature_flags,
239            reserved: [0; 15],
240        })
241    }
242
243    /// Check compatibility with expected type and version
244    pub fn check_compatibility(
245        &self,
246        expected_type: FormatType,
247        current_version: FormatVersion,
248    ) -> Result<CompatibilityResult, VersionError> {
249        if self.format_type != expected_type {
250            return Err(VersionError::TypeMismatch {
251                expected: expected_type,
252                found: self.format_type,
253            });
254        }
255
256        if self.version == current_version {
257            Ok(CompatibilityResult::Exact)
258        } else if current_version.is_compatible_with(&self.version) {
259            Ok(CompatibilityResult::BackwardCompatible {
260                file_version: self.version,
261                current_version,
262            })
263        } else if current_version.can_upgrade_from(&self.version) {
264            Ok(CompatibilityResult::NeedsMigration {
265                from: self.version,
266                to: current_version,
267            })
268        } else {
269            Err(VersionError::Incompatible {
270                file_version: self.version,
271                current_version,
272            })
273        }
274    }
275}
276
277/// Compatibility check result
278#[derive(Debug, Clone)]
279pub enum CompatibilityResult {
280    /// Exact version match
281    Exact,
282    /// File version is older but readable
283    BackwardCompatible {
284        file_version: FormatVersion,
285        current_version: FormatVersion,
286    },
287    /// Migration required before use
288    NeedsMigration {
289        from: FormatVersion,
290        to: FormatVersion,
291    },
292}
293
294/// Version-related errors
295#[derive(Debug, Clone)]
296pub enum VersionError {
297    /// Invalid magic bytes
298    InvalidMagic {
299        expected: [u8; 8],
300        found: [u8; 8],
301    },
302    /// Unknown format type
303    UnknownFormatType(u8),
304    /// Format type mismatch
305    TypeMismatch {
306        expected: FormatType,
307        found: FormatType,
308    },
309    /// Version incompatible
310    Incompatible {
311        file_version: FormatVersion,
312        current_version: FormatVersion,
313    },
314    /// Invalid header
315    InvalidHeader(String),
316    /// Migration failed
317    MigrationFailed {
318        from: FormatVersion,
319        to: FormatVersion,
320        reason: String,
321    },
322    /// Downgrade not supported
323    DowngradeNotSupported {
324        from: FormatVersion,
325        to: FormatVersion,
326    },
327}
328
329impl fmt::Display for VersionError {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        match self {
332            VersionError::InvalidMagic { expected, found } => {
333                write!(
334                    f,
335                    "Invalid magic: expected {:?}, found {:?}",
336                    expected, found
337                )
338            }
339            VersionError::UnknownFormatType(id) => {
340                write!(f, "Unknown format type: 0x{:02x}", id)
341            }
342            VersionError::TypeMismatch { expected, found } => {
343                write!(
344                    f,
345                    "Format type mismatch: expected {}, found {}",
346                    expected.name(),
347                    found.name()
348                )
349            }
350            VersionError::Incompatible {
351                file_version,
352                current_version,
353            } => {
354                write!(
355                    f,
356                    "Incompatible version: file is {}, current is {}",
357                    file_version, current_version
358                )
359            }
360            VersionError::InvalidHeader(msg) => {
361                write!(f, "Invalid header: {}", msg)
362            }
363            VersionError::MigrationFailed { from, to, reason } => {
364                write!(f, "Migration from {} to {} failed: {}", from, to, reason)
365            }
366            VersionError::DowngradeNotSupported { from, to } => {
367                write!(f, "Downgrade from {} to {} is not supported", from, to)
368            }
369        }
370    }
371}
372
373impl std::error::Error for VersionError {}
374
375/// Migration step
376pub trait Migration: Send + Sync {
377    /// Source version
378    fn from_version(&self) -> FormatVersion;
379    /// Target version
380    fn to_version(&self) -> FormatVersion;
381    /// Migrate data (returns new data)
382    fn migrate(&self, data: &[u8]) -> Result<Vec<u8>, VersionError>;
383    /// Check if migration is reversible
384    fn is_reversible(&self) -> bool;
385    /// Reverse migration (if reversible)
386    fn reverse(&self, data: &[u8]) -> Result<Vec<u8>, VersionError>;
387}
388
389/// Migration registry
390pub struct MigrationRegistry {
391    /// Registered migrations by format type
392    migrations: HashMap<FormatType, Vec<Box<dyn Migration>>>,
393}
394
395impl MigrationRegistry {
396    /// Create a new migration registry
397    pub fn new() -> Self {
398        Self {
399            migrations: HashMap::new(),
400        }
401    }
402
403    /// Register a migration
404    pub fn register(&mut self, format_type: FormatType, migration: Box<dyn Migration>) {
405        self.migrations
406            .entry(format_type)
407            .or_insert_with(Vec::new)
408            .push(migration);
409    }
410
411    /// Find migration path from one version to another
412    pub fn find_path(
413        &self,
414        format_type: FormatType,
415        from: FormatVersion,
416        to: FormatVersion,
417    ) -> Option<Vec<&dyn Migration>> {
418        let migrations = self.migrations.get(&format_type)?;
419
420        // Simple linear search for now (could use graph algorithm for complex paths)
421        let mut path = Vec::new();
422        let mut current = from;
423
424        while current < to {
425            let next = migrations
426                .iter()
427                .find(|m| m.from_version() == current && m.to_version() > current)?;
428            path.push(next.as_ref());
429            current = next.to_version();
430        }
431
432        if current == to {
433            Some(path)
434        } else {
435            None
436        }
437    }
438
439    /// Execute migration path
440    pub fn execute_path(
441        &self,
442        path: &[&dyn Migration],
443        data: &[u8],
444    ) -> Result<Vec<u8>, VersionError> {
445        let mut current_data = data.to_vec();
446        for migration in path {
447            current_data = migration.migrate(&current_data)?;
448        }
449        Ok(current_data)
450    }
451}
452
453impl Default for MigrationRegistry {
454    fn default() -> Self {
455        Self::new()
456    }
457}
458
459/// Upgrade policy configuration
460#[derive(Debug, Clone)]
461pub struct UpgradePolicy {
462    /// Allow automatic minor version upgrades
463    pub auto_minor_upgrade: bool,
464    /// Allow automatic major version upgrades
465    pub auto_major_upgrade: bool,
466    /// Create backup before migration
467    pub backup_before_migration: bool,
468    /// Supported upgrade paths
469    pub supported_paths: Vec<(FormatVersion, FormatVersion)>,
470}
471
472impl Default for UpgradePolicy {
473    fn default() -> Self {
474        Self {
475            auto_minor_upgrade: true,
476            auto_major_upgrade: false, // Require explicit action
477            backup_before_migration: true,
478            supported_paths: Vec::new(),
479        }
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[test]
488    fn test_format_version_compatibility() {
489        let v1_0 = FormatVersion::new(1, 0);
490        let v1_1 = FormatVersion::new(1, 1);
491        let v2_0 = FormatVersion::new(2, 0);
492
493        // Same version is compatible
494        assert!(v1_0.is_compatible_with(&v1_0));
495
496        // Newer minor is compatible with older
497        assert!(v1_1.is_compatible_with(&v1_0));
498
499        // Older minor is not compatible with newer
500        assert!(!v1_0.is_compatible_with(&v1_1));
501
502        // Different major is not compatible
503        assert!(!v2_0.is_compatible_with(&v1_0));
504    }
505
506    #[test]
507    fn test_upgrade_paths() {
508        let v1_0 = FormatVersion::new(1, 0);
509        let v1_1 = FormatVersion::new(1, 1);
510        let v2_0 = FormatVersion::new(2, 0);
511
512        // Can upgrade within same major
513        assert!(v1_1.can_upgrade_from(&v1_0));
514
515        // Can upgrade to next major.0
516        assert!(v2_0.can_upgrade_from(&v1_1));
517
518        // Cannot skip major versions
519        let v3_0 = FormatVersion::new(3, 0);
520        assert!(!v3_0.can_upgrade_from(&v1_0));
521    }
522
523    #[test]
524    fn test_file_header_roundtrip() {
525        let header = FileHeader::new(FormatType::WalSegment, FormatVersion::new(1, 2));
526
527        let bytes = header.to_bytes();
528        let parsed = FileHeader::from_bytes(&bytes).unwrap();
529
530        assert_eq!(parsed.format_type, FormatType::WalSegment);
531        assert_eq!(parsed.version, FormatVersion::new(1, 2));
532    }
533
534    #[test]
535    fn test_header_invalid_magic() {
536        let mut bytes = [0u8; FileHeader::SIZE];
537        bytes[0..8].copy_from_slice(b"INVALID!");
538
539        let result = FileHeader::from_bytes(&bytes);
540        assert!(matches!(result, Err(VersionError::InvalidMagic { .. })));
541    }
542
543    #[test]
544    fn test_compatibility_check() {
545        let header = FileHeader::new(FormatType::Manifest, FormatVersion::new(1, 0));
546
547        // Exact match
548        let result = header
549            .check_compatibility(FormatType::Manifest, FormatVersion::new(1, 0))
550            .unwrap();
551        assert!(matches!(result, CompatibilityResult::Exact));
552
553        // Backward compatible
554        let result = header
555            .check_compatibility(FormatType::Manifest, FormatVersion::new(1, 1))
556            .unwrap();
557        assert!(matches!(result, CompatibilityResult::BackwardCompatible { .. }));
558
559        // Needs migration
560        let result = header
561            .check_compatibility(FormatType::Manifest, FormatVersion::new(2, 0))
562            .unwrap();
563        assert!(matches!(result, CompatibilityResult::NeedsMigration { .. }));
564    }
565}