#[cfg(test)]
mod namespaced_tests {
use crate::git::versioned_store::namespaced::*;
use std::collections::HashSet;
use tempfile::TempDir;
struct CwdGuard {
original: std::path::PathBuf,
_lock: std::sync::MutexGuard<'static, ()>,
}
impl CwdGuard {
fn set(path: &std::path::Path) -> Self {
let lock = crate::git::versioned_store::cwd_lock()
.lock()
.expect("CWD mutex poisoned");
let original = std::env::current_dir().expect("Failed to get current dir");
std::env::set_current_dir(path).expect("Failed to change directory");
Self {
original,
_lock: lock,
}
}
}
impl Drop for CwdGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original);
}
}
fn setup_git_repo() -> (TempDir, std::path::PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let repo_path = temp_dir.path();
std::process::Command::new("git")
.args(["init"])
.current_dir(repo_path)
.output()
.expect("git init failed");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(repo_path)
.output()
.expect("git config name failed");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(repo_path)
.output()
.expect("git config email failed");
let dataset_path = repo_path.join("dataset");
std::fs::create_dir(&dataset_path).expect("Failed to create dataset dir");
(temp_dir, dataset_path)
}
#[test]
fn test_namespace_insert_get_delete() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
{
let mut ns = store.namespace("users");
ns.insert(b"user:1".to_vec(), b"Alice".to_vec())
.expect("insert failed");
ns.insert(b"user:2".to_vec(), b"Bob".to_vec())
.expect("insert failed");
}
{
let ns = store.namespace("users");
assert_eq!(ns.get(b"user:1"), Some(b"Alice".to_vec()));
assert_eq!(ns.get(b"user:2"), Some(b"Bob".to_vec()));
assert_eq!(ns.get(b"user:3"), None);
}
{
let mut ns = store.namespace("users");
assert!(ns.delete(b"user:1").expect("delete failed"));
assert!(!ns.delete(b"user:999").expect("delete failed"));
}
{
let ns = store.namespace("users");
assert_eq!(ns.get(b"user:1"), None);
assert_eq!(ns.get(b"user:2"), Some(b"Bob".to_vec()));
}
}
#[test]
fn test_namespace_list_keys() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
{
let mut ns = store.namespace("products");
ns.insert(b"prod:1".to_vec(), b"Laptop".to_vec()).unwrap();
ns.insert(b"prod:2".to_vec(), b"Mouse".to_vec()).unwrap();
ns.insert(b"prod:3".to_vec(), b"Keyboard".to_vec()).unwrap();
}
let keys = store.namespace("products").list_keys();
assert_eq!(keys.len(), 3);
assert!(keys.contains(&b"prod:1".to_vec()));
assert!(keys.contains(&b"prod:2".to_vec()));
assert!(keys.contains(&b"prod:3".to_vec()));
}
#[test]
fn test_multiple_namespaces_isolation() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.namespace("ns_a")
.insert(b"key1".to_vec(), b"value_a".to_vec())
.unwrap();
store
.namespace("ns_b")
.insert(b"key1".to_vec(), b"value_b".to_vec())
.unwrap();
assert_eq!(
store.namespace("ns_a").get(b"key1"),
Some(b"value_a".to_vec())
);
assert_eq!(
store.namespace("ns_b").get(b"key1"),
Some(b"value_b".to_vec())
);
let keys_a = store.namespace("ns_a").list_keys();
let keys_b = store.namespace("ns_b").list_keys();
assert_eq!(keys_a.len(), 1);
assert_eq!(keys_b.len(), 1);
assert!(store.namespace("ns_c").list_keys().is_empty());
assert_eq!(store.namespace("ns_c").get(b"key1"), None);
}
#[test]
fn test_default_namespace_backward_compat() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.insert(b"flat_key".to_vec(), b"flat_value".to_vec())
.unwrap();
assert_eq!(store.get(b"flat_key"), Some(b"flat_value".to_vec()));
assert_eq!(
store.namespace(DEFAULT_NAMESPACE).get(b"flat_key"),
Some(b"flat_value".to_vec())
);
let keys = store.namespace(DEFAULT_NAMESPACE).list_keys();
assert!(keys.contains(&b"flat_key".to_vec()));
}
#[test]
fn test_list_namespaces() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
let ns_list = store.list_namespaces();
assert!(ns_list.contains(&"default".to_string()));
store
.namespace("users")
.insert(b"u1".to_vec(), b"Alice".to_vec())
.unwrap();
store
.namespace("products")
.insert(b"p1".to_vec(), b"Widget".to_vec())
.unwrap();
store
.namespace("orders")
.insert(b"o1".to_vec(), b"Order1".to_vec())
.unwrap();
let ns_list = store.list_namespaces();
assert_eq!(ns_list.len(), 4); assert!(ns_list.contains(&"users".to_string()));
assert!(ns_list.contains(&"products".to_string()));
assert!(ns_list.contains(&"orders".to_string()));
assert!(ns_list.contains(&"default".to_string()));
}
#[test]
fn test_delete_namespace() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.namespace("temp")
.insert(b"key".to_vec(), b"value".to_vec())
.unwrap();
assert_eq!(store.list_namespaces().len(), 2);
assert!(store.delete_namespace("temp").unwrap());
assert!(!store.list_namespaces().contains(&"temp".to_string()));
assert!(!store.delete_namespace("temp").unwrap());
assert!(store.delete_namespace("default").is_err());
}
#[test]
fn test_get_namespace_root_hash() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
let hash_before = store.get_namespace_root_hash("default");
assert!(hash_before.is_some());
store
.namespace("default")
.insert(b"key".to_vec(), b"value".to_vec())
.unwrap();
store.commit("add key").unwrap();
let hash_after = store.get_namespace_root_hash("default");
assert!(hash_after.is_some());
store
.namespace("other")
.insert(b"okey".to_vec(), b"oval".to_vec())
.unwrap();
store.commit("add other ns").unwrap();
let hash_default = store.get_namespace_root_hash("default");
let hash_other = store.get_namespace_root_hash("other");
assert!(hash_default.is_some());
assert!(hash_other.is_some());
assert_ne!(hash_default, hash_other);
assert!(store.get_namespace_root_hash("nonexistent").is_none());
}
#[test]
fn test_namespace_commit_and_reopen() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
{
let mut store =
GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.namespace("users")
.insert(b"user:1".to_vec(), b"Alice".to_vec())
.unwrap();
store
.namespace("products")
.insert(b"prod:1".to_vec(), b"Widget".to_vec())
.unwrap();
store
.insert(b"default_key".to_vec(), b"default_val".to_vec())
.unwrap();
store.commit("Add data across namespaces").unwrap();
}
{
let mut store =
GitNamespacedKvStore::<32>::open(&dataset_path).expect("Failed to open");
assert_eq!(store.format_version, StoreFormatVersion::V2);
let ns_list = store.list_namespaces();
assert!(ns_list.contains(&"users".to_string()));
assert!(ns_list.contains(&"products".to_string()));
assert!(ns_list.contains(&"default".to_string()));
assert_eq!(
store.namespace("users").get(b"user:1"),
Some(b"Alice".to_vec())
);
assert_eq!(
store.namespace("products").get(b"prod:1"),
Some(b"Widget".to_vec())
);
assert_eq!(store.get(b"default_key"), Some(b"default_val".to_vec()));
}
}
#[test]
fn test_namespace_staging_isolation() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.namespace("ns_a")
.insert(b"key".to_vec(), b"staged_a".to_vec())
.unwrap();
store
.namespace("ns_b")
.insert(b"key".to_vec(), b"staged_b".to_vec())
.unwrap();
assert_eq!(
store.namespace("ns_a").get(b"key"),
Some(b"staged_a".to_vec())
);
assert_eq!(
store.namespace("ns_b").get(b"key"),
Some(b"staged_b".to_vec())
);
store.namespace("ns_a").delete(b"key").unwrap();
assert_eq!(store.namespace("ns_a").get(b"key"), None);
assert_eq!(
store.namespace("ns_b").get(b"key"),
Some(b"staged_b".to_vec())
);
}
#[test]
fn test_namespace_dirty_tracking() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.namespace("ns_a")
.insert(b"k1".to_vec(), b"v1".to_vec())
.unwrap();
store.commit("add ns_a data").unwrap();
assert_eq!(store.namespace("ns_a").get(b"k1"), Some(b"v1".to_vec()));
store
.namespace("ns_b")
.insert(b"k2".to_vec(), b"v2".to_vec())
.unwrap();
store.commit("add ns_b data").unwrap();
assert_eq!(store.namespace("ns_a").get(b"k1"), Some(b"v1".to_vec()));
assert_eq!(store.namespace("ns_b").get(b"k2"), Some(b"v2".to_vec()));
}
#[test]
fn test_namespace_branch_checkout() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.namespace("users")
.insert(b"u1".to_vec(), b"Alice".to_vec())
.unwrap();
store.commit("main data").unwrap();
store.create_branch("feature").unwrap();
store
.namespace("users")
.insert(b"u2".to_vec(), b"Bob".to_vec())
.unwrap();
store.commit("feature data").unwrap();
assert_eq!(store.namespace("users").get(b"u1"), Some(b"Alice".to_vec()));
assert_eq!(store.namespace("users").get(b"u2"), Some(b"Bob".to_vec()));
store.checkout("main").unwrap();
assert_eq!(store.namespace("users").get(b"u1"), Some(b"Alice".to_vec()));
assert_eq!(store.namespace("users").get(b"u2"), None);
store.checkout("feature").unwrap();
assert_eq!(store.namespace("users").get(b"u2"), Some(b"Bob".to_vec()));
}
#[test]
fn test_namespace_across_branches() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.namespace("ns_a")
.insert(b"key".to_vec(), b"main_a".to_vec())
.unwrap();
store.commit("main ns_a").unwrap();
store.create_branch("feature").unwrap();
store
.namespace("ns_b")
.insert(b"key".to_vec(), b"feature_b".to_vec())
.unwrap();
store.commit("feature ns_b").unwrap();
assert!(store.list_namespaces().contains(&"ns_a".to_string()));
assert!(store.list_namespaces().contains(&"ns_b".to_string()));
store.checkout("main").unwrap();
assert!(store.list_namespaces().contains(&"ns_a".to_string()));
}
#[test]
fn test_namespace_merge_no_conflict() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.namespace("ns_a")
.insert(b"key_a".to_vec(), b"val_a".to_vec())
.unwrap();
store.commit("main: ns_a data").unwrap();
store.create_branch("feature").unwrap();
store
.namespace("ns_b")
.insert(b"key_b".to_vec(), b"val_b".to_vec())
.unwrap();
store.commit("feature: ns_b data").unwrap();
store.checkout("main").unwrap();
store.merge_ignore_conflicts("feature").unwrap();
assert_eq!(
store.namespace("ns_a").get(b"key_a"),
Some(b"val_a".to_vec())
);
assert_eq!(
store.namespace("ns_b").get(b"key_b"),
Some(b"val_b".to_vec())
);
}
#[test]
fn test_namespace_merge_same_namespace() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.namespace("shared")
.insert(b"base_key".to_vec(), b"base_val".to_vec())
.unwrap();
store.commit("main: base data").unwrap();
store.create_branch("feature").unwrap();
store
.namespace("shared")
.insert(b"feature_key".to_vec(), b"feature_val".to_vec())
.unwrap();
store.commit("feature: add feature_key").unwrap();
store.checkout("main").unwrap();
store
.namespace("shared")
.insert(b"main_key".to_vec(), b"main_val".to_vec())
.unwrap();
store.commit("main: add main_key").unwrap();
store.merge_ignore_conflicts("feature").unwrap();
assert_eq!(
store.namespace("shared").get(b"base_key"),
Some(b"base_val".to_vec())
);
assert_eq!(
store.namespace("shared").get(b"feature_key"),
Some(b"feature_val".to_vec())
);
assert_eq!(
store.namespace("shared").get(b"main_key"),
Some(b"main_val".to_vec())
);
}
#[test]
fn test_namespace_changed_detection() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let mut store = GitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.namespace("ns_a")
.insert(b"k1".to_vec(), b"v1".to_vec())
.unwrap();
let commit1 = store.commit("add ns_a").unwrap();
let commit1_hex = commit1.to_hex().to_string();
store
.namespace("ns_b")
.insert(b"k2".to_vec(), b"v2".to_vec())
.unwrap();
let commit2 = store.commit("add ns_b").unwrap();
let commit2_hex = commit2.to_hex().to_string();
assert!(!store
.namespace_changed("ns_a", &commit1_hex, &commit2_hex)
.unwrap());
assert!(store
.namespace_changed("ns_b", &commit1_hex, &commit2_hex)
.unwrap());
store
.namespace("ns_a")
.insert(b"k1".to_vec(), b"v1_updated".to_vec())
.unwrap();
let commit3 = store.commit("update ns_a").unwrap();
let commit3_hex = commit3.to_hex().to_string();
assert!(store
.namespace_changed("ns_a", &commit2_hex, &commit3_hex)
.unwrap());
assert!(!store
.namespace_changed("ns_b", &commit2_hex, &commit3_hex)
.unwrap());
}
#[test]
fn test_v1_to_v2_migration() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
{
use crate::git::versioned_store::GitVersionedKvStore;
let mut flat_store =
GitVersionedKvStore::<32>::init(&dataset_path).expect("Failed to init flat store");
flat_store
.insert(b"key1".to_vec(), b"value1".to_vec())
.unwrap();
flat_store
.insert(b"key2".to_vec(), b"value2".to_vec())
.unwrap();
flat_store.commit("v1 data").unwrap();
}
let mut store = GitNamespacedKvStore::<32>::open(&dataset_path).expect("Failed to open");
assert_eq!(store.format_version, StoreFormatVersion::V1);
assert_eq!(store.get(b"key1"), Some(b"value1".to_vec()));
assert_eq!(store.get(b"key2"), Some(b"value2".to_vec()));
let report = store.migrate_v1_to_v2().unwrap();
assert_eq!(report.keys_migrated, 2);
assert_eq!(report.storage_version, StoreFormatVersion::V2);
assert_eq!(store.format_version, StoreFormatVersion::V2);
assert_eq!(store.get(b"key1"), Some(b"value1".to_vec()));
assert_eq!(store.get(b"key2"), Some(b"value2".to_vec()));
store
.namespace("new_ns")
.insert(b"nk".to_vec(), b"nv".to_vec())
.unwrap();
store.commit("post-migration").unwrap();
assert!(store.list_namespaces().contains(&"new_ns".to_string()));
}
#[test]
fn test_thread_safe_namespace_operations() {
let (_temp_dir, dataset_path) = setup_git_repo();
let _cwd = CwdGuard::set(&dataset_path);
let store =
ThreadSafeGitNamespacedKvStore::<32>::init(&dataset_path).expect("Failed to init");
store
.ns_insert("users", b"u1".to_vec(), b"Alice".to_vec())
.unwrap();
assert_eq!(store.ns_get("users", b"u1"), Some(b"Alice".to_vec()));
let keys = store.ns_list_keys("users");
assert_eq!(keys.len(), 1);
let ns_list = store.list_namespaces();
assert!(ns_list.contains(&"users".to_string()));
store.insert(b"dk".to_vec(), b"dv".to_vec()).unwrap();
assert_eq!(store.get(b"dk"), Some(b"dv".to_vec()));
store.commit("thread-safe test").unwrap();
assert_eq!(store.ns_get("users", b"u1"), Some(b"Alice".to_vec()));
assert_eq!(store.get(b"dk"), Some(b"dv".to_vec()));
}
#[test]
fn test_namespace_vs_prefix_comparison() {
use crate::git::versioned_store::GitVersionedKvStore;
use std::time::Instant;
let num_namespaces = 5;
let keys_per_ns = 20;
let (_td_old, dataset_path_old) = setup_git_repo();
let _cwd_old = CwdGuard::set(&dataset_path_old);
let mut old_store =
GitVersionedKvStore::<32>::init(&dataset_path_old).expect("Failed to init old store");
for ns_idx in 0..num_namespaces {
for key_idx in 0..keys_per_ns {
let key = format!("/ns_{ns_idx}/key_{key_idx}").into_bytes();
let value = format!("value_{ns_idx}_{key_idx}").into_bytes();
old_store.insert(key, value).unwrap();
}
}
old_store.commit("old: populate").unwrap();
let (_td_new, dataset_path_new) = setup_git_repo();
drop(_cwd_old);
let _cwd_new = CwdGuard::set(&dataset_path_new);
let mut new_store =
GitNamespacedKvStore::<32>::init(&dataset_path_new).expect("Failed to init new store");
for ns_idx in 0..num_namespaces {
for key_idx in 0..keys_per_ns {
let ns_name = format!("ns_{ns_idx}");
let key = format!("key_{key_idx}").into_bytes();
let value = format!("value_{ns_idx}_{key_idx}").into_bytes();
new_store.namespace(&ns_name).insert(key, value).unwrap();
}
}
new_store.commit("new: populate").unwrap();
let start_old = Instant::now();
let old_keys = old_store.list_keys();
let mut old_namespaces: HashSet<String> = HashSet::new();
for key in &old_keys {
let key_str = String::from_utf8_lossy(key);
if let Some(idx) = key_str[1..].find('/') {
old_namespaces.insert(key_str[1..idx + 1].to_string());
}
}
let old_ns_time = start_old.elapsed();
let start_new = Instant::now();
let new_namespaces = new_store.list_namespaces();
let new_ns_time = start_new.elapsed();
assert_eq!(old_namespaces.len(), num_namespaces);
assert_eq!(new_namespaces.len(), num_namespaces + 1);
let target_ns = "ns_2";
let prefix = format!("/{target_ns}/");
let start_old = Instant::now();
let old_ns_keys: Vec<_> = old_store
.list_keys()
.into_iter()
.filter(|k| String::from_utf8_lossy(k).starts_with(&prefix))
.collect();
let old_key_time = start_old.elapsed();
let start_new = Instant::now();
let new_ns_keys = new_store.namespace(target_ns).list_keys();
let new_key_time = start_new.elapsed();
assert_eq!(old_ns_keys.len(), keys_per_ns);
assert_eq!(new_ns_keys.len(), keys_per_ns);
let commit_before_hex = {
let log = new_store.log().unwrap();
log[0].id.to_hex().to_string()
};
new_store
.namespace("ns_0")
.insert(b"new_key".to_vec(), b"new_val".to_vec())
.unwrap();
let commit_after = new_store.commit("modify ns_0").unwrap();
let commit_after_hex = commit_after.to_hex().to_string();
let start_new = Instant::now();
let ns0_changed = new_store
.namespace_changed("ns_0", &commit_before_hex, &commit_after_hex)
.unwrap();
let ns1_changed = new_store
.namespace_changed("ns_1", &commit_before_hex, &commit_after_hex)
.unwrap();
let new_change_time = start_new.elapsed();
assert!(ns0_changed, "ns_0 should have changed");
assert!(!ns1_changed, "ns_1 should NOT have changed");
println!("\n===== Namespace Approach Comparison =====");
println!(
" Namespaces: {}, Keys per NS: {}",
num_namespaces, keys_per_ns
);
println!(" Total keys: {}", num_namespaces * keys_per_ns);
println!();
println!(" Namespace listing:");
println!(" Old (prefix scan): {:?}", old_ns_time);
println!(" New (registry): {:?}", new_ns_time);
println!();
println!(" Scoped key listing for '{target_ns}':");
println!(" Old (filter all): {:?}", old_key_time);
println!(" New (subtree): {:?}", new_key_time);
println!();
println!(" Change detection:");
println!(" Old: N/A (requires full diff + filter)");
println!(" New (hash compare): {:?}", new_change_time);
println!("=========================================\n");
}
}