redb-extras 0.8.0

Use-case agnostic utilities for redb, featuring sharded roaring bitmap tables
Documentation
use super::{copy_database, CopyPlan, DbCopyError};
use crate::Error;
use redb::{Database, MultimapTableDefinition, ReadableDatabase, TableDefinition};
use tempfile::NamedTempFile;

const USERS: TableDefinition<&str, u64> = TableDefinition::new("users");
const BLOBS: TableDefinition<&str, &[u8]> = TableDefinition::new("blobs");
const TAGS: MultimapTableDefinition<&str, u64> = MultimapTableDefinition::new("tags");

#[test]
fn copies_tables_and_multimaps() {
    let source_file = NamedTempFile::new().unwrap();
    let dest_file = NamedTempFile::new().unwrap();
    let source = Database::create(source_file.path()).unwrap();
    let dest = Database::create(dest_file.path()).unwrap();

    let write_txn = source.begin_write().unwrap();
    {
        let mut users = write_txn.open_table(USERS).unwrap();
        users.insert("alice", 1).unwrap();
        users.insert("bob", 2).unwrap();

        let mut blobs = write_txn.open_table(BLOBS).unwrap();
        blobs.insert("one", b"first".as_slice()).unwrap();
        blobs.insert("two", b"second".as_slice()).unwrap();

        let mut tags = write_txn.open_multimap_table(TAGS).unwrap();
        tags.insert("alice", 10).unwrap();
        tags.insert("alice", 20).unwrap();
        tags.insert("bob", 30).unwrap();
    }
    write_txn.commit().unwrap();

    let plan = CopyPlan::new().table(USERS).table(BLOBS).multimap(TAGS);

    copy_database(&source, &dest, &plan).unwrap();

    let read_txn = dest.begin_read().unwrap();
    let users = read_txn.open_table(USERS).unwrap();
    let blobs = read_txn.open_table(BLOBS).unwrap();
    let tags = read_txn.open_multimap_table(TAGS).unwrap();

    assert_eq!(users.get("alice").unwrap().unwrap().value(), 1);
    assert_eq!(users.get("bob").unwrap().unwrap().value(), 2);
    assert_eq!(
        blobs.get("one").unwrap().unwrap().value(),
        b"first".as_slice()
    );
    assert_eq!(
        blobs.get("two").unwrap().unwrap().value(),
        b"second".as_slice()
    );

    let mut alice_tags: Vec<u64> = tags
        .get("alice")
        .unwrap()
        .map(|value| value.unwrap().value())
        .collect();
    alice_tags.sort_unstable();
    assert_eq!(alice_tags, vec![10, 20]);

    let bob_tags: Vec<u64> = tags
        .get("bob")
        .unwrap()
        .map(|value| value.unwrap().value())
        .collect();
    assert_eq!(bob_tags, vec![30]);
}

#[test]
fn destination_conflicts_detected_before_copy() {
    let source_file = NamedTempFile::new().unwrap();
    let dest_file = NamedTempFile::new().unwrap();
    let source = Database::create(source_file.path()).unwrap();
    let dest = Database::create(dest_file.path()).unwrap();

    let source_txn = source.begin_write().unwrap();
    {
        let mut users = source_txn.open_table(USERS).unwrap();
        users.insert("alice", 1).unwrap();
    }
    source_txn.commit().unwrap();

    let dest_txn = dest.begin_write().unwrap();
    {
        let mut users = dest_txn.open_table(USERS).unwrap();
        users.insert("existing", 99).unwrap();
    }
    dest_txn.commit().unwrap();

    let plan = CopyPlan::new().table(USERS);
    let result = copy_database(&source, &dest, &plan);

    match result {
        Err(Error::DbCopy(DbCopyError::DestinationTablesExist(conflicts))) => {
            assert_eq!(conflicts, vec!["table users"]);
        }
        other => panic!("unexpected result: {other:?}"),
    }
}