Skip to main content

shard_core/store/
sqlite.rs

1use crate::chunker::Chunk;
2use anyhow::Result;
3use shard_storage::{open_backend, StorageBackend};
4use std::path::Path;
5
6pub struct SqliteStore {
7    backend: Box<dyn StorageBackend>,
8}
9
10impl SqliteStore {
11    pub fn new(root: &Path) -> Result<Self> {
12        let db_path = root.join("objects.db");
13        let backend = open_backend(&db_path, "sqlite")?;
14        Ok(Self { backend })
15    }
16
17    pub fn put_chunk(&self, chunk: &Chunk) -> Result<()> {
18        let hash_hex = chunk.hash.to_hex().to_string();
19        if !self.backend.contains(hash_hex.as_bytes())? {
20            self.backend.put(hash_hex.as_bytes(), &chunk.data)?;
21        }
22        Ok(())
23    }
24
25    pub fn get_chunk(&self, hash_hex: &str) -> Result<Vec<u8>> {
26        self.backend
27            .get(hash_hex.as_bytes())?
28            .ok_or_else(|| anyhow::anyhow!("Chunk not found: {}", hash_hex))
29    }
30
31    pub fn has_chunk(&self, hash_hex: &str) -> bool {
32        self.backend.contains(hash_hex.as_bytes()).unwrap_or(false)
33    }
34
35    pub fn iter_chunks(&self) -> Result<Vec<(String, String)>> {
36        let results = self.backend.iter_prefix(b"")?;
37        Ok(results
38            .into_iter()
39            .map(|(k, _)| {
40                let hash = String::from_utf8_lossy(&k).to_string();
41                (hash.clone(), hash)
42            })
43            .collect())
44    }
45
46    pub fn delete_chunk(&self, hash_hex: &str, _full_path: Option<&str>) -> Result<()> {
47        self.backend.delete(hash_hex.as_bytes())?;
48        Ok(())
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::chunker::Chunk;
56    use tempfile::tempdir;
57
58    fn fake_chunk(data: &[u8]) -> Chunk {
59        Chunk {
60            hash: blake3::hash(data),
61            data: data.to_vec(),
62            offset: 0,
63        }
64    }
65
66    #[test]
67    fn test_sqlite_put_get_roundtrip() {
68        let dir = tempdir().unwrap();
69        let store = SqliteStore::new(dir.path()).unwrap();
70        let chunk = fake_chunk(b"sqlite test data");
71        store.put_chunk(&chunk).unwrap();
72        let hash_hex = chunk.hash.to_hex().to_string();
73        assert!(store.has_chunk(&hash_hex));
74        let retrieved = store.get_chunk(&hash_hex).unwrap();
75        assert_eq!(retrieved, b"sqlite test data");
76    }
77
78    #[test]
79    fn test_sqlite_get_nonexistent() {
80        let dir = tempdir().unwrap();
81        let store = SqliteStore::new(dir.path()).unwrap();
82        let result = store.get_chunk("nonexistent");
83        assert!(result.is_err());
84    }
85
86    #[test]
87    fn test_sqlite_delete_chunk() {
88        let dir = tempdir().unwrap();
89        let store = SqliteStore::new(dir.path()).unwrap();
90        let chunk = fake_chunk(b"sqlite delete");
91        store.put_chunk(&chunk).unwrap();
92        let hash_hex = chunk.hash.to_hex().to_string();
93        assert!(store.has_chunk(&hash_hex));
94        store.delete_chunk(&hash_hex, None).unwrap();
95        assert!(!store.has_chunk(&hash_hex));
96    }
97
98    #[test]
99    fn test_sqlite_iter_chunks() {
100        let dir = tempdir().unwrap();
101        let store = SqliteStore::new(dir.path()).unwrap();
102        let chunks = vec![fake_chunk(b"sqlite a"), fake_chunk(b"sqlite b")];
103        for c in &chunks {
104            store.put_chunk(c).unwrap();
105        }
106        let entries = store.iter_chunks().unwrap();
107        assert_eq!(entries.len(), 2);
108    }
109}