prolly-store-sqlite 0.1.0

SQLite store adapter for prolly-map.
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

use prolly::{Config, Error, Prolly};
use prolly_store_sqlite::SqliteStore;

#[test]
fn sqlite_store_is_send_and_sync() {
    fn assert_send_sync<T: Send + Sync>() {}

    assert_send_sync::<SqliteStore>();
}

#[test]
fn sqlite_store_supports_store_contract_operations() {
    let store = SqliteStore::open_in_memory().unwrap();
    prolly_store_test::assert_store_contract(&store);
}

#[test]
fn sqlite_store_supports_manifest_contract_operations() {
    let store = SqliteStore::open_in_memory().unwrap();
    prolly_store_test::assert_manifest_store_contract(&store);
}

#[test]
fn sqlite_store_supports_node_scan_contract_operations() {
    let store = SqliteStore::open_in_memory().unwrap();
    prolly_store_test::assert_node_store_scan_contract(store);
}

#[test]
fn sqlite_store_commits_and_rolls_back_strict_transactions() {
    let store = Arc::new(SqliteStore::open_in_memory().unwrap());
    let prolly = Prolly::new(store, Config::default());

    let committed = prolly
        .transaction(|tx| {
            let tree = tx.put(&tx.create(), b"key".to_vec(), b"committed".to_vec())?;
            tx.publish_named_root(b"main", &tree)?;
            Ok(tree)
        })
        .unwrap();
    assert_eq!(
        prolly.load_named_root(b"main").unwrap(),
        Some(committed.clone())
    );

    let result: Result<(), Error> = prolly.transaction(|tx| {
        let tree = tx.put(&committed, b"key".to_vec(), b"rolled-back".to_vec())?;
        tx.publish_named_root(b"main", &tree)?;
        Err(Error::Serialize("force rollback".to_string()))
    });
    assert!(matches!(result, Err(Error::Serialize(_))));
    assert_eq!(prolly.load_named_root(b"main").unwrap(), Some(committed));
}

#[test]
fn sqlite_store_persists_prolly_tree_nodes_across_reopen() {
    let path = temp_db_path("persist");
    remove_sqlite_files(&path);

    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(4)
        .chunking_factor(2)
        .build();

    let tree = {
        let store = SqliteStore::open(&path).unwrap();
        let prolly = Prolly::new(store, config.clone());
        let mut tree = prolly.create();

        for i in 0..40 {
            tree = prolly
                .put(
                    &tree,
                    format!("k{i:02}").into_bytes(),
                    format!("v{i:02}").into_bytes(),
                )
                .unwrap();
        }

        tree
    };

    {
        let store = SqliteStore::open(&path).unwrap();
        let prolly = Prolly::new(store, config);

        assert_eq!(prolly.get(&tree, b"k00").unwrap(), Some(b"v00".to_vec()));
        assert_eq!(prolly.get(&tree, b"k17").unwrap(), Some(b"v17".to_vec()));
        assert_eq!(prolly.get(&tree, b"k39").unwrap(), Some(b"v39".to_vec()));
        assert_eq!(prolly.get(&tree, b"missing").unwrap(), None);
    }

    remove_sqlite_files(&path);
}

#[test]
fn sqlite_store_persists_named_root_across_reopen() {
    let path = temp_db_path("root-manifest");
    remove_sqlite_files(&path);

    let config = Config::builder()
        .min_chunk_size(2)
        .max_chunk_size(4)
        .chunking_factor(2)
        .build();

    let tree = {
        let store = Arc::new(SqliteStore::open(&path).unwrap());
        let prolly = Prolly::new(store.clone(), config.clone());
        let tree = prolly.create();
        let tree = prolly
            .put(&tree, b"project/name".to_vec(), b"trail".to_vec())
            .unwrap();

        prolly.publish_named_root(b"main", &tree).unwrap();
        tree
    };

    {
        let store = Arc::new(SqliteStore::open(&path).unwrap());
        let prolly = Prolly::new(store, config.clone());
        let loaded = prolly.load_named_root(b"main").unwrap().unwrap();
        assert_eq!(loaded, tree);

        assert_eq!(
            prolly.get(&loaded, b"project/name").unwrap(),
            Some(b"trail".to_vec())
        );
    }

    remove_sqlite_files(&path);
}

fn temp_db_path(label: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    std::env::temp_dir().join(format!(
        "trail-prolly-{label}-{}-{nanos}.db",
        std::process::id()
    ))
}

fn remove_sqlite_files(path: &Path) {
    let _ = fs::remove_file(path);
    let _ = fs::remove_file(path.with_extension("db-wal"));
    let _ = fs::remove_file(path.with_extension("db-shm"));
}