Skip to main content

feltdb/
storage.rs

1use crate::{FlowError, Result, StoredRow};
2use std::collections::HashMap;
3use std::fs::{self, OpenOptions};
4use std::io::{BufRead, BufReader, Write};
5use std::path::{Path, PathBuf};
6
7/// Trait defining the storage abstraction for FeltDB.
8///
9/// This allows FeltDB to work with different storage backends:
10/// - Memory (for testing)
11/// - Native files (for desktop/server)
12/// - Browser storage (OPFS/IndexedDB via WASM)
13pub trait Storage: Send + Sync {
14    /// Append a new operation to the log
15    fn append(&mut self, row: &StoredRow) -> Result<()>;
16
17    /// Read all operations from storage
18    fn read_all(&self) -> Result<Vec<StoredRow>>;
19
20    /// Create a checkpoint of the current state
21    fn checkpoint(&mut self, state: &CheckpointData) -> Result<()>;
22
23    /// Read the latest checkpoint if it exists
24    fn read_checkpoint(&self) -> Result<Option<CheckpointData>>;
25
26    /// Compact the storage by merging checkpoint and delta operations
27    fn compact(&mut self) -> Result<()>;
28
29    /// Get the storage backend type name (for debugging/runtime info)
30    fn storage_type(&self) -> &'static str;
31}
32
33/// Data structure for checkpoints
34#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub struct CheckpointData {
36    /// All rows at checkpoint time
37    pub rows: HashMap<String, HashMap<String, StoredRow>>,
38    /// Key counters at checkpoint time
39    pub key_counters: HashMap<String, u64>,
40    /// Operations since last checkpoint
41    pub delta_operations: Vec<StoredRow>,
42}
43
44/// In-memory storage backend for testing
45pub struct MemoryStorage {
46    operations: Vec<StoredRow>,
47    checkpoint: Option<CheckpointData>,
48}
49
50impl MemoryStorage {
51    pub fn new() -> Self {
52        Self {
53            operations: Vec::new(),
54            checkpoint: None,
55        }
56    }
57}
58
59impl Default for MemoryStorage {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl Storage for MemoryStorage {
66    fn append(&mut self, row: &StoredRow) -> Result<()> {
67        self.operations.push(row.clone());
68        Ok(())
69    }
70
71    fn read_all(&self) -> Result<Vec<StoredRow>> {
72        Ok(self.operations.clone())
73    }
74
75    fn checkpoint(&mut self, state: &CheckpointData) -> Result<()> {
76        self.checkpoint = Some(state.clone());
77        self.operations.clear();
78        Ok(())
79    }
80
81    fn read_checkpoint(&self) -> Result<Option<CheckpointData>> {
82        Ok(self.checkpoint.clone())
83    }
84
85    fn compact(&mut self) -> Result<()> {
86        // For in-memory, compaction is just checkpoint + clear deltas
87        if let Some(checkpoint) = &self.checkpoint {
88            self.operations = checkpoint.delta_operations.clone();
89        }
90        Ok(())
91    }
92
93    fn storage_type(&self) -> &'static str {
94        "memory"
95    }
96}
97
98/// File-based storage backend for native file operations
99pub struct FileStorage {
100    path: PathBuf,
101    checkpoint_path: PathBuf,
102}
103
104impl FileStorage {
105    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
106        let path = path.as_ref().to_path_buf();
107        if let Some(parent) = path.parent() {
108            fs::create_dir_all(parent)?;
109        }
110        if !path.exists() {
111            OpenOptions::new().create(true).append(true).open(&path)?;
112        }
113
114        let checkpoint_path = path.with_extension("checkpoint");
115        Ok(Self {
116            path,
117            checkpoint_path,
118        })
119    }
120}
121
122impl Storage for FileStorage {
123    fn append(&mut self, row: &StoredRow) -> Result<()> {
124        let mut file = OpenOptions::new()
125            .create(true)
126            .append(true)
127            .open(&self.path)?;
128        let mut line = serde_json::to_string(row)?;
129        line.push('\n');
130        file.write_all(line.as_bytes())?;
131        file.flush()?;
132        Ok(())
133    }
134
135    fn read_all(&self) -> Result<Vec<StoredRow>> {
136        let mut operations = Vec::new();
137
138        // First, try to read checkpoint if it exists
139        if let Ok(checkpoint_data) = self.read_checkpoint() {
140            if checkpoint_data.is_some() {
141                // Get all rows from checkpoint
142                if let Some(cp) = checkpoint_data {
143                    for bucket in cp.rows.values() {
144                        for row in bucket.values() {
145                            operations.push(row.clone());
146                        }
147                    }
148                    // Add delta operations
149                    operations.extend(cp.delta_operations.clone());
150                    return Ok(operations);
151                }
152            }
153        }
154
155        // If no checkpoint, read from main log file
156        if self.path.exists() {
157            let file = OpenOptions::new().read(true).open(&self.path)?;
158            let reader = BufReader::new(file);
159
160            for (index, line) in reader.lines().enumerate() {
161                let line = line?;
162                if line.trim().is_empty() {
163                    continue;
164                }
165                let row: StoredRow = serde_json::from_str(&line).map_err(|_| {
166                    FlowError::CorruptLogLine(Box::new(crate::LogCorruption {
167                        line_number: index + 1,
168                        byte_offset: 0,
169                        reason: "not a durable record this build understands".to_string(),
170                        excerpt: line.chars().take(120).collect(),
171                    }))
172                })?;
173                operations.push(row);
174            }
175        }
176
177        Ok(operations)
178    }
179
180    fn checkpoint(&mut self, state: &CheckpointData) -> Result<()> {
181        let checkpoint_json = serde_json::to_string(state)?;
182        let mut file = OpenOptions::new()
183            .create(true)
184            .write(true)
185            .truncate(true)
186            .open(&self.checkpoint_path)?;
187        file.write_all(checkpoint_json.as_bytes())?;
188        file.flush()?;
189        Ok(())
190    }
191
192    fn read_checkpoint(&self) -> Result<Option<CheckpointData>> {
193        if !self.checkpoint_path.exists() {
194            return Ok(None);
195        }
196
197        let content = fs::read_to_string(&self.checkpoint_path)?;
198        match serde_json::from_str(&content) {
199            Ok(data) => Ok(Some(data)),
200            Err(_) => Ok(None),
201        }
202    }
203
204    fn compact(&mut self) -> Result<()> {
205        // Compaction for file storage:
206        // 1. Create checkpoint
207        // 2. Truncate main log to only contain delta operations
208        // 3. On next read, we'll load from checkpoint + deltas
209
210        // This is handled automatically by read_all() which checks for checkpoint first
211        Ok(())
212    }
213
214    fn storage_type(&self) -> &'static str {
215        "file"
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn memory_storage_append_and_read() {
225        let mut storage = MemoryStorage::new();
226        let row = StoredRow {
227            capability: "test".to_string(),
228            key: "test:1".to_string(),
229            rust_type: "String".to_string(),
230            value: serde_json::json!("value"),
231            unix_ms: 1000,
232            content_hash: None,
233            flow_ref: None,
234            deleted: false,
235            operation: None,
236        };
237
238        storage.append(&row).expect("append should succeed");
239        let operations = storage.read_all().expect("read_all should succeed");
240        assert_eq!(operations.len(), 1);
241        assert_eq!(operations[0].key, "test:1");
242    }
243
244    #[test]
245    fn memory_storage_checkpoint() {
246        let mut storage = MemoryStorage::new();
247        let row = StoredRow {
248            capability: "test".to_string(),
249            key: "test:1".to_string(),
250            rust_type: "String".to_string(),
251            value: serde_json::json!("value"),
252            unix_ms: 1000,
253            content_hash: None,
254            flow_ref: None,
255            deleted: false,
256            operation: None,
257        };
258
259        storage.append(&row).expect("append");
260
261        let checkpoint = CheckpointData {
262            rows: Default::default(),
263            key_counters: Default::default(),
264            delta_operations: vec![row],
265        };
266
267        storage
268            .checkpoint(&checkpoint)
269            .expect("checkpoint should succeed");
270        let operations = storage.read_all().expect("read_all should succeed");
271        assert_eq!(operations.len(), 0); // Cleared after checkpoint
272    }
273}