shard_core/store/
sqlite.rs1use 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}