prolly-map 0.3.0

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

use common::configured_prolly;
use prolly::{Config, MemStore, Prolly};

#[test]
fn put_get_delete_and_range_are_ordered() {
    let prolly = configured_prolly();
    let mut tree = prolly.create();

    tree = prolly.put(&tree, b"b".to_vec(), b"2".to_vec()).unwrap();
    tree = prolly.put(&tree, b"a".to_vec(), b"1".to_vec()).unwrap();
    tree = prolly.put(&tree, b"c".to_vec(), b"3".to_vec()).unwrap();

    assert_eq!(prolly.get(&tree, b"a").unwrap(), Some(b"1".to_vec()));
    assert_eq!(prolly.get(&tree, b"missing").unwrap(), None);

    let entries = prolly
        .range(&tree, b"a", Some(b"c"))
        .unwrap()
        .collect::<Result<Vec<_>, _>>()
        .unwrap();
    assert_eq!(
        entries,
        vec![
            (b"a".to_vec(), b"1".to_vec()),
            (b"b".to_vec(), b"2".to_vec())
        ]
    );

    let tree = prolly.delete(&tree, b"b").unwrap();
    assert_eq!(prolly.get(&tree, b"b").unwrap(), None);
}

#[test]
fn updates_are_immutable_and_content_addressed() {
    let prolly = configured_prolly();
    let base = prolly.create();
    let first = prolly.put(&base, b"key".to_vec(), b"old".to_vec()).unwrap();
    let second = prolly
        .put(&first, b"key".to_vec(), b"new".to_vec())
        .unwrap();

    assert_eq!(prolly.get(&first, b"key").unwrap(), Some(b"old".to_vec()));
    assert_eq!(prolly.get(&second, b"key").unwrap(), Some(b"new".to_vec()));
    assert_ne!(first.root, second.root);
}

#[test]
fn same_content_builds_same_root() {
    let left = configured_prolly();
    let right = configured_prolly();

    let mut left_tree = left.create();
    let mut right_tree = right.create();
    for (key, value) in [(b"a", b"1"), (b"b", b"2"), (b"c", b"3"), (b"d", b"4")] {
        left_tree = left.put(&left_tree, key.to_vec(), value.to_vec()).unwrap();
        right_tree = right
            .put(&right_tree, key.to_vec(), value.to_vec())
            .unwrap();
    }

    assert_eq!(left_tree.root, right_tree.root);
}

#[test]
fn node_cache_can_be_observed_and_cleared() {
    let prolly = configured_prolly();
    let tree = prolly.create();
    let tree = prolly.put(&tree, b"a".to_vec(), b"1".to_vec()).unwrap();

    assert!(prolly.cache_len() > 0);
    assert_eq!(prolly.get(&tree, b"a").unwrap(), Some(b"1".to_vec()));

    prolly.clear_cache();
    assert_eq!(prolly.cache_len(), 0);
    assert_eq!(prolly.get(&tree, b"a").unwrap(), Some(b"1".to_vec()));
    assert!(prolly.cache_len() > 0);
}

#[test]
fn manager_metrics_track_cache_reads_writes_and_reset() {
    let prolly = configured_prolly();
    assert_eq!(prolly.metrics(), prolly::ProllyMetricsSnapshot::default());

    let tree = prolly.create();
    let tree = prolly.put(&tree, b"a".to_vec(), b"1".to_vec()).unwrap();
    let write_metrics = prolly.metrics();

    assert!(write_metrics.nodes_written > 0);
    assert!(write_metrics.bytes_written > 0);
    assert!(write_metrics.store_batch_put_calls > 0);
    assert_eq!(
        write_metrics.store_batch_put_nodes,
        write_metrics.nodes_written
    );

    prolly.reset_metrics();
    prolly.clear_cache();

    assert_eq!(prolly.get(&tree, b"a").unwrap(), Some(b"1".to_vec()));
    let cold_metrics = prolly.metrics();
    assert!(cold_metrics.node_cache_misses > 0);
    assert!(cold_metrics.nodes_read > 0);
    assert!(cold_metrics.bytes_read > 0);

    assert_eq!(prolly.get(&tree, b"a").unwrap(), Some(b"1".to_vec()));
    let warm_metrics = prolly.metrics();
    assert!(warm_metrics.node_cache_hits > cold_metrics.node_cache_hits);
    assert_eq!(warm_metrics.nodes_read, cold_metrics.nodes_read);

    prolly.reset_metrics();
    assert_eq!(prolly.metrics(), prolly::ProllyMetricsSnapshot::default());
}

