selene-db-graph 1.3.0

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

use crate::{GraphError, SharedGraph, VectorIndexConfig, VectorIndexKind};

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

fn vector(components: &[f32]) -> Value {
    Value::Vector(VectorValue::new(components.to_vec()).unwrap())
}

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

#[test]
fn create_vector_index_updates_working_graph_and_emits_schema_change() {
    let shared = SharedGraph::new(GraphId::new(8201));
    let label = db_string("mutator.vector.doc");
    let property = db_string("embedding");
    let name = db_string("doc_embedding_idx");
    let outcome = {
        let mut txn = shared.begin_write();
        {
            let mut mutator = txn.mutator();
            mutator
                .create_node(
                    LabelSet::single(label.clone()),
                    props([(property.clone(), vector(&[1.0, 2.0, 3.0]))]),
                )
                .unwrap();
            mutator
                .create_vector_index_named(
                    label.clone(),
                    property.clone(),
                    VectorIndexKind::Flat,
                    3,
                    Some(name.clone()),
                )
                .unwrap();
            assert_eq!(
                mutator
                    .read()
                    .vector_index_for(&label, &property)
                    .unwrap()
                    .cardinality(),
                1
            );
        }
        txn.commit().unwrap()
    };

    assert!(shared.read().vector_index_for(&label, &property).is_some());
    assert!(matches!(
        outcome.changes.as_slice(),
        [Change::NodeCreated { .. }, Change::SchemaChanged {
            graph,
            change: SchemaChange::VectorIndexCreated {
                label: changed_label,
                property: changed_property,
                kind: SchemaVectorIndexKind::Flat,
                dimension: 3,
                name: Some(changed_name),
                hnsw_config: None,
                ivf_config: None,
            },
        }] if *graph == GraphId::new(8201)
            && *changed_label == label
            && *changed_property == property
            && *changed_name == name
    ));
}

#[test]
fn create_hnsw_vector_index_emits_explicit_config_in_schema_change() {
    let shared = SharedGraph::new(GraphId::new(8206));
    let label = db_string("mutator.vector.hnsw.config");
    let property = db_string("embedding");
    let config = HnswIndexConfig::new(24, 128);
    let outcome = {
        let mut txn = shared.begin_write();
        {
            let mut mutator = txn.mutator();
            mutator
                .create_node(
                    LabelSet::single(label.clone()),
                    props([(property.clone(), vector(&[1.0, 0.0]))]),
                )
                .unwrap();
            mutator
                .create_vector_index_named_with_config(
                    label.clone(),
                    property.clone(),
                    VectorIndexKind::HnswCosine,
                    2,
                    None,
                    Some(config),
                )
                .unwrap();
            assert_eq!(
                mutator
                    .read()
                    .vector_index_for(&label, &property)
                    .unwrap()
                    .hnsw_config(),
                Some(config)
            );
        }
        txn.commit().unwrap()
    };

    assert!(matches!(
        outcome.changes.as_slice(),
        [Change::NodeCreated { .. }, Change::SchemaChanged {
            graph,
            change: SchemaChange::VectorIndexCreated {
                label: changed_label,
                property: changed_property,
                kind: SchemaVectorIndexKind::HnswCosine,
                dimension: 2,
                name: None,
                hnsw_config: Some(changed_config),
                ivf_config: None,
            },
        }] if *graph == GraphId::new(8206)
            && *changed_label == label
            && *changed_property == property
            && *changed_config == config
    ));
}

