tabularium 0.1.6

Markdown-oriented document store library (SQLite + Tantivy)
Documentation
//! Deep hierarchical paths — proof for the forge (Ferrum § test audit).

use super::{EntryId, EntryKind, SqliteDatabase};
use crate::Error;

fn temp_db() -> (tempfile::TempDir, String, std::path::PathBuf) {
    let dir = tempfile::tempdir().unwrap();
    let db_path = dir.path().join("deep.db");
    let idx_path = dir.path().join("deep.idx");
    let uri = format!("sqlite://{}", db_path.display());
    (dir, uri, idx_path)
}

#[tokio::test]
async fn deep_mkdir_chain_resolve_and_missing_parent() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    let id_a = db.create_directory("/a", None, false).await.unwrap();
    let id_ab = db.create_directory("/a/b", None, false).await.unwrap();
    let id_abc = db.create_directory("/a/b/c", None, false).await.unwrap();
    assert_ne!(id_a, id_ab);
    assert_ne!(id_ab, id_abc);
    assert_eq!(db.resolve_directory_path("/a/b/c").await.unwrap(), id_abc);
    let err = db
        .create_directory("/x/y/z", None, false)
        .await
        .unwrap_err();
    assert!(matches!(err, Error::NotFound(_)));
}

#[tokio::test]
async fn mkdir_parents_creates_missing_ancestors() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    let id = db.create_directory("/u/v/w", None, true).await.unwrap();
    assert_eq!(db.resolve_directory_path("/u/v/w").await.unwrap(), id);
}

#[tokio::test]
async fn mkdir_parents_idempotent_existing_leaf() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    let a = db.create_directory("/same/deep", None, true).await.unwrap();
    let b = db.create_directory("/same/deep", None, true).await.unwrap();
    assert_eq!(a, b);
}

#[tokio::test]
async fn mkdir_parents_rejects_file_segment() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    db.create_document_at_path("/notdir", "x").await.unwrap();
    let err = db
        .create_directory("/notdir/inside", None, true)
        .await
        .unwrap_err();
    assert!(matches!(err, Error::InvalidInput(_)));
}

#[tokio::test]
async fn five_level_canonical_paths_and_root() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    db.create_directory("/l1", None, false).await.unwrap();
    db.create_directory("/l1/l2", None, false).await.unwrap();
    db.create_directory("/l1/l2/l3", None, false).await.unwrap();
    db.create_directory("/l1/l2/l3/l4", None, false)
        .await
        .unwrap();
    let id_e = db
        .create_directory("/l1/l2/l3/l4/l5", None, false)
        .await
        .unwrap();
    assert_eq!(
        db.test_storage_canonical_path(id_e).await.unwrap(),
        "/l1/l2/l3/l4/l5"
    );
    let root = EntryId::from_raw(1);
    assert_eq!(db.test_storage_canonical_path(root).await.unwrap(), "/");
}

#[tokio::test]
async fn root_file_listing_and_resolve() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    let id = db
        .create_document_at_path("/root_doc", "body-root")
        .await
        .unwrap();
    assert_eq!(db.get_document(id).await.unwrap(), "body-root");
    let listed = db.list_directory("/").await.unwrap();
    let names: Vec<_> = listed.iter().map(super::ListedEntry::name).collect();
    assert!(names.contains(&"root_doc"));
    assert_eq!(db.resolve_file_path("/root_doc").await.unwrap(), id);
}