#[test]
fn bounded_node_cache_limits_entries_and_preserves_reads() {
    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(4)
        .chunking_factor(2)
        .node_cache_max_nodes(2)
        .build();
    let prolly = Prolly::new(MemStore::new(), config);
    let mut tree = prolly.create();

    for idx in 0..24 {
        tree = prolly
            .put(
                &tree,
                format!("k{idx:03}").into_bytes(),
                format!("v{idx:03}").into_bytes(),
            )
            .unwrap();
        assert!(prolly.cache_len() <= 2);
    }

    assert!(prolly.metrics().node_cache_evictions > 0);
    prolly.reset_metrics();

    for idx in 0..24 {
        assert_eq!(
            prolly.get(&tree, format!("k{idx:03}").as_bytes()).unwrap(),
            Some(format!("v{idx:03}").into_bytes())
        );
        assert!(prolly.cache_len() <= 2);
    }

    let metrics = prolly.metrics();
    assert!(metrics.node_cache_misses > 0);
    assert!(metrics.node_cache_evictions > 0);
}

#[test]
fn pinned_root_survives_bounded_node_cache_eviction() {
    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(4)
        .chunking_factor(2)
        .node_cache_max_nodes(1)
        .build();
    let prolly = Prolly::new(MemStore::new(), config);
    let mut tree = prolly.create();

    for idx in 0..32 {
        tree = prolly
            .put(
                &tree,
                format!("k{idx:03}").into_bytes(),
                format!("v{idx:03}").into_bytes(),
            )
            .unwrap();
    }
    assert!(prolly.collect_stats(&tree).unwrap().tree_height > 0);

    prolly.clear_cache();
    assert_eq!(prolly.pin_tree_root(&tree).unwrap(), 1);
    assert_eq!(prolly.cache_pinned_len(), 1);
    assert!(prolly.cache_pinned_bytes_len() > 0);
    assert_eq!(prolly.pin_tree_root(&tree).unwrap(), 0);

    for idx in 0..32 {
        assert_eq!(
            prolly.get(&tree, format!("k{idx:03}").as_bytes()).unwrap(),
            Some(format!("v{idx:03}").into_bytes())
        );
        assert_eq!(prolly.cache_pinned_len(), 1);
    }

    assert_eq!(prolly.unpin_all_cache_nodes(), 1);
    assert_eq!(prolly.cache_pinned_len(), 0);
    assert!(prolly.cache_len() <= 1);
}

#[test]
fn pinned_path_can_exceed_cache_limit_until_unpinned() {
    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(4)
        .chunking_factor(2)
        .node_cache_max_nodes(1)
        .build();
    let prolly = Prolly::new(MemStore::new(), config);
    let mut tree = prolly.create();

    for idx in 0..64 {
        tree = prolly
            .put(
                &tree,
                format!("k{idx:03}").into_bytes(),
                format!("v{idx:03}").into_bytes(),
            )
            .unwrap();
    }
    assert!(prolly.collect_stats(&tree).unwrap().tree_height > 0);

    prolly.clear_cache();
    let pinned = prolly.pin_tree_path(&tree, b"k031").unwrap();
    assert!(pinned > 1, "multi-level tree should pin root and leaf path");
    assert_eq!(prolly.cache_pinned_len(), pinned);
    assert_eq!(prolly.cache_len(), pinned);
    assert!(prolly.cache_len() > 1);

    assert_eq!(prolly.unpin_all_cache_nodes(), pinned);
    assert_eq!(prolly.cache_pinned_len(), 0);
    assert!(prolly.cache_len() <= 1);
}

#[test]
fn byte_bounded_node_cache_limits_serialized_weight_and_preserves_reads() {
    const CACHE_BYTES: usize = 512;

    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(4)
        .chunking_factor(2)
        .node_cache_max_bytes(CACHE_BYTES)
        .build();
    let prolly = Prolly::new(MemStore::new(), config);
    let mut tree = prolly.create();

    for idx in 0..48 {
        tree = prolly
            .put(
                &tree,
                format!("k{idx:03}").into_bytes(),
                format!("value-{idx:03}-payload").into_bytes(),
            )
            .unwrap();
        assert!(prolly.cache_bytes_len() <= CACHE_BYTES);
    }

    assert!(prolly.metrics().node_cache_evictions > 0);
    prolly.reset_metrics();

    for idx in 0..48 {
        assert_eq!(
            prolly.get(&tree, format!("k{idx:03}").as_bytes()).unwrap(),
            Some(format!("value-{idx:03}-payload").into_bytes())
        );
        assert!(prolly.cache_bytes_len() <= CACHE_BYTES);
    }

    let metrics = prolly.metrics();
    assert!(metrics.node_cache_misses > 0);
    assert!(metrics.node_cache_evictions > 0);
}

