shard-core 2.2.1

Core library for shard distributed VCS: chunking, compression, commits, branching, merging, WAL
Documentation
use crate::chunker::Chunk;
use anyhow::Result;
use shard_storage::{open_backend, StorageBackend};
use std::path::Path;

pub struct SqliteStore {
    backend: Box<dyn StorageBackend>,
}

impl SqliteStore {
    pub fn new(root: &Path) -> Result<Self> {
        let db_path = root.join("objects.db");
        let backend = open_backend(&db_path, "sqlite")?;
        Ok(Self { backend })
    }

    pub fn put_chunk(&self, chunk: &Chunk) -> Result<()> {
        let hash_hex = chunk.hash.to_hex().to_string();
        if !self.backend.contains(hash_hex.as_bytes())? {
            self.backend.put(hash_hex.as_bytes(), &chunk.data)?;
        }
        Ok(())
    }

    pub fn get_chunk(&self, hash_hex: &str) -> Result<Vec<u8>> {
        self.backend
            .get(hash_hex.as_bytes())?
            .ok_or_else(|| anyhow::anyhow!("Chunk not found: {}", hash_hex))
    }

    pub fn has_chunk(&self, hash_hex: &str) -> bool {
        self.backend.contains(hash_hex.as_bytes()).unwrap_or(false)
    }

    pub fn iter_chunks(&self) -> Result<Vec<(String, String)>> {
        let results = self.backend.iter_prefix(b"")?;
        Ok(results
            .into_iter()
            .map(|(k, _)| {
                let hash = String::from_utf8_lossy(&k).to_string();
                (hash.clone(), hash)
            })
            .collect())
    }

    pub fn delete_chunk(&self, hash_hex: &str, _full_path: Option<&str>) -> Result<()> {
        self.backend.delete(hash_hex.as_bytes())?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::chunker::Chunk;
    use tempfile::tempdir;

    fn fake_chunk(data: &[u8]) -> Chunk {
        Chunk {
            hash: blake3::hash(data),
            data: data.to_vec(),
            offset: 0,
        }
    }

    #[test]
    fn test_sqlite_put_get_roundtrip() {
        let dir = tempdir().unwrap();
        let store = SqliteStore::new(dir.path()).unwrap();
        let chunk = fake_chunk(b"sqlite test data");
        store.put_chunk(&chunk).unwrap();
        let hash_hex = chunk.hash.to_hex().to_string();
        assert!(store.has_chunk(&hash_hex));
        let retrieved = store.get_chunk(&hash_hex).unwrap();
        assert_eq!(retrieved, b"sqlite test data");
    }

    #[test]
    fn test_sqlite_get_nonexistent() {
        let dir = tempdir().unwrap();
        let store = SqliteStore::new(dir.path()).unwrap();
        let result = store.get_chunk("nonexistent");
        assert!(result.is_err());
    }

    #[test]
    fn test_sqlite_delete_chunk() {
        let dir = tempdir().unwrap();
        let store = SqliteStore::new(dir.path()).unwrap();
        let chunk = fake_chunk(b"sqlite delete");
        store.put_chunk(&chunk).unwrap();
        let hash_hex = chunk.hash.to_hex().to_string();
        assert!(store.has_chunk(&hash_hex));
        store.delete_chunk(&hash_hex, None).unwrap();
        assert!(!store.has_chunk(&hash_hex));
    }

    #[test]
    fn test_sqlite_iter_chunks() {
        let dir = tempdir().unwrap();
        let store = SqliteStore::new(dir.path()).unwrap();
        let chunks = vec![fake_chunk(b"sqlite a"), fake_chunk(b"sqlite b")];
        for c in &chunks {
            store.put_chunk(c).unwrap();
        }
        let entries = store.iter_chunks().unwrap();
        assert_eq!(entries.len(), 2);
    }
}