#[test]
fn create_ivf_vector_index_emits_explicit_config_in_schema_change() {
    let shared = SharedGraph::new(GraphId::new(8207));
    let label = db_string("mutator.vector.ivf.config");
    let property = db_string("embedding");
    let config = IvfIndexConfig::new(4);
    let outcome = {
        let mut txn = shared.begin_write();
        {
            let mut mutator = txn.mutator();
            for idx in 0..6 {
                mutator
                    .create_node(
                        LabelSet::single(label.clone()),
                        props([(property.clone(), vector(&[idx as f32 + 1.0, 1.0]))]),
                    )
                    .unwrap();
            }
            mutator
                .create_vector_index_named_with_configs(
                    label.clone(),
                    property.clone(),
                    VectorIndexKind::IvfCosine,
                    2,
                    None,
                    VectorIndexConfig::ivf(config),
                )
                .unwrap();
            let index = mutator.read().vector_index_for(&label, &property).unwrap();
            assert_eq!(index.ivf_config(), Some(config));
            assert_eq!(
                index.memory_usage().ivf_centroids,
                usize::from(config.target_centroids)
            );
        }
        txn.commit().unwrap()
    };

    assert!(matches!(
        outcome.changes.as_slice(),
        [Change::NodeCreated { .. }, Change::NodeCreated { .. }, Change::NodeCreated { .. },
            Change::NodeCreated { .. }, Change::NodeCreated { .. }, Change::NodeCreated { .. },
            Change::SchemaChanged {
                graph,
                change: SchemaChange::VectorIndexCreated {
                    label: changed_label,
                    property: changed_property,
                    kind: SchemaVectorIndexKind::IvfCosine,
                    dimension: 2,
                    name: None,
                    hnsw_config: None,
                    ivf_config: Some(changed_config),
                },
            }] if *graph == GraphId::new(8207)
            && *changed_label == label
            && *changed_property == property
            && *changed_config == config
    ));
}

#[test]
fn create_vector_index_rejects_duplicate_in_working_graph() {
    let shared = SharedGraph::new(GraphId::new(8202));
    let label = db_string("mutator.vector.duplicate");
    let property = db_string("embedding");
    let mut txn = shared.begin_write();
    let mut mutator = txn.mutator();
    mutator
        .create_vector_index(label.clone(), property.clone(), VectorIndexKind::Flat, 3)
        .unwrap();

    let err = mutator
        .create_vector_index(label.clone(), property.clone(), VectorIndexKind::Flat, 3)
        .unwrap_err();

    assert!(matches!(
        err,
        GraphError::VectorIndexAlreadyExists {
            label: err_label,
            property: err_property,
        } if err_label == label && err_property == property
    ));
}

#[test]
fn create_vector_index_rejects_zero_dimension() {
    let shared = SharedGraph::new(GraphId::new(8203));
    let label = db_string("mutator.vector.zero");
    let property = db_string("embedding");
    let err = shared
        .create_vector_index(label, property, VectorIndexKind::Flat, 0)
        .unwrap_err();

    assert!(matches!(
        err,
        GraphError::VectorIndexInvalidDimension { dimension: 0 }
    ));
}

#[test]
fn drop_vector_index_removes_from_working_graph_and_emits_schema_change() {
    let shared = SharedGraph::new(GraphId::new(8204));
    let label = db_string("mutator.vector.drop");
    let property = db_string("embedding");
    shared
        .create_vector_index(label.clone(), property.clone(), VectorIndexKind::Flat, 3)
        .unwrap();
    let outcome = {
        let mut txn = shared.begin_write();
        txn.mutator()
            .drop_vector_index(label.clone(), property.clone())
            .unwrap();
        txn.commit().unwrap()
    };

    assert!(shared.read().vector_index_for(&label, &property).is_none());
    assert!(matches!(
        outcome.changes.as_slice(),
        [Change::SchemaChanged {
            graph,
            change: SchemaChange::VectorIndexDropped {
                label: changed_label,
                property: changed_property,
            },
        }] if *graph == GraphId::new(8204)
            && *changed_label == label
            && *changed_property == property
    ));
}

#[test]
fn drop_vector_index_is_idempotent_and_emits_no_change_when_absent() {
    let shared = SharedGraph::new(GraphId::new(8205));
    let label = db_string("mutator.vector.absent");
    let property = db_string("embedding");
    let outcome = {
        let mut txn = shared.begin_write();
        txn.mutator().drop_vector_index(label, property).unwrap();
        txn.commit().unwrap()
    };

    assert!(outcome.changes.is_empty());
    assert_eq!(shared.read().vector_index_count(), 0);
}