#[test]
fn zero_node_cache_max_disables_node_cache() {
    let config = Config::builder().node_cache_max_nodes(0).build();
    let prolly = Prolly::new(MemStore::new(), config);
    let tree = prolly.create();
    let tree = prolly.put(&tree, b"a".to_vec(), b"1".to_vec()).unwrap();

    assert_eq!(prolly.cache_len(), 0);
    prolly.reset_metrics();

    assert_eq!(prolly.get(&tree, b"a").unwrap(), Some(b"1".to_vec()));
    assert_eq!(prolly.get(&tree, b"a").unwrap(), Some(b"1".to_vec()));

    let metrics = prolly.metrics();
    assert_eq!(prolly.cache_len(), 0);
    assert_eq!(metrics.node_cache_hits, 0);
    assert!(metrics.node_cache_misses >= 2);
    assert!(metrics.nodes_read >= 2);
}

#[test]
fn zero_node_cache_max_disables_pinning() {
    let config = Config::builder().node_cache_max_nodes(0).build();
    let prolly = Prolly::new(MemStore::new(), config);
    let tree = prolly.create();
    let tree = prolly.put(&tree, b"a".to_vec(), b"1".to_vec()).unwrap();

    assert_eq!(prolly.pin_tree_root(&tree).unwrap(), 0);
    assert_eq!(prolly.pin_tree_path(&tree, b"a").unwrap(), 0);
    assert_eq!(prolly.cache_len(), 0);
    assert_eq!(prolly.cache_pinned_len(), 0);
    assert_eq!(prolly.unpin_all_cache_nodes(), 0);
}

#[test]
fn zero_node_cache_byte_max_disables_node_cache() {
    let config = Config::builder().node_cache_max_bytes(0).build();
    let prolly = Prolly::new(MemStore::new(), config);
    let tree = prolly.create();
    let tree = prolly.put(&tree, b"a".to_vec(), b"1".to_vec()).unwrap();

    assert_eq!(prolly.cache_len(), 0);
    assert_eq!(prolly.cache_bytes_len(), 0);
    prolly.reset_metrics();

    assert_eq!(prolly.get(&tree, b"a").unwrap(), Some(b"1".to_vec()));
    assert_eq!(prolly.get(&tree, b"a").unwrap(), Some(b"1".to_vec()));

    let metrics = prolly.metrics();
    assert_eq!(prolly.cache_len(), 0);
    assert_eq!(prolly.cache_bytes_len(), 0);
    assert_eq!(metrics.node_cache_hits, 0);
    assert!(metrics.node_cache_misses >= 2);
    assert!(metrics.nodes_read >= 2);
}

#[test]
fn stats_diff_reports_growth_shrink_and_unchanged_trees() {
    let prolly = configured_prolly();
    let mut before = prolly.create();
    for idx in 0..8 {
        before = prolly
            .put(
                &before,
                format!("k{idx:03}").into_bytes(),
                format!("v{idx:03}").into_bytes(),
            )
            .unwrap();
    }

    let mut after = before.clone();
    for idx in 8..16 {
        after = prolly
            .put(
                &after,
                format!("k{idx:03}").into_bytes(),
                format!("v{idx:03}").into_bytes(),
            )
            .unwrap();
    }

    let growth = prolly.stats_diff(&before, &after).unwrap();
    assert_eq!(growth.before, prolly.collect_stats(&before).unwrap());
    assert_eq!(growth.after, prolly.collect_stats(&after).unwrap());
    assert_eq!(growth.absolute.total_key_value_pairs_diff, 8);
    assert!(growth.absolute.num_nodes_diff >= 0);
    assert!(growth.absolute.total_tree_size_bytes_diff > 0);
    assert!(growth.percentage.total_key_value_pairs_pct > 0.0);

    let shrink = prolly.stats_diff(&after, &before).unwrap();
    assert_eq!(shrink.absolute.total_key_value_pairs_diff, -8);
    assert!(shrink.absolute.total_tree_size_bytes_diff < 0);
    assert!(shrink.percentage.total_key_value_pairs_pct < 0.0);

    let unchanged = prolly.stats_diff(&after, &after).unwrap();
    assert_eq!(unchanged.absolute.total_key_value_pairs_diff, 0);
    assert_eq!(unchanged.absolute.total_tree_size_bytes_diff, 0);
    assert_eq!(unchanged.percentage.total_key_value_pairs_pct, 0.0);
}