Skip to main content

sentinel_dbms/
metadata.rs

1//! General metadata structures for collections and stores.
2//!
3//! This module provides metadata structures that are DBMS-wide.
4//! Metadata includes general collection and store information, statistics, and configuration with
5//! proper versioning.
6//!
7//! ## Storage Limits
8//!
9//! Collection metadata files are limited to 1MB total size to prevent unbounded growth.
10//! Store metadata files are limited to 10MB total size.
11//! These limits ensure metadata operations remain performant and prevent abuse.
12
13use serde::{Deserialize, Serialize};
14use sentinel_wal::{CollectionWalConfig, StoreWalConfig};
15
16use crate::META_SENTINEL_VERSION;
17
18/// Version of the metadata format.
19///
20/// This is a numeric version that supports fast-forward migration.
21/// Higher versions can read and migrate older metadata formats.
22pub type MetadataVersion = u32;
23
24/// Collection metadata stored on disk.
25///
26/// This struct contains all persistent metadata for a collection,
27/// including statistics, operational state, and WAL configuration.
28///
29/// Storage limit: 1MB total serialized size
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CollectionMetadata {
32    /// Metadata format version
33    pub version:          MetadataVersion,
34    /// Collection name
35    pub name:             String,
36    /// Creation timestamp (Unix timestamp)
37    pub created_at:       u64,
38    /// Last modification timestamp
39    pub updated_at:       u64,
40    /// Number of documents in the collection
41    pub document_count:   u64,
42    /// Total size of all documents (bytes)
43    pub total_size_bytes: u64,
44    /// WAL configuration for this collection
45    pub wal_config:       Option<CollectionWalConfig>,
46}
47
48impl CollectionMetadata {
49    /// Create new metadata for a collection
50    pub fn new(name: String) -> Self {
51        let now = std::time::SystemTime::now()
52            .duration_since(std::time::UNIX_EPOCH)
53            .unwrap()
54            .as_secs();
55
56        Self {
57            version: META_SENTINEL_VERSION,
58            name,
59            created_at: now,
60            updated_at: now,
61            document_count: 0,
62            total_size_bytes: 0,
63            wal_config: None,
64        }
65    }
66
67    /// Upgrade metadata to the current version if needed
68    ///
69    /// This method handles forward migration of metadata from older versions
70    /// to the current version. It modifies the metadata in-place.
71    pub fn upgrade_to_current(&mut self) -> Result<(), String> {
72        let current_version = META_SENTINEL_VERSION;
73
74        while self.version < current_version {
75            match self.version {
76                1 => {
77                    // Version 1 -> 2: Add any new fields with defaults
78                    // Currently no changes needed for version 2
79
80                    // this is currently a no-op, but we set the version to 2
81                    // when we add new fields in future versions
82                    self.version = current_version;
83                },
84
85                // Add future version migrations here as needed
86                // 2 => { /* migration logic */ self.version = 3; }
87                _ => {
88                    return Err(format!(
89                        "Unsupported metadata version: {} (current: {})",
90                        self.version, current_version
91                    ));
92                },
93            }
94        }
95
96        Ok(())
97    }
98
99    /// Check if metadata needs upgrade to current version
100    pub const fn needs_upgrade(&self) -> bool { self.version < META_SENTINEL_VERSION }
101
102    pub fn touch(&mut self) {
103        self.updated_at = std::time::SystemTime::now()
104            .duration_since(std::time::UNIX_EPOCH)
105            .unwrap()
106            .as_secs();
107    }
108
109    /// Increment document count and size
110    pub fn add_document(&mut self, size_bytes: u64) {
111        self.document_count = self
112            .document_count
113            .checked_add(1)
114            .unwrap_or(self.document_count);
115        self.total_size_bytes = self
116            .total_size_bytes
117            .checked_add(size_bytes)
118            .unwrap_or(self.total_size_bytes);
119        self.touch();
120    }
121
122    /// Decrement document count and size
123    pub fn remove_document(&mut self, size_bytes: u64) {
124        self.document_count = self.document_count.saturating_sub(1);
125        self.total_size_bytes = self.total_size_bytes.saturating_sub(size_bytes);
126        self.touch();
127    }
128
129    /// Update document size (for modifications)
130    pub fn update_document_size(&mut self, old_size: u64, new_size: u64) {
131        self.total_size_bytes = self
132            .total_size_bytes
133            .saturating_sub(old_size)
134            .checked_add(new_size)
135            .unwrap_or(self.total_size_bytes);
136        self.touch();
137    }
138}
139
140/// Store metadata stored on disk.
141///
142/// This struct contains all persistent metadata for the store,
143/// including global statistics, operational state, and WAL configuration.
144///
145/// Storage limit: 10MB total serialized size
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct StoreMetadata {
148    /// Metadata format version
149    pub version:          MetadataVersion,
150    /// Store creation timestamp
151    pub created_at:       u64,
152    /// Last modification timestamp
153    pub updated_at:       u64,
154    /// Total number of collections
155    pub collection_count: u64,
156    /// Total number of documents across all collections
157    pub total_documents:  u64,
158    /// Total size of all data (bytes)
159    pub total_size_bytes: u64,
160    /// WAL configuration for the store
161    pub wal_config:       StoreWalConfig,
162}
163
164#[allow(
165    clippy::arithmetic_side_effects,
166    reason = "counter increments in metadata"
167)]
168impl StoreMetadata {
169    /// Create new metadata for a store
170    pub fn new() -> Self {
171        let now = std::time::SystemTime::now()
172            .duration_since(std::time::UNIX_EPOCH)
173            .unwrap()
174            .as_secs();
175
176        Self {
177            version:          META_SENTINEL_VERSION,
178            created_at:       now,
179            updated_at:       now,
180            collection_count: 0,
181            total_documents:  0,
182            total_size_bytes: 0,
183            wal_config:       StoreWalConfig::default(),
184        }
185    }
186}
187
188impl Default for StoreMetadata {
189    fn default() -> Self { Self::new() }
190}
191
192#[allow(
193    clippy::arithmetic_side_effects,
194    reason = "counter increments in metadata"
195)]
196#[allow(
197    clippy::multiple_inherent_impl,
198    reason = "multiple impl blocks for StoreMetadata are intentional for organization"
199)]
200impl StoreMetadata {
201    /// Upgrade metadata to the current version if needed
202    ///
203    /// This method handles forward migration of metadata from older versions
204    /// to the current version. It modifies the metadata in-place.
205    pub fn upgrade_to_current(&mut self) -> Result<(), String> {
206        let current_version = META_SENTINEL_VERSION;
207
208        while self.version < current_version {
209            match self.version {
210                1 => {
211                    // Version 1 -> 2: Add any new fields with defaults
212                    // Currently no changes needed for version 2
213
214                    // Set version to current to avoid infinite loop
215                    self.version = current_version;
216                },
217                // Add future version migrations here as needed
218                // 2 => { /* migration logic */ self.version = 3; }
219                _ => {
220                    return Err(format!(
221                        "Unsupported metadata version: {} (current: {})",
222                        self.version, current_version
223                    ));
224                },
225            }
226        }
227
228        Ok(())
229    }
230
231    /// Check if metadata needs upgrading
232    pub const fn needs_upgrade(&self) -> bool { self.version < META_SENTINEL_VERSION }
233
234    /// Update the modification timestamp
235    pub fn touch(&mut self) {
236        self.updated_at = std::time::SystemTime::now()
237            .duration_since(std::time::UNIX_EPOCH)
238            .unwrap()
239            .as_secs();
240    }
241
242    /// Add a collection
243    pub fn add_collection(&mut self) {
244        self.collection_count += 1;
245        self.touch();
246    }
247
248    /// Remove a collection
249    pub fn remove_collection(&mut self) {
250        self.collection_count = self.collection_count.saturating_sub(1);
251        self.touch();
252    }
253
254    /// Update document statistics
255    pub fn update_documents(&mut self, document_delta: i64, size_delta: i64) {
256        self.total_documents = (self.total_documents as i128 + document_delta as i128).max(0) as u64;
257        self.total_size_bytes = (self.total_size_bytes as i128 + size_delta as i128).max(0) as u64;
258        self.touch();
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_collection_metadata_new() {
268        let metadata = CollectionMetadata::new("test_collection".to_string());
269        assert_eq!(metadata.version, META_SENTINEL_VERSION);
270        assert_eq!(metadata.name, "test_collection");
271        assert_eq!(metadata.document_count, 0);
272        assert_eq!(metadata.total_size_bytes, 0);
273        assert!(
274            metadata.created_at <=
275                std::time::SystemTime::now()
276                    .duration_since(std::time::UNIX_EPOCH)
277                    .unwrap()
278                    .as_secs()
279        );
280        assert_eq!(metadata.created_at, metadata.updated_at);
281    }
282
283    #[test]
284    fn test_collection_metadata_add_remove_document() {
285        let mut metadata = CollectionMetadata::new("test".to_string());
286
287        // Add document
288        metadata.add_document(100);
289        assert_eq!(metadata.document_count, 1);
290        assert_eq!(metadata.total_size_bytes, 100);
291        assert!(metadata.updated_at >= metadata.created_at);
292
293        let updated_at = metadata.updated_at;
294
295        // Add another document
296        metadata.add_document(200);
297        assert_eq!(metadata.document_count, 2);
298        assert_eq!(metadata.total_size_bytes, 300);
299        assert!(metadata.updated_at >= updated_at);
300
301        // Remove document
302        metadata.remove_document(100);
303        assert_eq!(metadata.document_count, 1);
304        assert_eq!(metadata.total_size_bytes, 200);
305
306        // Remove last document
307        metadata.remove_document(200);
308        assert_eq!(metadata.document_count, 0);
309        assert_eq!(metadata.total_size_bytes, 0);
310    }
311
312    #[test]
313    fn test_collection_metadata_update_document_size() {
314        let mut metadata = CollectionMetadata::new("test".to_string());
315        metadata.add_document(100);
316
317        metadata.update_document_size(100, 150);
318        assert_eq!(metadata.document_count, 1);
319        assert_eq!(metadata.total_size_bytes, 150);
320    }
321
322    #[test]
323    fn test_collection_metadata_upgrade() {
324        let mut metadata = CollectionMetadata::new("test".to_string());
325        metadata.version = 1;
326
327        assert!(metadata.needs_upgrade());
328        assert!(metadata.upgrade_to_current().is_ok());
329    }
330
331    #[test]
332    fn test_store_metadata_new() {
333        let metadata = StoreMetadata::new();
334        assert_eq!(metadata.version, META_SENTINEL_VERSION);
335        assert_eq!(metadata.collection_count, 0);
336        assert_eq!(metadata.total_documents, 0);
337        assert_eq!(metadata.total_size_bytes, 0);
338        assert!(
339            metadata.created_at <=
340                std::time::SystemTime::now()
341                    .duration_since(std::time::UNIX_EPOCH)
342                    .unwrap()
343                    .as_secs()
344        );
345    }
346
347    #[test]
348    fn test_store_metadata_operations() {
349        let mut metadata = StoreMetadata::new();
350
351        // Add collection
352        metadata.add_collection();
353        assert_eq!(metadata.collection_count, 1);
354
355        // Update documents
356        metadata.update_documents(5, 1000);
357        assert_eq!(metadata.total_documents, 5);
358        assert_eq!(metadata.total_size_bytes, 1000);
359
360        // Update again
361        metadata.update_documents(3, 500);
362        assert_eq!(metadata.total_documents, 8);
363        assert_eq!(metadata.total_size_bytes, 1500);
364
365        // Negative update (remove documents)
366        metadata.update_documents(-2, -200);
367        assert_eq!(metadata.total_documents, 6);
368        assert_eq!(metadata.total_size_bytes, 1300);
369
370        // Remove collection
371        metadata.remove_collection();
372        assert_eq!(metadata.collection_count, 0);
373    }
374
375    #[test]
376    fn test_store_metadata_upgrade() {
377        let mut metadata = StoreMetadata::new();
378        metadata.version = 1;
379
380        assert!(metadata.needs_upgrade());
381        assert!(metadata.upgrade_to_current().is_ok());
382    }
383
384    #[test]
385    fn test_metadata_serialization() {
386        let collection_meta = CollectionMetadata::new("test".to_string());
387        let serialized = serde_json::to_string(&collection_meta).unwrap();
388        let deserialized: CollectionMetadata = serde_json::from_str(&serialized).unwrap();
389        assert_eq!(collection_meta.name, deserialized.name);
390        assert_eq!(collection_meta.version, deserialized.version);
391
392        let store_meta = StoreMetadata::new();
393        let serialized = serde_json::to_string(&store_meta).unwrap();
394        let deserialized: StoreMetadata = serde_json::from_str(&serialized).unwrap();
395        assert_eq!(store_meta.version, deserialized.version);
396    }
397}