prolly-map 0.4.0

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

use std::sync::Arc;

use common::{assert_tree_invariants, configured_prolly, entries};
use prolly::{BatchBuilder, Config, MemStore, Mutation, Prolly};

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

    let tree = prolly
        .batch(
            &tree,
            vec![
                Mutation::Upsert {
                    key: b"a".to_vec(),
                    val: b"1".to_vec(),
                },
                Mutation::Upsert {
                    key: b"b".to_vec(),
                    val: b"2".to_vec(),
                },
                Mutation::Delete {
                    key: b"missing".to_vec(),
                },
            ],
        )
        .unwrap();

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

#[test]
fn batch_builder_matches_incremental_tree_for_unsorted_input() {
    let store = Arc::new(MemStore::new());
    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(5)
        .chunking_factor(2)
        .hash_seed(7)
        .build();

    let mut builder = BatchBuilder::new(store.clone(), config.clone());
    for i in (0..64).rev() {
        builder.add(
            format!("key-{i:03}").into_bytes(),
            format!("value-{i}").into_bytes(),
        );
    }
    let built = builder.build().unwrap();

    let prolly = Prolly::new(store, config);
    let mut incremental = prolly.create();
    for i in 0..64 {
        incremental = prolly
            .put(
                &incremental,
                format!("key-{i:03}").into_bytes(),
                format!("value-{i}").into_bytes(),
            )
            .unwrap();
    }

    assert_eq!(entries(&prolly, &built), entries(&prolly, &incremental));
    assert!(built.root.is_some());
    assert!(incremental.root.is_some());
}

#[test]
fn batch_mutations_are_last_write_wins_and_match_repeated_ops() {
    let batched = configured_prolly();
    let mut base = batched.create();
    base = batched.put(&base, b"a".to_vec(), b"old".to_vec()).unwrap();
    base = batched.put(&base, b"b".to_vec(), b"old".to_vec()).unwrap();

    let mutations = vec![
        Mutation::Upsert {
            key: b"b".to_vec(),
            val: b"first".to_vec(),
        },
        Mutation::Delete { key: b"a".to_vec() },
        Mutation::Upsert {
            key: b"c".to_vec(),
            val: b"3".to_vec(),
        },
        Mutation::Upsert {
            key: b"b".to_vec(),
            val: b"second".to_vec(),
        },
    ];

    let batched_tree = batched.batch(&base, mutations).unwrap();

    let repeated = configured_prolly();
    let mut expected = repeated.create();
    expected = repeated
        .put(&expected, b"a".to_vec(), b"old".to_vec())
        .unwrap();
    expected = repeated
        .put(&expected, b"b".to_vec(), b"old".to_vec())
        .unwrap();
    expected = repeated.delete(&expected, b"a").unwrap();
    expected = repeated
        .put(&expected, b"c".to_vec(), b"3".to_vec())
        .unwrap();
    expected = repeated
        .put(&expected, b"b".to_vec(), b"second".to_vec())
        .unwrap();

    assert_eq!(
        entries(&batched, &batched_tree),
        entries(&repeated, &expected)
    );
}

#[test]
fn batch_mutations_across_many_leaves_match_repeated_ops() {
    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(4)
        .chunking_factor(u32::MAX)
        .hash_seed(19)
        .build();
    let batched = Prolly::new(MemStore::new(), config.clone());
    let repeated = Prolly::new(MemStore::new(), config);

    let mut batched_base = batched.create();
    let mut repeated_tree = repeated.create();
    for i in 0..80 {
        let key = format!("k{i:03}").into_bytes();
        let val = format!("v{i:03}").into_bytes();
        batched_base = batched
            .put(&batched_base, key.clone(), val.clone())
            .unwrap();
        repeated_tree = repeated.put(&repeated_tree, key, val).unwrap();
    }

    let mutations = vec![
        Mutation::Upsert {
            key: b"k003".to_vec(),
            val: b"updated-left".to_vec(),
        },
        Mutation::Delete {
            key: b"k017".to_vec(),
        },
        Mutation::Upsert {
            key: b"k039".to_vec(),
            val: b"updated-middle".to_vec(),
        },
        Mutation::Delete {
            key: b"k058".to_vec(),
        },
        Mutation::Upsert {
            key: b"k071".to_vec(),
            val: b"updated-right".to_vec(),
        },
    ];

    let batched_tree = batched.batch(&batched_base, mutations.clone()).unwrap();

    for mutation in mutations {
        repeated_tree = match mutation {
            Mutation::Upsert { key, val } => repeated.put(&repeated_tree, key, val).unwrap(),
            Mutation::Delete { key } => repeated.delete(&repeated_tree, &key).unwrap(),
        };
    }

    assert_eq!(
        entries(&batched, &batched_tree),
        entries(&repeated, &repeated_tree)
    );
}

#[test]
fn batch_mutations_across_many_leaves_handle_inserts_deletes_and_splits() {
    let batched_store = Arc::new(MemStore::new());
    let repeated_store = Arc::new(MemStore::new());
    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(4)
        .chunking_factor(u32::MAX)
        .hash_seed(23)
        .build();
    let batched = Prolly::new(batched_store.clone(), config.clone());
    let repeated = Prolly::new(repeated_store.clone(), config.clone());

    let mut batched_base = batched.create();
    let mut repeated_tree = repeated.create();
    for i in 0..96 {
        let key = format!("k{i:03}").into_bytes();
        let val = format!("v{i:03}").into_bytes();
        batched_base = batched
            .put(&batched_base, key.clone(), val.clone())
            .unwrap();
        repeated_tree = repeated.put(&repeated_tree, key, val).unwrap();
    }

    let mutations = vec![
        Mutation::Upsert {
            key: b"k003a".to_vec(),
            val: b"insert-left-gap".to_vec(),
        },
        Mutation::Upsert {
            key: b"k004a".to_vec(),
            val: b"insert-left-split".to_vec(),
        },
        Mutation::Delete {
            key: b"k018".to_vec(),
        },
        Mutation::Delete {
            key: b"k019".to_vec(),
        },
        Mutation::Upsert {
            key: b"k039a".to_vec(),
            val: b"insert-middle-gap".to_vec(),
        },
        Mutation::Upsert {
            key: b"k040a".to_vec(),
            val: b"insert-middle-split".to_vec(),
        },
        Mutation::Delete {
            key: b"k070".to_vec(),
        },
        Mutation::Upsert {
            key: b"k090a".to_vec(),
            val: b"insert-right-gap".to_vec(),
        },
    ];

    let batched_tree = batched.batch(&batched_base, mutations.clone()).unwrap();

    for mutation in mutations {
        repeated_tree = match mutation {
            Mutation::Upsert { key, val } => repeated.put(&repeated_tree, key, val).unwrap(),
            Mutation::Delete { key } => repeated.delete(&repeated_tree, &key).unwrap(),
        };
    }

    assert_eq!(
        entries(&batched, &batched_tree),
        entries(&repeated, &repeated_tree)
    );
    assert_tree_invariants(&batched_store, &batched_tree, &config);
}