fslite-sqlite 0.1.0

A transport-independent, async virtual filesystem with a SQLite-backed persistent backend, HTTP adapter, and CLI.
Documentation
use std::collections::BTreeSet;

use fslite_core::{
    CreateOptions, ErrorCode, NodeKind, PageRequest, RequestContext, TreeOptions, VirtualPath,
};
use fslite_sqlite::SqliteFileSystem;

fn path(input: &str) -> VirtualPath {
    VirtualPath::parse(input).unwrap()
}

#[tokio::test]
async fn mkdir_parents_creates_missing_ancestors() {
    let fs = SqliteFileSystem::open_in_memory(Default::default())
        .await
        .unwrap();
    let workspace = fs.create_workspace(Default::default()).await.unwrap();
    let ctx = RequestContext::trusted(workspace.id);

    let made = fs
        .mkdir(
            &ctx,
            &path("/a/b/c"),
            CreateOptions::default().parents(true),
        )
        .await
        .unwrap();
    assert_eq!(made.kind, NodeKind::Directory);
    assert_eq!(made.name, "c");

    assert_eq!(
        fs.stat(&ctx, &path("/a"), Default::default())
            .await
            .unwrap()
            .kind,
        NodeKind::Directory
    );
    assert_eq!(
        fs.stat(&ctx, &path("/a/b"), Default::default())
            .await
            .unwrap()
            .kind,
        NodeKind::Directory
    );
}

#[tokio::test]
async fn mkdir_without_parents_fails_when_ancestor_missing() {
    let fs = SqliteFileSystem::open_in_memory(Default::default())
        .await
        .unwrap();
    let workspace = fs.create_workspace(Default::default()).await.unwrap();
    let ctx = RequestContext::trusted(workspace.id);

    let error = fs
        .mkdir(&ctx, &path("/x/y"), CreateOptions::default())
        .await
        .unwrap_err();
    assert_eq!(error.code(), ErrorCode::NotFound);
}

#[tokio::test]
async fn mkdir_existing_directory_without_exist_ok_is_already_exists() {
    let fs = SqliteFileSystem::open_in_memory(Default::default())
        .await
        .unwrap();
    let workspace = fs.create_workspace(Default::default()).await.unwrap();
    let ctx = RequestContext::trusted(workspace.id);

    fs.mkdir(&ctx, &path("/dup"), Default::default())
        .await
        .unwrap();
    let error = fs
        .mkdir(&ctx, &path("/dup"), Default::default())
        .await
        .unwrap_err();
    assert_eq!(error.code(), ErrorCode::AlreadyExists);
}

#[tokio::test]
async fn mkdir_existing_directory_with_exist_ok_returns_existing_node() {
    let fs = SqliteFileSystem::open_in_memory(Default::default())
        .await
        .unwrap();
    let workspace = fs.create_workspace(Default::default()).await.unwrap();
    let ctx = RequestContext::trusted(workspace.id);

    let first = fs
        .mkdir(&ctx, &path("/dup2"), Default::default())
        .await
        .unwrap();
    let second = fs
        .mkdir(
            &ctx,
            &path("/dup2"),
            CreateOptions::default().exist_ok(true),
        )
        .await
        .unwrap();
    assert_eq!(first.id, second.id);
    assert_eq!(first.revision, second.revision);
}

#[tokio::test]
async fn read_dir_lists_children_sorted_by_name() {
    let fs = SqliteFileSystem::open_in_memory(Default::default())
        .await
        .unwrap();
    let workspace = fs.create_workspace(Default::default()).await.unwrap();
    let ctx = RequestContext::trusted(workspace.id);

    for name in ["zebra", "apple", "mango"] {
        fs.mkdir(&ctx, &path(&format!("/{name}")), Default::default())
            .await
            .unwrap();
    }

    let page = fs
        .read_dir(&ctx, &VirtualPath::root(), Default::default())
        .await
        .unwrap();
    let names: Vec<&str> = page.items.iter().map(|node| node.name.as_str()).collect();
    assert_eq!(names, vec!["apple", "mango", "zebra"]);
    assert!(page.next_cursor.is_none());
}

#[tokio::test]
async fn read_dir_cursor_continuation_visits_each_child_once() {
    let fs = SqliteFileSystem::open_in_memory(Default::default())
        .await
        .unwrap();
    let workspace = fs.create_workspace(Default::default()).await.unwrap();
    let ctx = RequestContext::trusted(workspace.id);

    let expected: BTreeSet<String> = (0..7).map(|index| format!("child-{index}")).collect();
    for name in &expected {
        fs.mkdir(&ctx, &path(&format!("/{name}")), Default::default())
            .await
            .unwrap();
    }

    let mut seen = BTreeSet::new();
    let mut cursor = None;
    loop {
        let page = fs
            .read_dir(
                &ctx,
                &VirtualPath::root(),
                PageRequest::default().limit(2).cursor(cursor.clone()),
            )
            .await
            .unwrap();
        for node in &page.items {
            assert!(seen.insert(node.name.clone()), "duplicate entry visited");
        }
        cursor = page.next_cursor;
        if cursor.is_none() {
            break;
        }
    }

    assert_eq!(seen, expected);
}

#[tokio::test]
async fn tree_respects_max_depth() {
    let fs = SqliteFileSystem::open_in_memory(Default::default())
        .await
        .unwrap();
    let workspace = fs.create_workspace(Default::default()).await.unwrap();
    let ctx = RequestContext::trusted(workspace.id);

    fs.mkdir(
        &ctx,
        &path("/a/b/c/d"),
        CreateOptions::default().parents(true),
    )
    .await
    .unwrap();

    let page = fs
        .tree(
            &ctx,
            &VirtualPath::root(),
            TreeOptions::default().max_depth(Some(2)),
            Default::default(),
        )
        .await
        .unwrap();

    let paths: Vec<String> = page
        .items
        .iter()
        .map(|entry| entry.path.as_str().to_string())
        .collect();
    assert!(paths.contains(&"/a".to_string()));
    assert!(paths.contains(&"/a/b".to_string()));
    assert!(!paths.iter().any(|p| p == "/a/b/c" || p == "/a/b/c/d"));
    assert!(page.items.iter().all(|entry| entry.depth <= 2));
}

#[tokio::test]
async fn identical_paths_in_two_workspaces_do_not_leak() {
    let fs = SqliteFileSystem::open_in_memory(Default::default())
        .await
        .unwrap();
    let first = fs.create_workspace(Default::default()).await.unwrap();
    let second = fs.create_workspace(Default::default()).await.unwrap();
    let first_ctx = RequestContext::trusted(first.id);
    let second_ctx = RequestContext::trusted(second.id);

    fs.mkdir(&first_ctx, &path("/shared"), Default::default())
        .await
        .unwrap();
    fs.mkdir(&second_ctx, &path("/shared"), Default::default())
        .await
        .unwrap();

    let first_listing = fs
        .read_dir(&first_ctx, &VirtualPath::root(), Default::default())
        .await
        .unwrap();
    let second_listing = fs
        .read_dir(&second_ctx, &VirtualPath::root(), Default::default())
        .await
        .unwrap();

    assert_eq!(first_listing.items.len(), 1);
    assert_eq!(second_listing.items.len(), 1);
    assert_ne!(first_listing.items[0].id, second_listing.items[0].id);
}