#[tokio::test]
async fn ensure_directory_path_idempotent_and_deep_file_crud() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    let leaf = db
        .test_storage_ensure_directory_path("/p/q/r/s")
        .await
        .unwrap();
    let again = db
        .test_storage_ensure_directory_path("/p/q/r/s")
        .await
        .unwrap();
    assert_eq!(leaf, again);
    assert_eq!(
        db.test_storage_resolve_path("/p/q/r/s", Some(EntryKind::Dir))
            .await
            .unwrap(),
        leaf
    );
    let fid = db
        .create_file_in_directory("/p/q/r/s", "note", "v1")
        .await
        .unwrap();
    assert_eq!(db.get_document(fid).await.unwrap(), "v1");
    db.update_document(fid, "v2").await.unwrap();
    assert_eq!(db.get_document(fid).await.unwrap(), "v2");
    db.append_document(fid, "more").await.unwrap();
    assert_eq!(db.get_document(fid).await.unwrap(), "v2\nmore");
    db.delete_document(fid).await.unwrap();
    assert!(
        db.test_storage_resolve_path("/p/q/r/s/note", Some(EntryKind::File))
            .await
            .is_err()
    );
}

#[tokio::test]
async fn list_directory_mixed_kinds_at_depths() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    db.create_directory("/a", None, false).await.unwrap();
    db.create_directory("/a/b", None, false).await.unwrap();
    db.create_directory("/a/b/c", None, false).await.unwrap();
    db.create_directory("/a/d", None, false).await.unwrap();
    db.create_file_in_directory("/a", "doc.txt", "x")
        .await
        .unwrap();
    let at_a = db.list_directory("/a").await.unwrap();
    let mut names: Vec<_> = at_a.iter().map(|e| (e.name(), e.kind())).collect();
    names.sort_by(|x, y| x.0.cmp(y.0));
    assert_eq!(names.len(), 3);
    assert!(names.iter().any(|(n, k)| *n == "b" && *k == EntryKind::Dir));
    assert!(names.iter().any(|(n, k)| *n == "d" && *k == EntryKind::Dir));
    assert!(
        names
            .iter()
            .any(|(n, k)| *n == "doc.txt" && *k == EntryKind::File)
    );
    let at_ab = db.list_directory("/a/b").await.unwrap();
    assert_eq!(at_ab.len(), 1);
    assert_eq!(at_ab[0].name(), "c");
    assert_eq!(at_ab[0].kind(), EntryKind::Dir);
}

#[tokio::test]
async fn move_file_between_directories_updates_search() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    db.create_directory("/src_dir", None, false).await.unwrap();
    db.create_directory("/dst_dir", None, false).await.unwrap();
    let fid = db
        .create_file_in_directory("/src_dir", "doc", "moveneedle_unique")
        .await
        .unwrap();
    let h1 = db
        .search_hits("moveneedle_unique", Some("/src_dir"), 10)
        .await
        .unwrap();
    assert_eq!(h1.len(), 1);
    db.move_document_to_directory(fid, "/dst_dir", "doc")
        .await
        .unwrap();
    assert!(
        db.search_hits("moveneedle_unique", Some("/src_dir"), 10)
            .await
            .unwrap()
            .is_empty()
    );
    let h2 = db
        .search_hits("moveneedle_unique", Some("/dst_dir"), 10)
        .await
        .unwrap();
    assert_eq!(h2.len(), 1);
    assert_eq!(h2[0].path(), "/dst_dir/doc");
}

#[tokio::test]
async fn move_directory_valid_and_rejects_cycle() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    db.create_directory("/tree", None, false).await.unwrap();
    db.create_directory("/tree/a", None, false).await.unwrap();
    db.create_directory("/tree/a/b", None, false).await.unwrap();
    db.create_directory("/tree/other", None, false)
        .await
        .unwrap();
    db.move_directory("/tree/a/b", "/tree/other", "nested")
        .await
        .unwrap();
    db.resolve_directory_path("/tree/other/nested")
        .await
        .unwrap();

    db.create_directory("/cyc", None, false).await.unwrap();
    db.create_directory("/cyc/a", None, false).await.unwrap();
    db.create_directory("/cyc/a/b", None, false).await.unwrap();
    let err = db
        .move_directory("/cyc/a", "/cyc/a/b", "bad")
        .await
        .unwrap_err();
    assert!(matches!(err, Error::InvalidInput(_)));
}

