Skip to main content

hematite/catalog/
header.rs

1//! Database header management for Hematite database.
2//!
3//! M0 storage contract notes:
4//! - The database header is always stored at page 0.
5//! - Header versioning is strict: older on-disk versions are rejected.
6//! - Header checksum covers all semantic header fields to detect corruption.
7
8use super::ids::TableId;
9use crate::error::Result;
10
11/// Database header structure stored on page 0.
12#[derive(Debug, Clone)]
13pub struct DatabaseHeader {
14    /// Magic bytes to identify Hematite database files
15    pub magic: [u8; 4],
16    /// Database format version
17    pub version: u32,
18    /// Root page of the schema B-tree
19    pub schema_root_page: u32,
20    /// Next available table ID
21    pub next_table_id: u32,
22    /// Header checksum for integrity verification
23    pub checksum: u32,
24}
25
26impl DatabaseHeader {
27    /// Magic bytes for Hematite database files
28    pub const MAGIC: [u8; 4] = *b"HMTD";
29    /// Current database format version.
30    ///
31    /// Version 2 is the first version after the M0 storage reset that intentionally
32    /// breaks compatibility with previous files.
33    pub const CURRENT_VERSION: u32 = 2;
34    /// Fixed page ID for database header (consistent with existing implementation)
35    pub const HEADER_PAGE_ID: u32 = 0;
36
37    /// Create a new database header with default values
38    pub fn new(schema_root_page: u32) -> Self {
39        let mut header = Self {
40            magic: Self::MAGIC,
41            version: Self::CURRENT_VERSION,
42            schema_root_page,
43            next_table_id: 1,
44            checksum: 0,
45        };
46        header.checksum = header.calculate_checksum();
47        header
48    }
49
50    /// Calculate checksum for header integrity
51    pub fn calculate_checksum(&self) -> u32 {
52        use std::collections::hash_map::DefaultHasher;
53        use std::hash::{Hash, Hasher};
54
55        let mut hasher = DefaultHasher::new();
56        self.magic.hash(&mut hasher);
57        self.version.hash(&mut hasher);
58        self.schema_root_page.hash(&mut hasher);
59        self.next_table_id.hash(&mut hasher);
60        hasher.finish() as u32
61    }
62
63    /// Verify header integrity
64    pub fn verify_checksum(&self) -> bool {
65        self.checksum == self.calculate_checksum()
66    }
67
68    /// Serialize header to page data
69    pub fn serialize(&self, bytes: &mut [u8]) -> Result<()> {
70        let offset = 0;
71
72        // Write magic bytes
73        bytes[offset..offset + 4].copy_from_slice(&self.magic);
74
75        // Write version
76        bytes[offset + 4..offset + 8].copy_from_slice(&self.version.to_le_bytes());
77
78        // Write schema root page ID
79        bytes[offset + 8..offset + 12].copy_from_slice(&self.schema_root_page.to_le_bytes());
80
81        // Write next table ID
82        bytes[offset + 12..offset + 16].copy_from_slice(&self.next_table_id.to_le_bytes());
83
84        // Write checksum
85        bytes[offset + 16..offset + 20].copy_from_slice(&self.checksum.to_le_bytes());
86
87        // Zero out the rest of the header page
88        for byte in bytes.iter_mut().skip(20) {
89            *byte = 0;
90        }
91
92        Ok(())
93    }
94
95    /// Deserialize header from page data
96    pub fn deserialize(bytes: &[u8]) -> Result<Self> {
97        let offset = 0;
98
99        // Read magic bytes
100        let mut magic = [0u8; 4];
101        magic.copy_from_slice(&bytes[offset..offset + 4]);
102
103        // Verify magic bytes
104        if magic != Self::MAGIC {
105            return Err(crate::error::HematiteError::StorageError(
106                "Invalid database file: wrong magic bytes".to_string(),
107            ));
108        }
109
110        // Read version
111        let version = u32::from_le_bytes([
112            bytes[offset + 4],
113            bytes[offset + 5],
114            bytes[offset + 6],
115            bytes[offset + 7],
116        ]);
117        if version != Self::CURRENT_VERSION {
118            return Err(crate::error::HematiteError::StorageError(format!(
119                "Unsupported database header version: expected {}, got {}",
120                Self::CURRENT_VERSION,
121                version
122            )));
123        }
124
125        // Read schema root page ID
126        let schema_root_page = u32::from_le_bytes([
127            bytes[offset + 8],
128            bytes[offset + 9],
129            bytes[offset + 10],
130            bytes[offset + 11],
131        ]);
132
133        // Read next table ID
134        let next_table_id = u32::from_le_bytes([
135            bytes[offset + 12],
136            bytes[offset + 13],
137            bytes[offset + 14],
138            bytes[offset + 15],
139        ]);
140
141        // Read checksum
142        let checksum = u32::from_le_bytes([
143            bytes[offset + 16],
144            bytes[offset + 17],
145            bytes[offset + 18],
146            bytes[offset + 19],
147        ]);
148
149        let header = Self {
150            magic,
151            version,
152            schema_root_page,
153            next_table_id,
154            checksum,
155        };
156
157        // Verify checksum
158        if !header.verify_checksum() {
159            return Err(crate::error::HematiteError::StorageError(
160                "Database header checksum verification failed".to_string(),
161            ));
162        }
163
164        Ok(header)
165    }
166
167    /// Update next table ID and recalculate checksum
168    pub fn increment_table_id(&mut self) -> TableId {
169        let table_id = TableId::new(self.next_table_id);
170        self.next_table_id += 1;
171        self.checksum = self.calculate_checksum();
172        table_id
173    }
174}