grust-lancedb 0.5.0

LanceDB GraphStore backend for Grust.
Documentation
use grust_core::prelude::*;
use tempfile::tempdir;

use super::*;

async fn store() -> LanceDbGraphStore {
    let dir = tempdir().expect("tempdir");
    let uri = dir.keep().display().to_string();
    let store = LanceDbGraphStore::connect(LanceDbConfig {
        uri,
        table_prefix: "test_graph".to_string(),
        batch_size: 2,
    })
    .await
    .expect("connect");
    store.bootstrap().await.expect("bootstrap");
    store.clear().await.expect("clear");
    store
}

fn sample_graph() -> Graph {
    let mut builder = Graph::builder();
    builder
        .node("Person", "person-1")
        .prop("name", "Ada")
        .prop("age", 36i64)
        .finish();
    builder
        .node("Talk", "talk-1")
        .prop("title", "Analytical Engine")
        .finish();
    builder.node("Room", "room-1").prop("name", "Main").finish();
    builder
        .edge("PRESENTS", "person-1", "talk-1")
        .prop("source", "schedule")
        .finish();
    builder.edge("HOSTED_IN", "talk-1", "room-1").finish();
    builder.build()
}

#[tokio::test]
async fn bootstrap_creates_empty_tables() {
    let store = store().await;

    let nodes = store.open_nodes().await.expect("nodes table");
    let edges = store.open_edges().await.expect("edges table");

    assert_eq!(nodes.count_rows(None).await.expect("node count"), 0);
    assert_eq!(edges.count_rows(None).await.expect("edge count"), 0);
}

#[tokio::test]
async fn apply_schema_creates_typed_tables_and_mirrors_writes() {
    let store = store().await;
    let schema = GraphSchema::builder()
        .node(
            "Person",
            vec![
                grust_core::Field::required("name", FieldType::String),
                grust_core::Field::optional("age", FieldType::Int),
            ],
        )
        .node(
            "Talk",
            vec![grust_core::Field::required("title", FieldType::String)],
        )
        .node(
            "Room",
            vec![grust_core::Field::required("name", FieldType::String)],
        )
        .edge(
            "PRESENTS",
            vec![Label::new("Person")],
            vec![Label::new("Talk")],
            vec![grust_core::Field::required("source", FieldType::String)],
        )
        .edge(
            "HOSTED_IN",
            vec![Label::new("Talk")],
            vec![Label::new("Room")],
            Vec::<grust_core::Field>::new(),
        )
        .build();

    store.apply_schema(&schema).await.expect("apply_schema");
    store.put_graph(&sample_graph()).await.expect("put_graph");

    let person_table = store
        .open_table(&store.typed_node_table_name("Person").unwrap())
        .await
        .expect("typed person table");
    let edge_table = store
        .open_table(&store.typed_edge_table_name("PRESENTS").unwrap())
        .await
        .expect("typed presents table");

    assert_eq!(person_table.count_rows(None).await.expect("person rows"), 1);
    assert_eq!(edge_table.count_rows(None).await.expect("edge rows"), 1);
}

#[tokio::test]
async fn applied_schema_rejects_wrong_typed_property() {
    let store = store().await;
    let schema = GraphSchema::builder()
        .node(
            "Person",
            vec![grust_core::Field::required("age", FieldType::Int)],
        )
        .build();
    store.apply_schema(&schema).await.expect("apply_schema");

    let error = store
        .put_node(&Node::new("Person", "person-1", {
            let mut props = Props::new();
            props.insert("age".to_string(), Value::from("old"));
            props
        }))
        .await
        .expect_err("wrong field type should fail");

    assert!(error.to_string().contains("field 'age' expected Int"));
}

#[tokio::test]
async fn put_and_get_node() {
    let store = store().await;
    let node = Node::new("Person", "person-1", {
        let mut props = Props::new();
        props.insert("name".to_string(), Value::from("Ada"));
        props
    });

    let id = store.put_node(&node).await.expect("put_node");
    assert_eq!(id.as_str(), "person-1");

    let fetched = store
        .get_node(&NodeId::new("person-1"))
        .await
        .expect("get_node")
        .expect("node exists");
    assert_eq!(fetched.label.as_str(), "Person");
    assert_eq!(
        fetched.props.get("name").and_then(Value::as_str),
        Some("Ada")
    );
}

