Skip to main content

mangle_db/
file_backend.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! File-based IDB backend: stores cached IDB snapshots as simplerow files.
16//!
17//! Layout in the cache directory:
18//! - `{db_name}.meta.json` — CacheMeta (program hash, edb fingerprint, timestamp)
19//! - `{db_name}.idb.mgr`  — IDB facts in simplerow format
20
21use std::path::PathBuf;
22
23use anyhow::Result;
24
25use crate::backend::{CacheMeta, IdbBackend, IdbSnapshot};
26use crate::simplerow;
27
28/// File-based IDB cache backend.
29pub struct FileIdbBackend {
30    dir: PathBuf,
31}
32
33impl FileIdbBackend {
34    pub fn new(dir: impl Into<PathBuf>) -> Self {
35        Self { dir: dir.into() }
36    }
37
38    fn meta_path(&self, db_name: &str) -> PathBuf {
39        self.dir.join(format!("{db_name}.meta.json"))
40    }
41
42    fn idb_path(&self, db_name: &str) -> PathBuf {
43        self.dir.join(format!("{db_name}.idb.mgr"))
44    }
45}
46
47impl IdbBackend for FileIdbBackend {
48    fn load(&self, db_name: &str) -> Result<Option<(CacheMeta, IdbSnapshot)>> {
49        let meta_path = self.meta_path(db_name);
50        let idb_path = self.idb_path(db_name);
51
52        if !meta_path.exists() || !idb_path.exists() {
53            return Ok(None);
54        }
55
56        let meta_json = std::fs::read_to_string(&meta_path)?;
57        let meta: CacheMeta = serde_json::from_str(&meta_json)?;
58
59        let idb_data = std::fs::read(&idb_path)?;
60        let sr_data = simplerow::read_from_bytes(&idb_data)?;
61
62        let relations: Vec<_> = sr_data.tables.into_iter().collect();
63        let snapshot = IdbSnapshot { relations };
64
65        Ok(Some((meta, snapshot)))
66    }
67
68    fn save(&self, db_name: &str, meta: &CacheMeta, snapshot: &IdbSnapshot) -> Result<()> {
69        std::fs::create_dir_all(&self.dir)?;
70
71        let meta_json = serde_json::to_string_pretty(meta)?;
72        std::fs::write(self.meta_path(db_name), meta_json)?;
73
74        let mut file = std::fs::File::create(self.idb_path(db_name))?;
75        let tables: Vec<_> = snapshot
76            .relations
77            .iter()
78            .map(|(name, facts)| (name.clone(), facts.clone()))
79            .collect();
80        simplerow::write_simple_row(&mut file, &tables)?;
81
82        Ok(())
83    }
84
85    fn invalidate(&self, db_name: &str) -> Result<()> {
86        let meta_path = self.meta_path(db_name);
87        let idb_path = self.idb_path(db_name);
88
89        if meta_path.exists() {
90            std::fs::remove_file(&meta_path)?;
91        }
92        if idb_path.exists() {
93            std::fs::remove_file(&idb_path)?;
94        }
95
96        Ok(())
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use mangle_common::Value;
104
105    #[test]
106    fn test_file_idb_backend_round_trip() -> Result<()> {
107        let dir = tempfile::tempdir()?;
108        let backend = FileIdbBackend::new(dir.path());
109
110        // Initially empty
111        assert!(backend.load("test")?.is_none());
112
113        let meta = CacheMeta {
114            program_hash: [0xAB; 32],
115            edb_fingerprint: vec![0xCD; 32],
116            created_at: 1234567890,
117        };
118
119        let snapshot = IdbSnapshot {
120            relations: vec![(
121                "derived".to_string(),
122                vec![
123                    vec![Value::Number(1), Value::Number(2)],
124                    vec![Value::Number(3), Value::Number(4)],
125                ],
126            )],
127        };
128
129        backend.save("test", &meta, &snapshot)?;
130
131        let (loaded_meta, loaded_snapshot) = backend.load("test")?.expect("should exist");
132        assert_eq!(loaded_meta.program_hash, meta.program_hash);
133        assert_eq!(loaded_meta.edb_fingerprint, meta.edb_fingerprint);
134        assert_eq!(loaded_meta.created_at, meta.created_at);
135        assert_eq!(loaded_snapshot.relations.len(), 1);
136        assert_eq!(loaded_snapshot.relations[0].0, "derived");
137        assert_eq!(loaded_snapshot.relations[0].1.len(), 2);
138
139        // Invalidate
140        backend.invalidate("test")?;
141        assert!(backend.load("test")?.is_none());
142
143        Ok(())
144    }
145}