use crate::api::Graph;
use crate::gremlin::{traversal::TraversalBuilder, value::Value};
use crate::schema::definition::{DataType, EdgeMode, GraphOptions, SchemaMode};
use crate::types::StoreError;
use smol_str::SmolStr;
use tempfile::tempdir;
#[test]
fn test_management_explicit_declaration_and_cas() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
{
let schema = graph.schema();
assert_eq!(schema.read().unwrap().version, 0);
}
{
let mut mgmt = graph.open_management();
mgmt.add_vertex_label("person").add_edge_label("knows").add_property_key("age", DataType::Int32);
mgmt.commit().unwrap();
}
{
let schema = graph.schema();
let s = schema.read().unwrap();
assert_eq!(s.version, 1);
assert!(s.vertex_label_id("person").is_some());
assert!(s.edge_label_id("knows").is_some());
assert!(s.prop_key_id("age").is_some());
}
drop(graph);
let graph_reopened = Graph::open(dir.path()).unwrap();
{
let schema = graph_reopened.schema();
let s = schema.read().unwrap();
assert_eq!(s.version, 1);
assert!(s.vertex_label_id("person").is_some());
assert!(s.edge_label_id("knows").is_some());
assert!(s.prop_key_id("age").is_some());
}
let mut mgmt1 = graph_reopened.open_management();
let mut mgmt2 = graph_reopened.open_management();
mgmt1.add_vertex_label("software");
mgmt1.commit().unwrap();
mgmt2.add_vertex_label("project");
let err = mgmt2.commit().unwrap_err();
assert!(matches!(err, StoreError::SchemaConflict(_)));
{
let mut mgmt = graph_reopened.open_management();
mgmt.set_edge_mode(EdgeMode::Multi);
mgmt.commit().unwrap();
}
{
let schema = graph_reopened.schema();
assert_eq!(schema.read().unwrap().edge_mode, EdgeMode::Multi);
}
{
let mut mgmt = graph_reopened.open_management();
mgmt.set_edge_mode(EdgeMode::Single);
let err = mgmt.commit().unwrap_err();
assert!(matches!(err, StoreError::SchemaConflict(_)));
}
}
#[test]
fn test_schema_mode_auto_implicit_writes_and_types() {
let dir = tempdir().unwrap();
let graph =
Graph::open_with_options(dir.path(), GraphOptions { mode: SchemaMode::Auto, edge_mode: EdgeMode::Single })
.unwrap();
{
let mut tx = graph.begin();
tx.g().addV("person").property("id", 1i64).property("name", "Alice").next().unwrap();
tx.commit().unwrap();
}
{
let schema = graph.schema();
let s = schema.read().unwrap();
assert!(s.vertex_label_id("person").is_some());
assert!(s.prop_key_id("name").is_some());
assert_eq!(s.prop_key_types.get(&s.prop_key_id("name").unwrap()).unwrap().data_type, DataType::String);
}
{
let mut tx = graph.begin();
let err = tx.g().addV("person").property("id", 2i64).property("name", 123i32).next().unwrap_err();
assert!(matches!(err, StoreError::SchemaViolation(_)));
}
{
let mut tx = graph.begin();
tx.g().addV("animal").property("id", 3i64).property("species", "cat").next().unwrap();
tx.rollback(); }
drop(graph);
let graph_reopened = Graph::open(dir.path()).unwrap();
{
let schema = graph_reopened.schema();
let s = schema.read().unwrap();
assert!(s.vertex_label_id("animal").is_none());
assert!(s.prop_key_id("species").is_none());
}
}
#[test]
fn test_management_commit_atomic_on_partial_failure() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
{
let mut mgmt = graph.open_management();
mgmt.add_property_key("age", DataType::Int32);
mgmt.commit().unwrap();
}
assert_eq!(graph.schema().read().unwrap().version, 1);
{
let mut mgmt = graph.open_management();
mgmt.add_vertex_label("ghost");
mgmt.add_property_key("age", DataType::Int64); let err = mgmt.commit().unwrap_err();
assert!(matches!(err, StoreError::SchemaConflict(_)));
}
let schema = graph.schema();
let s = schema.read().unwrap();
assert_eq!(s.version, 1, "version must not change on a failed commit");
assert!(s.vertex_label_id("ghost").is_none(), "ghost must not leak into the live schema from a failed batch");
}
#[test]
fn test_management_commit_noop_does_not_bump_version() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
{
let mgmt = graph.open_management();
mgmt.commit().unwrap();
}
assert_eq!(graph.schema().read().unwrap().version, 0);
{
let mut mgmt = graph.open_management();
mgmt.add_property_key("age", DataType::Int32);
mgmt.commit().unwrap();
}
assert_eq!(graph.schema().read().unwrap().version, 1);
{
let mut mgmt = graph.open_management();
mgmt.add_property_key("age", DataType::Int32);
mgmt.commit().unwrap();
}
assert_eq!(graph.schema().read().unwrap().version, 1, "idempotent redeclare must not bump version");
}
#[test]
fn test_auto_mode_version_bumps_once_per_new_label() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
assert_eq!(graph.schema().read().unwrap().version, 0);
{
let mut tx = graph.begin();
tx.g().addV("person").property("id", 1i64).next().unwrap();
tx.commit().unwrap();
}
assert_eq!(graph.schema().read().unwrap().version, 1);
}
#[test]
fn test_schema_mode_strict_rejections() {
let dir = tempdir().unwrap();
let graph =
Graph::open_with_options(dir.path(), GraphOptions { mode: SchemaMode::Strict, edge_mode: EdgeMode::Single })
.unwrap();
{
let mut tx = graph.begin();
let err = tx.g().addV("person").property("id", 1i64).next().unwrap_err();
assert!(matches!(err, StoreError::SchemaViolation(_)));
}
{
let mut mgmt = graph.open_management();
mgmt.add_vertex_label("person").add_edge_label("knows").add_property_key("name", DataType::String);
mgmt.commit().unwrap();
}
{
let mut tx = graph.begin();
tx.g().addV("person").property("id", 1i64).property("name", "Alice").next().unwrap();
tx.commit().unwrap();
}
{
let mut tx = graph.begin();
let err = tx.g().addV("person").property("id", 1i64).property("age", 30i32).next().unwrap_err();
assert!(matches!(err, StoreError::SchemaViolation(_)));
}
{
let mut tx = graph.begin();
let err = tx.g().V([]).hasLabel(["animal"]).next().unwrap_err();
assert!(matches!(err, StoreError::SchemaViolation(_)));
let err = tx.g().V([]).has("age", 30i32).next().unwrap_err();
assert!(matches!(err, StoreError::SchemaViolation(_)));
let err = tx.g().V([]).out(["purchased"]).next().unwrap_err();
assert!(matches!(err, StoreError::SchemaViolation(_)));
}
}
#[test]
fn test_open_with_options_ignored_on_existing_db() {
let dir = tempdir().unwrap();
{
let graph = Graph::open_with_options(
dir.path(),
GraphOptions { mode: SchemaMode::Strict, edge_mode: EdgeMode::Single },
)
.unwrap();
assert_eq!(graph.schema().read().unwrap().mode, SchemaMode::Strict);
}
let reopened =
Graph::open_with_options(dir.path(), GraphOptions { mode: SchemaMode::Auto, edge_mode: EdgeMode::Multi })
.unwrap();
let s = reopened.schema();
let s = s.read().unwrap();
assert_eq!(s.mode, SchemaMode::Strict, "persisted schema_mode must win over new GraphOptions");
assert_eq!(s.edge_mode, EdgeMode::Single, "persisted edge_mode must win over new GraphOptions");
}
#[test]
fn test_auto_mode_write_invalidates_concurrent_schema_management_session() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
let mut mgmt = graph.open_management();
mgmt.add_vertex_label("project");
{
let mut tx = graph.begin();
tx.g().addV("person").property("id", 1i64).next().unwrap();
tx.commit().unwrap();
}
assert_eq!(graph.schema().read().unwrap().version, 1);
let err = mgmt.commit().unwrap_err();
assert!(matches!(err, StoreError::SchemaConflict(_)));
}
#[test]
fn test_auto_mode_unresolved_read_filters_yield_zero_results() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
{
let mut tx = graph.begin();
tx.g().addV("person").property("id", 1i64).next().unwrap();
tx.commit().unwrap();
}
let mut tx = graph.begin();
assert!(tx.g().V([1]).out(["never_registered"]).to_list().unwrap().is_empty());
assert!(tx.g().V([1]).hasLabel(["never_registered"]).to_list().unwrap().is_empty());
assert!(tx.g().V([1]).has("never_registered", 1i32).to_list().unwrap().is_empty());
}
#[test]
fn test_edge_property_type_mismatch_rejected() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
let mut tx = graph.begin();
tx.g().addV("person").property("id", 1i64).next().unwrap();
tx.g().addV("person").property("id", 2i64).next().unwrap();
tx.g().addE("knows").from(1).to(2).property("since", 2020i32).next().unwrap();
let err = tx.g().addE("knows").from(1).to(2).property("since", "a long time").next().unwrap_err();
assert!(matches!(err, StoreError::SchemaViolation(_)));
}
#[test]
fn test_concurrent_auto_mode_writes_do_not_starve_schema_lock() {
use crate::{api::TxSession, gremlin::traversal::__};
fn upsert_vertex(tx: &mut TxSession, vertex_id: i64) {
tx.g()
.V([vertex_id])
.count()
.coalesce([
__().V([vertex_id]).id(),
__().addV("person").property("id", vertex_id).property("name", "x").property("age", 30i32),
])
.next()
.unwrap();
}
fn upsert_edge(tx: &mut TxSession, src: i64, dst: i64) {
tx.g()
.V([src])
.coalesce([
__().outE(["knows"]).r#where(__().otherV().hasId([dst])).label(),
__().addE("knows").from(src).to(dst).property("weight", 1.0f64).property("since", 0i64),
])
.next()
.unwrap();
}
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
let (done_tx, done_rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
const THREADS: i64 = 4;
const PAIRS_PER_THREAD: i64 = 20;
let handles: Vec<_> = (0..THREADS)
.map(|t| {
let graph = graph.clone();
std::thread::spawn(move || {
for i in 0..PAIRS_PER_THREAD {
let src = t * 1000 + i;
let dst = src + 1;
for attempt in 0..5 {
let mut tx = graph.begin();
upsert_vertex(&mut tx, src);
upsert_vertex(&mut tx, dst);
upsert_edge(&mut tx, src, dst);
if tx.commit().is_ok() || attempt == 4 {
break;
}
}
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let _ = done_tx.send(());
});
done_rx
.recv_timeout(std::time::Duration::from_secs(60))
.expect("concurrent Auto-mode writes did not complete within 60s -- schema lock contention regressed");
}
#[test]
fn test_concurrent_auto_mode_complex_distinct_schemas() {
const THREADS: usize = 4;
const ITERATIONS: usize = 10;
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let graph_clone = graph.clone();
std::thread::spawn(move || {
let handles: Vec<_> = (0..THREADS)
.map(|t| {
let graph = graph_clone.clone();
std::thread::spawn(move || {
for i in 0..ITERATIONS {
let vertex_label = format!("v_lbl_{}_{}", t, i);
let edge_label = format!("e_lbl_{}_{}", t, i);
let prop_name = format!("p_name_{}_{}", t, i);
let prop_age = format!("p_age_{}_{}", t, i);
let src_id = (t * 10000 + i) as i64;
let dst_id = (t * 10000 + 5000 + i) as i64;
let mut attempt: u64 = 0;
loop {
let mut tx = graph.begin();
tx.g()
.addV(&vertex_label)
.property("id", src_id)
.property(&prop_name, format!("name_{}", src_id))
.property(&prop_age, src_id)
.next()
.unwrap();
tx.g()
.addV(&vertex_label)
.property("id", dst_id)
.property(&prop_name, format!("name_{}", dst_id))
.property(&prop_age, dst_id)
.next()
.unwrap();
tx.g().addE(&edge_label).from(src_id).to(dst_id).property("weight", 1.5f64).next().unwrap();
if tx.commit().is_ok() {
break;
}
let jitter = (attempt % 10) + 1;
std::thread::sleep(std::time::Duration::from_millis(jitter.min(50)));
attempt += 1;
}
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let _ = done_tx.send(());
});
done_rx
.recv_timeout(std::time::Duration::from_secs(60))
.expect("Complex concurrent Auto-mode schema updates did not complete within 60s");
let schema_lock = graph.schema();
let schema = schema_lock.read().unwrap();
for t in 0..THREADS {
for i in 0..ITERATIONS {
let vertex_label = format!("v_lbl_{}_{}", t, i);
let edge_label = format!("e_lbl_{}_{}", t, i);
let prop_name = format!("p_name_{}_{}", t, i);
let prop_age = format!("p_age_{}_{}", t, i);
assert!(schema.vertex_label_id(&vertex_label).is_some(), "Vertex label {} missing", vertex_label);
assert!(schema.edge_label_id(&edge_label).is_some(), "Edge label {} missing", edge_label);
assert!(schema.prop_key_id(&prop_name).is_some(), "Prop key {} missing", prop_name);
assert!(schema.prop_key_id(&prop_age).is_some(), "Prop key {} missing", prop_age);
}
}
}
#[test]
fn test_schema_persistence_across_restart() {
let dir = tempdir().unwrap();
let path = dir.path().to_path_buf();
{
let graph =
Graph::open_with_options(&path, GraphOptions { mode: SchemaMode::Strict, edge_mode: EdgeMode::Single })
.unwrap();
let mut mgmt = graph.open_management();
mgmt.add_vertex_label("person")
.add_edge_label("knows")
.add_property_key("name", DataType::String)
.add_property_key("age", DataType::Int32);
mgmt.commit().unwrap();
let mut tx = graph.begin();
tx.g().addV("person").property("id", 1i64).property("name", "alice").property("age", 30i32).next().unwrap();
tx.commit().unwrap();
graph.close().unwrap();
}
{
let graph = Graph::open(&path).unwrap();
let mut snap = graph.read();
let v = snap.g().withProperties([]).V([1]).next().unwrap().unwrap();
if let Value::Vertex(v) = v {
assert_eq!(v.label, SmolStr::from("person"));
assert_eq!(v.properties.get("name").unwrap()[0], Value::String("alice".to_string()));
} else {
panic!("Expected Vertex");
}
let mut tx = graph.begin();
let result = tx.g().addV("undeclared").property("id", 99i64).next();
assert!(result.is_err(), "undeclared label in (persisted) Strict mode should error");
let s = graph.schema();
let schema = s.read().unwrap();
assert!(!schema.persisted_vertex_labels.is_empty(), "persisted_vertex_labels should be non-empty after reopen");
assert!(!schema.persisted_edge_labels.is_empty(), "persisted_edge_labels should be non-empty after reopen");
assert!(!schema.persisted_prop_keys.is_empty(), "persisted_prop_keys should be non-empty after reopen");
graph.close().unwrap();
}
}
#[test]
fn test_schema_conflict_on_incompatible_redeclaration() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
{
let mut mgmt = graph.open_management();
mgmt.add_property_key("score", DataType::Int64);
mgmt.commit().unwrap();
}
{
let mut mgmt = graph.open_management();
mgmt.add_property_key("score", DataType::String);
let result = mgmt.commit();
assert!(
matches!(result, Err(StoreError::SchemaConflict(_))),
"Expected SchemaConflict for incompatible redeclaration, got: {:?}",
result
);
}
{
let mut mgmt = graph.open_management();
mgmt.add_property_key("score", DataType::Int64);
mgmt.commit().unwrap(); }
graph.close().unwrap();
}
#[test]
fn test_edge_mode_ratchet_multi_to_single_rejected() {
let dir = tempdir().unwrap();
let graph =
Graph::open_with_options(dir.path(), GraphOptions { mode: SchemaMode::Auto, edge_mode: EdgeMode::Multi })
.unwrap();
let mut mgmt = graph.open_management();
mgmt.set_edge_mode(EdgeMode::Single);
let result = mgmt.commit();
assert!(
matches!(result, Err(StoreError::SchemaConflict(_))),
"Expected SchemaConflict when ratcheting Multi→Single, got: {:?}",
result
);
graph.close().unwrap();
let dir2 = tempdir().unwrap();
let graph2 = Graph::open(dir2.path()).unwrap();
{
let mut mgmt = graph2.open_management();
mgmt.set_edge_mode(EdgeMode::Multi);
mgmt.commit().unwrap(); }
graph2.close().unwrap();
}
#[test]
fn test_set_schema_mode_both_directions() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
{
let mut mgmt = graph.open_management();
mgmt.set_schema_mode(SchemaMode::Strict).add_vertex_label("item").add_property_key("val", DataType::Int64);
mgmt.commit().unwrap();
}
{
let mut tx = graph.begin();
let result = tx.g().addV("ghost").property("id", 1i64).next();
assert!(
matches!(result, Err(StoreError::SchemaViolation(_))),
"Expected SchemaViolation in Strict mode, got: {:?}",
result
);
}
{
let mut mgmt = graph.open_management();
mgmt.set_schema_mode(SchemaMode::Auto);
mgmt.commit().unwrap();
}
{
let mut tx = graph.begin();
tx.g().addV("ghost").property("id", 1i64).next().unwrap();
tx.commit().unwrap();
}
let mut snap = graph.read();
let v = snap.g().V([1]).hasLabel(["ghost"]).next().unwrap();
assert!(v.is_some(), "ghost vertex should exist after auto-registration");
graph.close().unwrap();
}
#[test]
fn test_graph_close_with_clones() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
{
let mut tx = graph.begin();
tx.g().addV("person").property("id", 1i64).next().unwrap();
tx.commit().unwrap();
}
let graph2 = graph.clone();
graph.close().unwrap();
let mut snap = graph2.read();
let v = snap.g().V([1]).next().unwrap();
assert!(v.is_some(), "clone handle should still work after original is closed");
graph2.close().unwrap();
}
#[test]
fn test_schema_management_fluent_api_and_display() {
let dir = tempdir().unwrap();
let graph = Graph::open(dir.path()).unwrap();
{
let mut mgmt = graph.open_management();
mgmt.add_vertex_label("person").add_edge_label("knows").add_property_key("age", DataType::Int32);
mgmt.commit().unwrap();
}
{
let s = graph.schema();
let s = s.read().unwrap();
assert!(s.vertex_label_id("person").is_some());
assert!(s.edge_label_id("knows").is_some());
assert!(s.prop_key_id("age").is_some());
}
{
let mut mgmt = graph.open_management();
mgmt.add_vertex_label("software").add_property_key("lang", DataType::String);
let display_str = format!("{}", mgmt);
assert!(display_str.contains("=== RocksGraph Schema"));
assert!(display_str.contains("Schema Mode: Auto"));
assert!(display_str.contains("Edge Mode: Single"));
assert!(display_str.contains("- person"));
assert!(display_str.contains("- knows"));
assert!(display_str.contains("- age (Int32)"));
assert!(!display_str.contains("- software"));
assert!(!display_str.contains("- lang"));
mgmt.commit().unwrap();
}
{
let s = graph.schema();
let s = s.read().unwrap();
assert!(s.vertex_label_id("software").is_some());
assert!(s.prop_key_id("lang").is_some());
let mgmt = graph.open_management();
let display_str = format!("{}", mgmt);
assert!(display_str.contains("- software"));
assert!(display_str.contains("- lang (String)"));
}
}