use std::collections::HashMap;
use marsdb::{Database, PropertyValue};
fn seeded() -> Database {
let db = Database::in_memory().unwrap();
db.execute("CREATE (:Extra)").unwrap();
for u in 0..6 {
db.execute_with_params(
"CREATE (:User {uid: $u})",
&HashMap::from([("u".to_string(), PropertyValue::Int(u))]),
)
.unwrap();
}
for m in 0..30 {
db.execute_with_params(
"CREATE (:Movie {mid: $m})",
&HashMap::from([("m".to_string(), PropertyValue::Int(m))]),
)
.unwrap();
}
for u in 0..6i64 {
for m in 0..30i64 {
if (u + m) % 3 == 0 {
db.execute_with_params(
"MATCH (a:User {uid: $u}), (b:Movie {mid: $m}) \
CREATE (a)-[:RATED {rating: $r}]->(b)",
&HashMap::from([
("u".to_string(), PropertyValue::Int(u)),
("m".to_string(), PropertyValue::Int(m)),
("r".to_string(), PropertyValue::Float((m % 5) as f64 + 0.5)),
]),
)
.unwrap();
}
}
}
db.execute("MATCH (a:User {uid: 0}), (b:Movie {mid: 1}) CREATE (a)-[:RATED]->(b)")
.unwrap();
db.execute(
"MATCH (a:User {uid: 1}), (b:Movie {mid: 2}) CREATE (a)-[:VIEWED {rating: 0.5}]->(b)",
)
.unwrap();
db.execute("MATCH (a:User {uid: 2}), (x:Extra) CREATE (a)-[:RATED {rating: 0.5}]->(x)")
.unwrap();
db
}
fn rows_sorted(
db: &Database,
cypher: &str,
params: &HashMap<String, PropertyValue>,
) -> Vec<String> {
let result = db.execute_with_params(cypher, params).unwrap();
let mut out: Vec<String> = result.rows.iter().map(|r| format!("{r:?}")).collect();
out.sort();
out
}
fn plan_of(db: &Database, cypher: &str) -> String {
format!(
"{:?}",
db.execute(&format!("EXPLAIN {cypher}")).unwrap().rows
)
}
fn expected_rated_pairs(pred: impl Fn(f64) -> bool) -> Vec<(i64, i64)> {
let mut out = Vec::new();
for u in 0..6i64 {
for m in 0..30i64 {
if (u + m) % 3 == 0 && pred((m % 5) as f64 + 0.5) {
out.push((u, m));
}
}
}
out.sort_unstable();
out
}
fn got_pairs(db: &Database, cypher: &str) -> Vec<(i64, i64)> {
let result = db.execute(cypher).unwrap();
let mut out: Vec<(i64, i64)> = result
.rows
.iter()
.map(|row| {
let int = |v: &marsdb::Value| match v {
marsdb::Value::Property(PropertyValue::Int(i)) => *i,
other => panic!("{other:?}"),
};
(int(&row[0]), int(&row[1]))
})
.collect();
out.sort_unstable();
out
}
#[test]
fn sweep_and_anchor_paths_agree_across_shapes() {
let db = seeded();
let cases: &[(&str, &str, PropertyValue)] = &[
(
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating < 1.0 RETURN u.uid, m.mid",
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating < $x RETURN u.uid, m.mid",
PropertyValue::Float(1.0),
),
(
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating >= 3.5 RETURN count(*)",
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating >= $x RETURN count(*)",
PropertyValue::Float(3.5),
),
(
"MATCH (m:Movie)<-[r:RATED]-(u:User) WHERE r.rating < 1.0 RETURN m.mid, u.uid",
"MATCH (m:Movie)<-[r:RATED]-(u:User) WHERE r.rating < $x RETURN m.mid, u.uid",
PropertyValue::Float(1.0),
),
(
"MATCH (u:User)-[r]->(m:Movie) WHERE r.rating < 1.0 RETURN u.uid, m.mid",
"MATCH (u:User)-[r]->(m:Movie) WHERE r.rating < $x RETURN u.uid, m.mid",
PropertyValue::Float(1.0),
),
(
"MATCH (u:User)-[r:RATED|VIEWED]->(m:Movie) WHERE r.rating < 1.0 RETURN u.uid, m.mid",
"MATCH (u:User)-[r:RATED|VIEWED]->(m:Movie) WHERE r.rating < $x RETURN u.uid, m.mid",
PropertyValue::Float(1.0),
),
(
"MATCH (a)-[r:RATED]->(b) WHERE r.rating < 1.0 RETURN a.uid, b.mid",
"MATCH (a)-[r:RATED]->(b) WHERE r.rating < $x RETURN a.uid, b.mid",
PropertyValue::Float(1.0),
),
];
for (literal, paramed, value) in cases {
let lit_plan = plan_of(&db, literal);
assert!(lit_plan.contains("EdgeTypeScan"), "{literal}: {lit_plan}");
let got = rows_sorted(&db, literal, &HashMap::new());
let want = rows_sorted(
&db,
paramed,
&HashMap::from([("x".to_string(), value.clone())]),
);
assert_eq!(got, want, "{literal}");
assert!(!got.is_empty(), "{literal} matched nothing -- weak test");
}
assert_eq!(
got_pairs(
&db,
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating < 1.0 RETURN u.uid, m.mid"
),
expected_rated_pairs(|r| r < 1.0),
);
assert_eq!(
got_pairs(
&db,
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating >= 3.5 RETURN u.uid, m.mid"
),
expected_rated_pairs(|r| r >= 3.5),
);
assert_eq!(
got_pairs(
&db,
"MATCH (m:Movie)<-[r:RATED]-(u:User) WHERE r.rating < 1.0 RETURN u.uid, m.mid"
),
expected_rated_pairs(|r| r < 1.0),
);
}
#[test]
fn null_predicates_and_missing_props() {
let db = seeded();
let is_null = db
.execute("MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating IS NULL RETURN u.uid, m.mid")
.unwrap();
assert_eq!(is_null.rows.len(), 1);
assert!(plan_of(
&db,
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating IS NULL RETURN u.uid"
)
.contains("EdgeTypeScan"));
let not_null = db
.execute("MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating IS NOT NULL RETURN count(*)")
.unwrap();
let all = db
.execute("MATCH (u:User)-[r:RATED]->(m:Movie) RETURN count(*)")
.unwrap();
let n = |r: &marsdb::QueryResult| format!("{:?}", r.rows[0][0]);
assert_ne!(n(¬_null), n(&all));
}
#[test]
fn endpoint_labels_filter_in_scan() {
let db = seeded();
let to_movie = db
.execute("MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating = 0.5 RETURN count(*)")
.unwrap();
let to_any = db
.execute("MATCH (u:User)-[r:RATED]->(x) WHERE r.rating = 0.5 RETURN count(*)")
.unwrap();
let n = |r: &marsdb::QueryResult| format!("{:?}", r.rows[0][0]);
assert_ne!(n(&to_movie), n(&to_any));
}
#[test]
fn delete_and_stats_through_the_sweep() {
let db = seeded();
let before = db
.execute("MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating < 1.0 RETURN count(*)")
.unwrap();
let expected = match &before.rows[0][0] {
marsdb::Value::Property(PropertyValue::Int(n)) => *n as u64,
other => panic!("{other:?}"),
};
assert!(expected > 0);
let stats = db
.execute("MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating < 1.0 DELETE r")
.unwrap()
.stats;
assert_eq!(stats.relationships_deleted, expected);
let after = db
.execute("MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating < 1.0 RETURN count(*)")
.unwrap();
assert_eq!(format!("{:?}", after.rows[0][0]), "Property(Int(0))");
}
#[test]
fn residual_endpoint_filters_still_apply() {
let db = seeded();
let lit = rows_sorted(
&db,
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating < 2.0 AND u.uid = 3 RETURN m.mid",
&HashMap::new(),
);
let par = rows_sorted(
&db,
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating < $x AND u.uid = 3 RETURN m.mid",
&HashMap::from([("x".to_string(), PropertyValue::Float(2.0))]),
);
assert_eq!(lit, par);
assert!(!lit.is_empty());
}
#[test]
fn ineligible_shapes_keep_the_traditional_plan() {
let db = seeded();
for cypher in [
"MATCH (u:User)-[r:RATED]-(m:Movie) WHERE r.rating < 1.0 RETURN u.uid",
"MATCH (u:User {uid: 1})-[r:RATED]->(m:Movie) WHERE r.rating < 1.0 RETURN m.mid",
"MATCH (u:User)-[r:RATED]->(m:Movie) RETURN count(*)",
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating = r.rating RETURN count(*)",
] {
let plan = plan_of(&db, cypher);
assert!(!plan.contains("EdgeTypeScan"), "{cypher}: {plan}");
db.execute(cypher).unwrap();
}
}
#[test]
fn limit_stops_early_and_streaming_composes() {
let db = seeded();
let r = db
.execute("MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating < 6.0 RETURN u.uid LIMIT 3")
.unwrap();
assert_eq!(r.rows.len(), 3);
struct Count(usize);
impl marsdb::RowSink for Count {
fn columns(&mut self, _: &[String]) {}
fn row(&mut self, _: Vec<marsdb::Value>) -> std::ops::ControlFlow<()> {
self.0 += 1;
std::ops::ControlFlow::Continue(())
}
}
let mut sink = Count(0);
db.execute_streaming(
"MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating < 1.0 RETURN u.uid",
&HashMap::new(),
&marsdb::ExecutionOptions::default(),
&mut sink,
)
.unwrap();
assert!(sink.0 > 0);
}