use sparrowdb::{open, GraphDb};
use sparrowdb_execution::types::Value;
use sparrowdb_storage::node_store::Value as StoreValue;
fn make_db() -> (tempfile::TempDir, GraphDb) {
let dir = tempfile::tempdir().expect("tempdir");
let db = open(dir.path()).expect("open");
(dir, db)
}
#[test]
fn check_1_one_hop_scan() {
let (_dir, db) = make_db();
db.execute("CREATE (n:Person {name: 'Alice'})").unwrap();
db.execute("CREATE (n:Person {name: 'Bob'})").unwrap();
db.execute(
"MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) \
CREATE (a)-[:KNOWS]->(b)",
)
.expect("create KNOWS edge");
let result = db
.execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name")
.expect("1-hop MATCH must succeed");
assert_eq!(
result.rows.len(),
1,
"check 1: expected 1 row from 1-hop MATCH"
);
let src_name = match &result.rows[0][0] {
Value::String(s) => s.clone(),
v => panic!("check 1: expected String for a.name, got {v:?}"),
};
let dst_name = match &result.rows[0][1] {
Value::String(s) => s.clone(),
v => panic!("check 1: expected String for b.name, got {v:?}"),
};
assert_eq!(src_name, "Alice", "check 1: source must be Alice");
assert_eq!(dst_name, "Bob", "check 1: destination must be Bob");
}
#[test]
fn check_2_two_hop_asp_join() {
let (_dir, db) = make_db();
db.execute("CREATE (n:Person {name: 'Alice'})").unwrap();
db.execute("CREATE (n:Person {name: 'Bob'})").unwrap();
db.execute("CREATE (n:Person {name: 'Carol'})").unwrap();
db.execute(
"MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) CREATE (a)-[:KNOWS]->(b)",
)
.unwrap();
db.execute(
"MATCH (b:Person {name: 'Bob'}), (c:Person {name: 'Carol'}) CREATE (b)-[:KNOWS]->(c)",
)
.unwrap();
let result = db
.execute(
"MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) \
RETURN a.name, c.name",
)
.expect("2-hop MATCH must succeed");
assert_eq!(
result.rows.len(),
1,
"check 2: expected exactly 1 row from 2-hop MATCH"
);
let fof = match &result.rows[0][1] {
Value::String(s) => s.clone(),
v => panic!("check 2: expected String for c.name, got {v:?}"),
};
assert_eq!(fof, "Carol", "check 2: friend-of-friend must be Carol");
}
#[test]
fn check_3_wal_crash_recovery() {
let dir = tempfile::tempdir().expect("tempdir");
let db_path = dir.path().join("db");
{
let db = GraphDb::open(&db_path).expect("open session 1");
let mut tx = db.begin_write().expect("begin_write");
tx.create_node_named(1, &[("col_0".to_string(), StoreValue::Int64(0xDEAD))])
.ok();
}
{
let store = sparrowdb_storage::node_store::NodeStore::open(&db_path)
.expect("node store after crash");
let hwm = store.hwm_for_label(1).expect("hwm_for_label");
assert_eq!(
hwm, 0,
"check 3: uncommitted node must NOT be visible after restart"
);
}
{
let db = GraphDb::open(&db_path).expect("open session 2");
let mut tx = db.begin_write().expect("begin_write");
tx.create_node_named(2, &[("col_0".to_string(), StoreValue::Int64(42))])
.expect("create node");
tx.commit().expect("commit");
}
{
let store = sparrowdb_storage::node_store::NodeStore::open(&db_path)
.expect("node store after commit");
let hwm = store.hwm_for_label(2).expect("hwm_for_label 2");
assert_eq!(hwm, 1, "check 3: committed node must survive reopen");
}
}
#[test]
fn check_4_checkpoint_optimize_no_error() {
let (_dir, db) = make_db();
db.execute("CREATE (n:Item {name: 'widget'})").unwrap();
db.execute("CREATE (n:Item {name: 'gadget'})").unwrap();
db.execute("CHECKPOINT")
.expect("check 4: CHECKPOINT must succeed");
db.execute("OPTIMIZE")
.expect("check 4: OPTIMIZE must succeed");
let result = db
.execute("MATCH (n:Item) RETURN n.name")
.expect("check 4: MATCH after CHECKPOINT/OPTIMIZE must succeed");
assert_eq!(
result.rows.len(),
2,
"check 4: both items must be visible after CHECKPOINT and OPTIMIZE"
);
}
#[test]
fn check_5_snapshot_isolation() {
let (_dir, db) = make_db();
let alice;
let age_col = sparrowdb::fnv1a_col_id("age");
{
let mut tx = db.begin_write().expect("begin_write");
alice = tx
.create_node_named(
1,
&[
("name".to_string(), StoreValue::Bytes(b"Alice".to_vec())),
("age".to_string(), StoreValue::Int64(30)),
],
)
.expect("create Alice");
tx.commit().expect("commit");
}
let old_reader = db.begin_read().expect("begin_read (snapshot 1)");
let props_before = old_reader
.get_node(alice, &[age_col])
.expect("get_node before write");
assert_eq!(
props_before[0].1,
StoreValue::Int64(30),
"check 5: old reader must see age=30 before write"
);
{
let mut tx = db.begin_write().expect("begin_write tx2");
tx.set_node_col(alice, age_col, StoreValue::Int64(31));
tx.commit().expect("commit tx2");
}
let props_after = old_reader
.get_node(alice, &[age_col])
.expect("get_node after write");
assert_eq!(
props_after[0].1,
StoreValue::Int64(30),
"check 5: snapshot-pinned reader must still see age=30 after writer commits"
);
let new_reader = db.begin_read().expect("begin_read (snapshot 2)");
let props_new = new_reader
.get_node(alice, &[age_col])
.expect("get_node new reader");
assert_eq!(
props_new[0].1,
StoreValue::Int64(31),
"check 5: new reader must see age=31 after writer commits"
);
}
#[test]
fn check_6_encryption_auth_wrong_key() {
use sparrowdb_storage::encryption::EncryptionContext;
let key_a = [0x42u8; 32];
let key_b = [0x99u8; 32];
let plaintext = vec![0xABu8; 512];
let ctx_a = EncryptionContext::with_key(key_a);
let ctx_b = EncryptionContext::with_key(key_b);
let ciphertext = ctx_a
.encrypt_page(0, &plaintext)
.expect("check 6: encrypt_page must succeed");
let result = ctx_b.decrypt_page(0, &ciphertext);
assert!(
matches!(result, Err(sparrowdb_common::Error::EncryptionAuthFailed)),
"check 6: wrong key must return EncryptionAuthFailed, got: {result:?}"
);
let recovered = ctx_a
.decrypt_page(0, &ciphertext)
.expect("check 6: correct key must decrypt successfully");
assert_eq!(
recovered, plaintext,
"check 6: decrypted output must match original plaintext"
);
}
#[test]
fn check_7_order_by_large_result() {
let (_dir, db) = make_db();
for i in (1i64..=50).rev() {
let q = format!("CREATE (n:Metric {{val: {i}}})");
db.execute(&q).expect("CREATE Metric node");
}
let result = db
.execute("MATCH (n:Metric) RETURN n.val ORDER BY n.val ASC")
.expect("check 7: ORDER BY must succeed");
assert_eq!(
result.rows.len(),
50,
"check 7: expected 50 rows from ORDER BY scan"
);
let vals: Vec<i64> = result
.rows
.iter()
.filter_map(|row| match row.first() {
Some(Value::Int64(v)) => Some(*v),
_ => None,
})
.collect();
let sorted: Vec<i64> = {
let mut v = vals.clone();
v.sort_unstable();
v
};
assert_eq!(
vals, sorted,
"check 7: ORDER BY ASC must return rows in ascending order"
);
assert_eq!(vals[0], 1, "check 7: first row must be val=1 (smallest)");
assert_eq!(vals[49], 50, "check 7: last row must be val=50 (largest)");
}
#[test]
fn check_8_python_binding_skip_if_not_configured() {
eprintln!("check 8: Python binding round-trip — skipped (requires maturin build + Python interpreter)");
}
#[test]
fn check_9_nodejs_binding_skip_if_not_configured() {
eprintln!(
"check 9: Node.js binding round-trip — skipped (requires napi-rs build + Node.js runtime)"
);
}
#[test]
fn check_10_mutation_round_trip() {
let (_dir, db) = make_db();
db.execute("MERGE (n:Widget {name: 'sprocket'})")
.expect("check 10: first MERGE must succeed");
db.execute("MERGE (n:Widget {name: 'sprocket'})")
.expect("check 10: second MERGE must succeed");
let count = db
.execute("MATCH (n:Widget) RETURN COUNT(*)")
.expect("check 10: COUNT after MERGE");
assert_eq!(
count.rows[0][0],
Value::Int64(1),
"check 10: two identical MERGEs must produce exactly one node"
);
db.execute("MATCH (n:Widget {name: 'sprocket'}) SET n.version = 2")
.expect("check 10: MATCH SET must succeed");
let updated = db
.execute("MATCH (n:Widget {name: 'sprocket'}) RETURN n.version")
.expect("check 10: MATCH after SET");
assert_eq!(
updated.rows.len(),
1,
"check 10: must find one Widget after SET"
);
assert_eq!(
updated.rows[0][0],
Value::Int64(2),
"check 10: version must be 2 after SET"
);
db.execute("CREATE (n:Person {name: 'Alice'})").unwrap();
db.execute("CREATE (n:Person {name: 'Bob'})").unwrap();
db.execute(
"MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) CREATE (a)-[:KNOWS]->(b)",
)
.expect("check 10: CREATE edge must succeed");
let edge_result = db
.execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name")
.expect("check 10: MATCH on created edge");
assert_eq!(
edge_result.rows.len(),
1,
"check 10: the created KNOWS edge must be queryable"
);
}
#[test]
fn check_11_mvcc_write_write_conflict() {
use sparrowdb_common::Error;
let (_dir, db) = make_db();
let _w1 = db.begin_write().expect("check 11: first WriteTx must open");
let err = db
.begin_write()
.err()
.expect("check 11: second begin_write must fail with WriterBusy");
assert!(
matches!(err, Error::WriterBusy),
"check 11: expected WriterBusy, got: {err:?}"
);
drop(_w1);
let w2 = db
.begin_write()
.expect("check 11: begin_write after lock release must succeed");
w2.commit().expect("check 11: commit after lock release");
}
#[test]
fn check_12_unwind_list_expansion() {
let (_dir, db) = make_db();
let int_result = db
.execute("UNWIND [10, 20, 30] AS x RETURN x")
.expect("check 12: UNWIND integer list must succeed");
assert_eq!(
int_result.rows.len(),
3,
"check 12: UNWIND [10,20,30] must produce 3 rows"
);
let vals: Vec<i64> = int_result
.rows
.iter()
.filter_map(|r| match r.first() {
Some(Value::Int64(v)) => Some(*v),
_ => None,
})
.collect();
assert_eq!(
vals,
vec![10, 20, 30],
"check 12: UNWIND must preserve element order"
);
let str_result = db
.execute("UNWIND ['foo', 'bar'] AS s RETURN s")
.expect("check 12: UNWIND string list must succeed");
assert_eq!(
str_result.rows.len(),
2,
"check 12: UNWIND ['foo','bar'] must produce 2 rows"
);
let empty_result = db
.execute("UNWIND [] AS x RETURN x")
.expect("check 12: UNWIND empty list must succeed");
assert_eq!(
empty_result.rows.len(),
0,
"check 12: UNWIND [] must produce 0 rows"
);
}
#[test]
fn check_13_variable_length_paths() {
let (_dir, db) = make_db();
db.execute("CREATE (n:Node {name: 'A'})").unwrap();
db.execute("CREATE (n:Node {name: 'B'})").unwrap();
db.execute("CREATE (n:Node {name: 'C'})").unwrap();
db.execute("MATCH (a:Node {name: 'A'}), (b:Node {name: 'B'}) CREATE (a)-[:LINK]->(b)")
.unwrap();
db.execute("MATCH (b:Node {name: 'B'}), (c:Node {name: 'C'}) CREATE (b)-[:LINK]->(c)")
.unwrap();
let hop1 = db
.execute("MATCH (a:Node {name: 'A'})-[:LINK*1]->(b:Node) RETURN b.name")
.expect("check 13: *1 query must succeed");
assert_eq!(
hop1.rows.len(),
1,
"check 13: [:LINK*1] must return exactly 1 result (direct hop only)"
);
let hop12 = db
.execute("MATCH (a:Node {name: 'A'})-[:LINK*1..2]->(b:Node) RETURN b.name")
.expect("check 13: *1..2 query must succeed");
assert_eq!(
hop12.rows.len(),
2,
"check 13: [:LINK*1..2] must return 2 results (B and C)"
);
let names: Vec<String> = hop12
.rows
.iter()
.filter_map(|r| match r.first() {
Some(Value::String(s)) => Some(s.clone()),
_ => None,
})
.collect();
assert!(
names.contains(&"B".to_string()),
"check 13: B must be reachable via *1..2"
);
assert!(
names.contains(&"C".to_string()),
"check 13: C must be reachable via *1..2"
);
}
#[test]
fn check_14_fulltext_search() {
let (_dir, db) = make_db();
db.create_fulltext_index("acceptanceIndex")
.expect("check 14: create_fulltext_index must succeed");
let node_a = {
let mut tx = db.begin_write().expect("begin_write A");
let label_id = tx.create_label("Doc").expect("create_label") as u32;
let node_id = tx
.create_node_named(
label_id,
&[(
"content".to_string(),
StoreValue::Bytes(b"sparrow database".to_vec()),
)],
)
.expect("create node A");
tx.add_to_fulltext_index("acceptanceIndex", node_id, "sparrow database")
.expect("index A");
tx.commit().expect("commit A");
node_id
};
{
let catalog = sparrowdb_catalog::catalog::Catalog::open(_dir.path()).unwrap();
let label_id = catalog.get_label("Doc").unwrap().unwrap() as u32;
let mut tx = db.begin_write().expect("begin_write B");
let node_b = tx
.create_node_named(
label_id,
&[(
"content".to_string(),
StoreValue::Bytes(b"graph storage engine".to_vec()),
)],
)
.expect("create node B");
tx.add_to_fulltext_index("acceptanceIndex", node_b, "graph storage engine")
.expect("index B");
tx.commit().expect("commit B");
}
let result = db
.execute("CALL db.index.fulltext.queryNodes('acceptanceIndex', 'sparrow') YIELD node")
.expect("check 14: CALL fulltext search must succeed");
assert_eq!(
result.rows.len(),
1,
"check 14: 'sparrow' must match exactly 1 node"
);
let returned_id = match &result.rows[0][0] {
Value::NodeRef(n) => n.0,
other => panic!("check 14: expected NodeRef, got {other:?}"),
};
assert_eq!(
returned_id, node_a.0,
"check 14: returned node must be node A (the one indexed with 'sparrow')"
);
let result2 = db
.execute("CALL db.index.fulltext.queryNodes('acceptanceIndex', 'graph') YIELD node")
.expect("check 14: second CALL fulltext search must succeed");
assert_eq!(
result2.rows.len(),
1,
"check 14: 'graph' must match exactly 1 node"
);
let result3 = db
.execute("CALL db.index.fulltext.queryNodes('acceptanceIndex', 'xyzzy') YIELD node")
.expect("check 14: CALL for missing term must succeed (empty result)");
assert_eq!(
result3.rows.len(),
0,
"check 14: unknown term must return 0 rows"
);
}