#[tokio::test]
async fn recursive_delete_clears_storage_and_search() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    db.create_directory("/deep", None, false).await.unwrap();
    db.create_directory("/deep/x", None, false).await.unwrap();
    db.create_directory("/deep/x/y", None, false).await.unwrap();
    db.create_file_in_directory("/deep/x/y", "f1", "deepf1needle")
        .await
        .unwrap();
    db.create_file_in_directory("/deep/x/y", "f2", "deepf2needle")
        .await
        .unwrap();
    assert_eq!(
        db.search_hits("deepf1needle", None, 10)
            .await
            .unwrap()
            .len(),
        1
    );
    db.delete_directory_recursive("/deep").await.unwrap();
    assert!(
        db.search_hits("deepf1needle", None, 10)
            .await
            .unwrap()
            .is_empty()
    );
    assert!(db.resolve_directory_path("/deep").await.is_err());
}

#[tokio::test]
async fn rename_directory_and_file_at_depth() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    db.create_directory("/d", None, false).await.unwrap();
    db.create_directory("/d/old", None, false).await.unwrap();
    let fid = db
        .create_file_in_directory("/d/old", "f", "txt")
        .await
        .unwrap();
    db.rename_directory("/d/old", "/d/new").await.unwrap();
    db.resolve_directory_path("/d/new").await.unwrap();
    assert!(db.resolve_directory_path("/d/old").await.is_err());
    assert_eq!(db.resolve_file_path("/d/new/f").await.unwrap(), fid);
    db.rename_document(fid, "g").await.unwrap();
    assert!(db.resolve_file_path("/d/new/f").await.is_err());
    db.resolve_file_path("/d/new/g").await.unwrap();
}

#[tokio::test]
async fn search_subtree_scoped_and_segment_boundary() {
    let (_d, uri, idx) = temp_db();
    let db = SqliteDatabase::init(&uri, &idx, 0).await.unwrap();
    db.create_directory("/ns", None, false).await.unwrap();
    db.create_directory("/ns/a", None, false).await.unwrap();
    db.create_directory("/ns/b", None, false).await.unwrap();
    db.create_file_in_directory("/ns/a", "doc1", "unique_deep_a_alpha")
        .await
        .unwrap();
    db.create_file_in_directory("/ns/b", "doc2", "unique_deep_b_beta")
        .await
        .unwrap();
    let a_only = db
        .search_hits("unique_deep_a", Some("/ns/a"), 10)
        .await
        .unwrap();
    assert_eq!(a_only.len(), 1);
    assert_eq!(a_only[0].path(), "/ns/a/doc1");
    let ns_b = db
        .search_hits("unique_deep_b", Some("/ns"), 10)
        .await
        .unwrap();
    assert_eq!(ns_b.len(), 1);
    assert!(
        db.search_hits("unique_deep_a", Some("/ns/b"), 10)
            .await
            .unwrap()
            .is_empty()
    );
    let both = db.search_hits("unique_deep", None, 10).await.unwrap();
    assert_eq!(both.len(), 2);
    db.create_directory("/segtrap", None, false).await.unwrap();
    db.create_directory("/segtrap/a", None, false)
        .await
        .unwrap();
    db.create_directory("/segtrap/a/b", None, false)
        .await
        .unwrap();
    db.create_directory("/segtrap/a/bc", None, false)
        .await
        .unwrap();
    db.create_file_in_directory("/segtrap/a/b", "x", "trapxyz123seg")
        .await
        .unwrap();
    db.create_file_in_directory("/segtrap/a/bc", "y", "trapxyz123seg")
        .await
        .unwrap();
    let scoped = db
        .search_hits("trapxyz123seg", Some("/segtrap/a/b"), 10)
        .await
        .unwrap();
    assert_eq!(scoped.len(), 1);
    assert_eq!(scoped[0].path(), "/segtrap/a/b/x");
}