fathomdb 0.5.3

Local datastore for persistent AI agents with graph, vector, and full-text search on SQLite
Documentation
#![allow(clippy::expect_used, clippy::missing_panics_doc)]

use fathomdb::{ChunkInsert, ChunkPolicy, Engine, EngineOptions, NodeInsert, WriteRequest};
use tempfile::NamedTempFile;

fn open_engine() -> (NamedTempFile, Engine) {
    let db = NamedTempFile::new().expect("temporary db");
    let engine = Engine::open(EngineOptions::new(db.path())).expect("engine opens");
    (db, engine)
}

fn seed_tasks(engine: &Engine) {
    engine
        .register_fts_property_schema(
            "Task",
            &["$.title".to_owned(), "$.resolved".to_owned()],
            None,
        )
        .expect("register property schema");

    engine
        .writer()
        .submit(WriteRequest {
            label: "seed-tasks".to_owned(),
            nodes: vec![
                NodeInsert {
                    row_id: "task-open-row".to_owned(),
                    logical_id: "task-open".to_owned(),
                    kind: "Task".to_owned(),
                    properties: r#"{"title":"implement login","resolved":false}"#.to_owned(),
                    source_ref: Some("seed-tasks".to_owned()),
                    upsert: false,
                    chunk_policy: ChunkPolicy::Preserve,
                    content_ref: None,
                },
                NodeInsert {
                    row_id: "task-done-row".to_owned(),
                    logical_id: "task-done".to_owned(),
                    kind: "Task".to_owned(),
                    properties: r#"{"title":"implement logout","resolved":true}"#.to_owned(),
                    source_ref: Some("seed-tasks".to_owned()),
                    upsert: false,
                    chunk_policy: ChunkPolicy::Preserve,
                    content_ref: None,
                },
                NodeInsert {
                    row_id: "task-open2-row".to_owned(),
                    logical_id: "task-open2".to_owned(),
                    kind: "Task".to_owned(),
                    properties: r#"{"title":"implement dashboard","resolved":false}"#.to_owned(),
                    source_ref: Some("seed-tasks".to_owned()),
                    upsert: false,
                    chunk_policy: ChunkPolicy::Preserve,
                    content_ref: None,
                },
            ],
            node_retires: vec![],
            edges: vec![],
            edge_retires: vec![],
            chunks: vec![
                ChunkInsert {
                    id: "task-open-chunk".to_owned(),
                    node_logical_id: "task-open".to_owned(),
                    text_content: "implement the login feature with OAuth2 support".to_owned(),
                    byte_start: None,
                    byte_end: None,
                    content_hash: None,
                },
                ChunkInsert {
                    id: "task-done-chunk".to_owned(),
                    node_logical_id: "task-done".to_owned(),
                    text_content: "implement the logout feature with session clearing".to_owned(),
                    byte_start: None,
                    byte_end: None,
                    content_hash: None,
                },
                ChunkInsert {
                    id: "task-open2-chunk".to_owned(),
                    node_logical_id: "task-open2".to_owned(),
                    text_content: "implement the dashboard with charts and filters".to_owned(),
                    byte_start: None,
                    byte_end: None,
                    content_hash: None,
                },
            ],
            runs: vec![],
            steps: vec![],
            actions: vec![],
            optional_backfills: vec![],
            vec_inserts: vec![],
            operational_writes: vec![],
        })
        .expect("seed tasks");
}

#[test]
fn fused_bool_eq_false_filters_to_unresolved_tasks_only() {
    let (_db, engine) = open_engine();
    seed_tasks(&engine);

    let rows = engine
        .query("Task")
        .text_search("implement", 10)
        .filter_json_fused_bool_eq("$.resolved", false)
        .expect("filter_json_fused_bool_eq succeeds")
        .execute()
        .expect("search executes");

    let ids: Vec<&str> = rows
        .hits
        .iter()
        .map(|h| h.node.logical_id.as_str())
        .collect();
    assert_eq!(
        rows.hits.len(),
        2,
        "expected 2 unresolved tasks, got {ids:?}"
    );
    for hit in &rows.hits {
        assert_ne!(
            hit.node.logical_id, "task-done",
            "resolved task must not appear in unresolved filter"
        );
    }
}

#[test]
fn fused_bool_eq_true_filters_to_resolved_tasks_only() {
    let (_db, engine) = open_engine();
    seed_tasks(&engine);

    let rows = engine
        .query("Task")
        .text_search("implement", 10)
        .filter_json_fused_bool_eq("$.resolved", true)
        .expect("filter_json_fused_bool_eq succeeds")
        .execute()
        .expect("search executes");

    let ids: Vec<&str> = rows
        .hits
        .iter()
        .map(|h| h.node.logical_id.as_str())
        .collect();
    assert_eq!(rows.hits.len(), 1, "expected 1 resolved task, got {ids:?}");
    assert_eq!(rows.hits[0].node.logical_id, "task-done");
}

#[test]
fn fused_bool_eq_missing_schema_raises_validation_error() {
    let (_db, engine) = open_engine();

    let result = engine
        .query("UnregisteredKind")
        .text_search("anything", 10)
        .filter_json_fused_bool_eq("$.resolved", false);

    assert!(
        result.is_err(),
        "expected BuilderValidationError for missing schema"
    );
}