selene-db-graph 1.2.0

In-memory property-graph storage core (ArcSwap + imbl CoW, label/typed indexes, write funnel) for selene-db.
Documentation
use selene_core::{
    DbString, GraphId, LabelDiff, LabelSet, NodeId, PropertyDiff, PropertyMap, Value,
};

use crate::{GraphError, SharedGraph};

fn db_string(value: &str) -> DbString {
    selene_core::db_string(value).unwrap()
}

fn props(pairs: impl IntoIterator<Item = (DbString, Value)>) -> PropertyMap {
    PropertyMap::from_pairs(pairs).unwrap()
}

fn hit_ids(graph: &SharedGraph, label: &DbString, property: &DbString, query: &str) -> Vec<NodeId> {
    graph
        .read()
        .text_index_for(label, property)
        .unwrap()
        .search(query, 10)
        .into_iter()
        .map(|hit| hit.node_id)
        .collect()
}

#[test]
fn text_index_tracks_create_update_and_delete_documents() {
    let shared = SharedGraph::new(GraphId::new(9101));
    let label = db_string("text.index.doc");
    let property = db_string("body");
    let other = db_string("text.index.other");
    let (doc_a, doc_b) = {
        let mut txn = shared.begin_write();
        let mut mutator = txn.mutator();
        let doc_a = mutator
            .create_node(
                LabelSet::single(label.clone()),
                props([(property.clone(), Value::String(db_string("alpha beta")))]),
            )
            .unwrap();
        let doc_b = mutator
            .create_node(
                LabelSet::single(label.clone()),
                props([(other, Value::Int(9))]),
            )
            .unwrap();
        txn.commit().unwrap();
        (doc_a, doc_b)
    };

    shared
        .create_text_index(label.clone(), property.clone())
        .unwrap();
    assert_eq!(hit_ids(&shared, &label, &property, "alpha"), vec![doc_a]);

    {
        let mut txn = shared.begin_write();
        txn.mutator()
            .update_node(
                doc_b,
                LabelDiff::new([], []).unwrap(),
                PropertyDiff::new(
                    [(property.clone(), Value::String(db_string("gamma alpha")))],
                    [],
                )
                .unwrap(),
            )
            .unwrap();
        txn.commit().unwrap();
    }
    assert_eq!(hit_ids(&shared, &label, &property, "gamma"), vec![doc_b]);

    {
        let mut txn = shared.begin_write();
        txn.mutator()
            .update_node(
                doc_a,
                LabelDiff::new([], []).unwrap(),
                PropertyDiff::new([(property.clone(), Value::String(db_string("delta")))], [])
                    .unwrap(),
            )
            .unwrap();
        txn.commit().unwrap();
    }
    assert_eq!(
        hit_ids(&shared, &label, &property, "beta"),
        Vec::<NodeId>::new()
    );

    {
        let mut txn = shared.begin_write();
        txn.mutator().delete_node(doc_b).unwrap();
        txn.commit().unwrap();
    }
    assert_eq!(
        shared
            .read()
            .text_index_for(&label, &property)
            .unwrap()
            .stats()
            .documents,
        1
    );
    assert_eq!(
        hit_ids(&shared, &label, &property, "gamma"),
        Vec::<NodeId>::new()
    );
}

#[test]
fn create_text_index_rejects_duplicate_and_drop_is_idempotent() {
    let shared = SharedGraph::new(GraphId::new(9102));
    let label = db_string("text.index.duplicate");
    let property = db_string("body");

    shared
        .create_text_index_named(label.clone(), property.clone(), Some(db_string("body_idx")))
        .unwrap();
    let err = shared
        .create_text_index(label.clone(), property.clone())
        .unwrap_err();
    assert!(matches!(
        err,
        GraphError::TextIndexAlreadyExists {
            label: err_label,
            property: err_property,
        } if err_label == label && err_property == property
    ));
    assert_eq!(shared.read().text_index_count(), 1);
    assert_eq!(
        shared
            .read()
            .iter_text_index_entries()
            .next()
            .and_then(|(_, _, _, _, name)| name),
        Some(db_string("body_idx"))
    );

    shared
        .drop_text_index(label.clone(), property.clone())
        .unwrap();
    shared.drop_text_index(label, property).unwrap();
    assert_eq!(shared.read().text_index_count(), 0);
}

#[test]
fn text_index_ignores_non_string_values_and_tracks_label_membership() {
    let shared = SharedGraph::new(GraphId::new(9103));
    let label = db_string("text.index.string.only");
    let other_label = db_string("text.index.other.label");
    let property = db_string("body");
    let doc = {
        let mut txn = shared.begin_write();
        let doc = txn
            .mutator()
            .create_node(
                LabelSet::single(other_label.clone()),
                props([(property.clone(), Value::Int(42))]),
            )
            .unwrap();
        txn.commit().unwrap();
        doc
    };

    shared
        .create_text_index(label.clone(), property.clone())
        .unwrap();
    assert_eq!(
        shared
            .read()
            .text_index_for(&label, &property)
            .unwrap()
            .stats()
            .documents,
        0
    );

    {
        let mut txn = shared.begin_write();
        txn.mutator()
            .update_node(
                doc,
                LabelDiff::new([label.clone()], [other_label]).unwrap(),
                PropertyDiff::new(
                    [(property.clone(), Value::String(db_string("needle hay")))],
                    [],
                )
                .unwrap(),
            )
            .unwrap();
        txn.commit().unwrap();
    }
    assert_eq!(hit_ids(&shared, &label, &property, "needle"), vec![doc]);

    {
        let mut txn = shared.begin_write();
        txn.mutator()
            .update_node(
                doc,
                LabelDiff::new([], []).unwrap(),
                PropertyDiff::new([(property.clone(), Value::Int(7))], []).unwrap(),
            )
            .unwrap();
        txn.commit().unwrap();
    }
    assert_eq!(
        shared
            .read()
            .text_index_for(&label, &property)
            .unwrap()
            .stats()
            .documents,
        0
    );
}