prolly-map 0.4.0

Content-addressed versioned map storage primitives.
Documentation
mod common;

use std::sync::Arc;

use prolly::{Cid, Config, MemStore, Prolly, SecondaryIndex, SecondaryIndexRegistry, Store};

#[test]
fn mark_reachable_deduplicates_roots_and_shared_nodes() {
    let prolly = common::configured_prolly();
    let empty = prolly.create();
    let tree = prolly.put(&empty, b"k1".to_vec(), b"v1".to_vec()).unwrap();
    let tree = prolly.put(&tree, b"k2".to_vec(), b"v2".to_vec()).unwrap();

    let reachable = prolly
        .mark_reachable(&[empty, tree.clone(), tree.clone()])
        .unwrap();

    assert!(reachable.live_nodes > 0);
    assert_eq!(reachable.live_nodes, reachable.cids().len());
    assert_eq!(
        reachable.live_nodes,
        reachable.leaf_nodes + reachable.internal_nodes
    );
    assert!(reachable.live_bytes > 0);
    assert!(reachable.contains(tree.root.as_ref().unwrap()));
}

#[test]
fn plan_gc_reports_reclaimable_and_missing_candidates_without_deleting() {
    let store = Arc::new(MemStore::new());
    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(4)
        .chunking_factor(2)
        .build();
    let prolly = Prolly::new(store.clone(), config);

    let base = prolly.create();
    let base = prolly.put(&base, b"k".to_vec(), b"old".to_vec()).unwrap();
    let updated = prolly.put(&base, b"k".to_vec(), b"new".to_vec()).unwrap();

    let mut candidates = prolly
        .mark_reachable(&[base.clone(), updated.clone()])
        .unwrap();
    let missing = Cid::from_bytes(b"missing-gc-candidate");
    candidates.live_cids.push(missing);

    let plan = prolly
        .plan_gc(std::slice::from_ref(&updated), &candidates.live_cids)
        .unwrap();

    assert!(plan.reclaimable_nodes > 0);
    assert!(plan.reclaimable_bytes > 0);
    assert_eq!(plan.missing_candidates, 1);
    assert_eq!(
        plan.candidate_nodes,
        plan.retained_candidate_nodes() + plan.reclaimable_nodes + plan.missing_candidates
    );
    for cid in plan.reclaimable_cids() {
        assert!(!plan.reachability.contains(cid));
        assert!(store.get(cid.as_bytes()).unwrap().is_some());
    }
}

#[test]
fn sweep_gc_deletes_only_unreachable_candidates_and_clears_manager_cache() {
    let store = Arc::new(MemStore::new());
    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(4)
        .chunking_factor(2)
        .build();
    let prolly = Prolly::new(store.clone(), config);

    let base = prolly.create();
    let base = prolly.put(&base, b"k".to_vec(), b"old".to_vec()).unwrap();
    let updated = prolly.put(&base, b"k".to_vec(), b"new".to_vec()).unwrap();

    let candidates = prolly
        .mark_reachable(&[base.clone(), updated.clone()])
        .unwrap()
        .into_cids();
    assert!(prolly.cache_len() > 0);

    let sweep = prolly
        .sweep_gc(std::slice::from_ref(&updated), &candidates)
        .unwrap();

    assert!(sweep.deleted_nodes > 0);
    assert_eq!(sweep.deleted_nodes, sweep.plan.reclaimable_nodes);
    assert_eq!(sweep.deleted_bytes, sweep.plan.reclaimable_bytes);
    assert_eq!(prolly.cache_len(), 0);
    for cid in sweep.plan.reclaimable_cids() {
        assert_eq!(store.get(cid.as_bytes()).unwrap(), None);
    }

    assert_eq!(prolly.get(&updated, b"k").unwrap(), Some(b"new".to_vec()));
    assert!(prolly.get(&base, b"k").is_err());
}

#[test]
fn indexed_gc_plan_is_global_and_keeps_unrelated_named_roots() {
    let prolly = Prolly::new(Arc::new(MemStore::new()), Config::default());
    let unrelated = prolly
        .versioned_map(b"unrelated")
        .put(b"key", b"value")
        .unwrap();
    let registry = SecondaryIndexRegistry::new()
        .register(
            SecondaryIndex::non_unique("by-value", 1, "gc.by-value/v1", |_, value| {
                Ok(vec![value.to_vec()])
            })
            .unwrap(),
        )
        .unwrap();
    let indexed = prolly.indexed_map(b"users", registry).unwrap();
    indexed.put(b"user-1", b"active").unwrap();
    indexed.ensure_index(b"by-value").unwrap();
    indexed.keep_last(0).unwrap();

    let plan = indexed.plan_indexed_gc().unwrap();
    assert!(plan
        .reachability
        .contains(unrelated.tree.root.as_ref().unwrap()));
}