#[tokio::test]
async fn idempotent_put_node_updates_props() {
    let store = store().await;
    store
        .put_node(&Node::new("Person", "person-1", {
            let mut props = Props::new();
            props.insert("name".to_string(), Value::from("Ada v1"));
            props
        }))
        .await
        .expect("first put");

    store
        .put_node(&Node::new("Person", "person-1", {
            let mut props = Props::new();
            props.insert("name".to_string(), Value::from("Ada v2"));
            props
        }))
        .await
        .expect("second put");

    let fetched = store
        .get_node(&NodeId::new("person-1"))
        .await
        .expect("get_node")
        .expect("node exists");
    assert_eq!(
        fetched.props.get("name").and_then(Value::as_str),
        Some("Ada v2")
    );
}

#[tokio::test]
async fn put_graph_and_get_edges() {
    let store = store().await;
    let graph = sample_graph();
    let report = store.put_graph(&graph).await.expect("put_graph");
    assert_eq!(report.nodes, 3);
    assert_eq!(report.edges, 2);

    let edges = store
        .get_edges(EdgeQuery {
            from: Some(NodeId::new("person-1")),
            label: Some(Label::new("PRESENTS")),
            ..Default::default()
        })
        .await
        .expect("get_edges");

    assert_eq!(edges.len(), 1);
    assert_eq!(edges[0].to.as_str(), "talk-1");
    assert_eq!(edges[0].label.as_str(), "PRESENTS");
}

#[tokio::test]
async fn traverse_one_and_two_hops() {
    let store = store().await;
    store.put_graph(&sample_graph()).await.expect("put_graph");

    let talks = store
        .traverse(Traversal::from_node("person-1").out("PRESENTS").to("Talk"))
        .await
        .expect("one hop");
    assert_eq!(talks.len(), 1);
    assert_eq!(talks[0].id.as_str(), "talk-1");

    let rooms = store
        .traverse(
            Traversal::from_node("person-1")
                .out("PRESENTS")
                .to("Talk")
                .out("HOSTED_IN")
                .to("Room"),
        )
        .await
        .expect("two hops");
    assert_eq!(rooms.len(), 1);
    assert_eq!(rooms[0].id.as_str(), "room-1");
}

#[tokio::test]
async fn starts_by_label_and_property() {
    let store = store().await;
    store.put_graph(&sample_graph()).await.expect("put_graph");

    let people = store
        .traverse(Traversal {
            start: Start::NodesByLabel(Label::new("Person")),
            steps: Vec::new(),
            limit: None,
        })
        .await
        .expect("label start");
    assert_eq!(people.len(), 1);

    let ada = store
        .traverse(Traversal {
            start: Start::NodesByProperty {
                label: Label::new("Person"),
                key: "name".to_string(),
                value: Value::from("Ada"),
            },
            steps: Vec::new(),
            limit: None,
        })
        .await
        .expect("property start");
    assert_eq!(ada.len(), 1);
    assert_eq!(ada[0].id.as_str(), "person-1");
}

#[tokio::test]
async fn clear_removes_graph() {
    let store = store().await;
    store.put_graph(&sample_graph()).await.expect("put_graph");
    store.clear().await.expect("clear");

    let missing = store
        .get_node(&NodeId::new("person-1"))
        .await
        .expect("get_node");
    assert!(missing.is_none());
}

#[test]
fn edge_key_prefers_explicit_id() {
    let edge = Edge::new("KNOWS", "a", "b", Props::new()).with_id("edge-1");
    assert_eq!(edge_key(&edge), "edge-1");

    let edge = Edge::new("KNOWS", "a", "b", Props::new());
    assert_eq!(edge_key(&edge), "a\u{1f}KNOWS\u{1f}b");
}