#![cfg(feature = "lpg")]
use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
fn seed_items(n: usize) -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for i in 0..n {
let r = seed_value(i as u64);
session
.execute(&format!("INSERT (:Item {{id: {i}, r: {r}}})"))
.unwrap();
}
db
}
fn seed_value(i: u64) -> i64 {
#[allow(clippy::cast_possible_wrap)]
let v = (i.wrapping_mul(2_654_435_761) % 1_000_000) as i64;
v
}
fn explain(db: &GrafeoDB, query: &str) -> String {
let session = db.session();
let result = session
.execute(&format!("EXPLAIN {query}"))
.expect("EXPLAIN should not fail");
match &result.rows()[0][0] {
Value::String(s) => s.to_string(),
other => panic!("EXPLAIN should return String, got {other:?}"),
}
}
fn profile(db: &GrafeoDB, query: &str) -> String {
let session = db.session();
let result = session
.execute(&format!("PROFILE {query}"))
.expect("PROFILE should not panic");
match &result.rows()[0][0] {
Value::String(s) => s.to_string(),
other => panic!("PROFILE should return String, got {other:?}"),
}
}
#[cfg(feature = "vector-index")]
#[test]
fn cypher_vector_topk_still_fires_first() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for i in 0..10 {
#[allow(clippy::cast_possible_wrap)]
let x = (i + 1) as i64;
#[allow(clippy::cast_possible_wrap)]
let y = (9 - i) as i64;
session
.execute(&format!(
"INSERT (:Doc {{id: {i}, embedding: [{x}.0, {y}.0, 0.0]}})"
))
.unwrap();
}
db.create_vector_index(
"Doc",
"embedding",
Some(3),
Some("cosine"),
None,
None,
None,
)
.unwrap();
let result = session
.execute(
"MATCH (d:Doc) RETURN d.id \
ORDER BY cosine_similarity(d.embedding, [1.0, 0.0, 0.0]) DESC LIMIT 3",
)
.unwrap();
assert_eq!(result.row_count(), 3);
let top_id = match &result.rows()[0][0] {
Value::Int64(i) => *i,
other => panic!("expected Int64 id, got {other:?}"),
};
assert_eq!(top_id, 9, "id=9 has embedding closest to [1,0,0]");
}
#[test]
fn cypher_order_by_after_optional_match_uses_topk() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for i in 0..10 {
session
.execute(&format!("INSERT (:Person {{id: {i}, r: {i}}})"))
.unwrap();
if i % 2 == 0 {
session
.execute(&format!(
"MATCH (p:Person {{id: {i}}}) INSERT (p)-[:KNOWS]->(:Friend {{tag: {i}}})"
))
.unwrap();
}
}
let result = session
.execute(
"MATCH (p:Person) OPTIONAL MATCH (p)-[:KNOWS]->(f:Friend) \
RETURN p.id, f.tag ORDER BY p.r DESC LIMIT 3",
)
.unwrap();
assert_eq!(result.row_count(), 3);
assert_eq!(result.rows()[0][0], Value::Int64(9));
assert_eq!(result.rows()[1][0], Value::Int64(8));
assert_eq!(result.rows()[2][0], Value::Int64(7));
}
#[test]
fn cypher_order_by_with_filter_uses_topk() {
let db = seed_items(88);
let session = db.session();
let result = session
.execute("MATCH (n:Item) WHERE n.r > 100000 RETURN n.r ORDER BY n.r DESC LIMIT 5")
.unwrap();
let mut expected: Vec<i64> = (0..88_u64)
.map(seed_value)
.filter(|r| *r > 100_000)
.collect();
expected.sort_unstable_by(|a, b| b.cmp(a));
expected.truncate(5);
let actual: Vec<i64> = result
.rows()
.iter()
.map(|row| match &row[0] {
Value::Int64(r) => *r,
other => panic!("expected Int64, got {other:?}"),
})
.collect();
assert_eq!(actual, expected, "filter + sort + top-K result mismatch");
for r in &actual {
assert!(*r > 100_000, "filter should be honoured: r={r}");
}
}
#[test]
fn cypher_order_by_aggregate_alias_falls_through() {
let db = seed_items(19);
let session = db.session();
let result = session
.execute("MATCH (n:Item) RETURN n.id, count(*) AS c ORDER BY c DESC LIMIT 5")
.unwrap();
assert_eq!(result.row_count(), 5);
for row in result.rows() {
assert_eq!(row[1], Value::Int64(1), "every group has count 1");
}
}
#[test]
fn cypher_skip_limit_falls_through() {
let db = seed_items(19);
let session = db.session();
let result = session
.execute("MATCH (n:Item) RETURN n.r ORDER BY n.r DESC SKIP 5 LIMIT 5")
.unwrap();
assert_eq!(result.row_count(), 5);
let plan = explain(
&db,
"MATCH (n:Item) RETURN n.r ORDER BY n.r DESC SKIP 5 LIMIT 5",
);
assert!(
plan.contains("Skip"),
"Plan should contain Skip operator:\n{plan}"
);
}
#[test]
fn cypher_order_by_limit_unfused_under_profile() {
let db = seed_items(19);
let plan = profile(&db, "MATCH (n:Item) RETURN n.r ORDER BY n.r DESC LIMIT 5");
assert!(
plan.contains("Sort"),
"PROFILE under heap-rewrite-disabled should show Sort:\n{plan}"
);
assert!(
plan.contains("Limit"),
"PROFILE under heap-rewrite-disabled should show Limit:\n{plan}"
);
assert!(
plan.contains("rows="),
"PROFILE should report row counts:\n{plan}"
);
assert!(
!plan.contains("TopK"),
"PROFILE must not show TopK; rewrite should be gated by !profiling:\n{plan}"
);
}
#[test]
fn cypher_order_by_limit_uses_topk() {
let db = seed_items(88);
let session = db.session();
let result = session
.execute("MATCH (n:Item) RETURN n.r ORDER BY n.r DESC LIMIT 5")
.unwrap();
assert_eq!(result.row_count(), 5);
let mut all: Vec<i64> = (0..88_u64).map(seed_value).collect();
all.sort_unstable_by(|a, b| b.cmp(a));
let expected_top5: Vec<Value> = all.iter().take(5).map(|&v| Value::Int64(v)).collect();
let actual_top5: Vec<Value> = result.rows().iter().map(|row| row[0].clone()).collect();
assert_eq!(actual_top5, expected_top5);
}