#![cfg(feature = "gql")]
use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
use grafeo_engine::database::QueryResult;
#[cfg(all(feature = "text-index", feature = "vector-index"))]
fn article_fixture(text_index: bool, vector_index: bool) -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let rows: [(&str, &str, Vec<f32>, bool); 3] = [
(
"Graph Neural Networks",
"attention mechanisms in graph neural networks for node classification",
vec![0.9, 0.1, 0.0],
true,
),
(
"Rust Database Internals",
"building a database engine in rust with MVCC transactions",
vec![0.1, 0.9, 0.0],
false,
),
(
"Transformer Architectures",
"attention mechanisms and transformer models for natural language",
vec![0.8, 0.2, 0.1],
true,
),
];
for (title, body, emb, published) in rows {
let n = db.create_node(&["Article"]);
db.set_node_property(n, "title", Value::String(title.into()));
db.set_node_property(n, "body", Value::String(body.into()));
db.set_node_property(n, "embedding", Value::Vector(emb.into()));
db.set_node_property(n, "published", Value::Bool(published));
}
if vector_index {
db.create_vector_index(
"Article",
"embedding",
Some(3),
Some("cosine"),
None,
None,
None,
)
.expect("create vector index");
}
if text_index {
db.create_text_index("Article", "body")
.expect("create text index");
}
db
}
fn social_graph() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
db.session()
.execute(
"CREATE (alix:Person {name: 'Alix', age: 30, city: 'Amsterdam'}),
(gus:Person {name: 'Gus', age: 25, city: 'Berlin'}),
(vincent:Person {name: 'Vincent', age: 40, city: 'Paris'}),
(jules:Person {name: 'Jules', age: 35, city: 'Amsterdam'}),
(mia:Person {name: 'Mia', age: 22, city: 'Prague'}),
(alix)-[:KNOWS]->(gus),
(gus)-[:KNOWS]->(vincent),
(alix)-[:KNOWS]->(jules)",
)
.unwrap();
db
}
fn strings_col0(r: &QueryResult) -> Vec<String> {
r.rows()
.iter()
.filter_map(|row| match &row[0] {
Value::String(s) => Some(s.to_string()),
_ => None,
})
.collect()
}
#[cfg(all(feature = "text-index", feature = "vector-index"))]
#[test]
fn test_hybrid_or_vector_or_text() {
let session = article_fixture(true, true).session();
let r = session
.execute(
"MATCH (doc:Article) \
WHERE text_score(doc.body, 'rust database') > 0.3 \
OR cosine_similarity(doc.embedding, [0.8, 0.2, 0.1]) > 0.8 \
RETURN doc.title",
)
.expect("OR compound hybrid should plan and execute");
let titles = strings_col0(&r);
assert!(
titles.contains(&"Rust Database Internals".to_string()),
"expected rust article via text branch, got {titles:?}"
);
assert!(
titles.len() >= 2,
"expected union to span both branches, got {titles:?}"
);
}
#[cfg(all(feature = "text-index", feature = "vector-index"))]
#[test]
fn test_hybrid_or_swapped_order() {
let session = article_fixture(true, true).session();
let r = session
.execute(
"MATCH (doc:Article) \
WHERE cosine_similarity(doc.embedding, [0.1, 0.9, 0.0]) > 0.8 \
OR text_score(doc.body, 'attention') > 0.0 \
RETURN doc.title",
)
.expect("OR with vector on left, text on right should plan");
let titles = strings_col0(&r);
assert!(
titles.len() >= 2,
"expected 2+ titles from union of vector + text branches, got {titles:?}"
);
}
#[cfg(all(feature = "text-index", feature = "vector-index"))]
#[test]
fn test_hybrid_and_with_scalar_remainder() {
let session = article_fixture(true, true).session();
let r = session
.execute(
"MATCH (doc:Article) \
WHERE text_score(doc.body, 'attention') > 0.0 \
AND cosine_similarity(doc.embedding, [0.8, 0.2, 0.1]) > 0.3 \
AND doc.published = true \
RETURN doc.title \
ORDER BY doc.title",
)
.expect("AND with scalar remainder should plan and execute");
let titles = strings_col0(&r);
assert!(
!titles.contains(&"Rust Database Internals".to_string()),
"scalar remainder must filter out unpublished articles, got {titles:?}"
);
assert!(
!titles.is_empty(),
"expected at least one published attention-article, got {titles:?}"
);
}
#[cfg(all(feature = "text-index", feature = "vector-index"))]
#[test]
fn test_hybrid_and_missing_text_index_falls_through() {
let session = article_fixture(false, true).session();
let r = session
.execute(
"MATCH (doc:Article) \
WHERE cosine_similarity(doc.embedding, [0.9, 0.1, 0.0]) > 0.5 \
AND text_match(doc.body, 'attention') \
RETURN doc.title",
)
.expect("missing text index should fall through, not error");
assert_eq!(r.row_count(), 0);
}
#[cfg(all(feature = "text-index", feature = "vector-index"))]
#[test]
fn test_hybrid_or_missing_text_index_falls_through() {
let session = article_fixture(false, true).session();
let r = session
.execute(
"MATCH (doc:Article) \
WHERE cosine_similarity(doc.embedding, [0.9, 0.1, 0.0]) > 0.5 \
OR text_match(doc.body, 'attention') \
RETURN doc.title",
)
.expect("OR with missing text index should fall through");
assert!(r.row_count() >= 1);
}
#[cfg(all(feature = "text-index", feature = "vector-index"))]
#[test]
fn test_hybrid_and_missing_vector_index_falls_through() {
let session = article_fixture(true, false).session();
let r = session
.execute(
"MATCH (doc:Article) \
WHERE cosine_similarity(doc.embedding, [0.9, 0.1, 0.0]) > 0.5 \
AND text_match(doc.body, 'attention') \
RETURN doc.title",
)
.expect("missing vector index should fall through");
assert!(r.row_count() >= 1);
}
#[cfg(all(feature = "text-index", feature = "vector-index"))]
#[test]
fn test_hybrid_and_non_literal_vector_falls_through() {
let session = article_fixture(true, true).session();
let r = session
.execute(
"MATCH (doc:Article) \
WHERE cosine_similarity(doc.embedding, doc.embedding) > 0.9 \
AND text_match(doc.body, 'attention') \
RETURN doc.title",
)
.expect("non-literal vector must fall through, not error");
let titles = strings_col0(&r);
assert!(
!titles.contains(&"Rust Database Internals".to_string()),
"text branch must exclude rust article, got {titles:?}"
);
}
#[cfg(all(feature = "text-index", feature = "vector-index"))]
#[test]
fn test_hybrid_or_with_nested_and_scalar() {
let session = article_fixture(true, true).session();
let r = session.execute(
"MATCH (doc:Article) \
WHERE (cosine_similarity(doc.embedding, [0.9, 0.1, 0.0]) > 0.5 AND doc.published = true) \
OR text_match(doc.body, 'rust database') \
RETURN doc.title",
);
if let Ok(rs) = r {
assert!(rs.row_count() >= 1);
}
}
#[cfg(feature = "text-index")]
#[test]
fn test_text_pushdown_with_remaining() {
let db = GrafeoDB::new_in_memory();
let rows = [
("rust guide", "rust memory and transactions", true),
("rust draft", "rust memory safety", false),
("graph", "property graphs and queries", true),
];
for (title, body, published) in rows {
let n = db.create_node(&["Article"]);
db.set_node_property(n, "title", Value::String(title.into()));
db.set_node_property(n, "body", Value::String(body.into()));
db.set_node_property(n, "published", Value::Bool(published));
}
db.create_text_index("Article", "body").unwrap();
let r = db
.session()
.execute(
"MATCH (doc:Article) \
WHERE text_score(doc.body, 'rust') > 0.0 \
AND doc.published = true \
RETURN doc.title",
)
.expect("text pushdown with remainder should plan and execute");
let titles = strings_col0(&r);
assert_eq!(
titles,
vec!["rust guide".to_string()],
"only the published rust article must pass, got {titles:?}"
);
}
#[cfg(feature = "text-index")]
#[test]
fn test_text_pushdown_with_remaining_reversed() {
let db = GrafeoDB::new_in_memory();
let rows = [
("rust guide", "rust memory", true),
("draft", "rust draft", false),
];
for (title, body, published) in rows {
let n = db.create_node(&["Article"]);
db.set_node_property(n, "title", Value::String(title.into()));
db.set_node_property(n, "body", Value::String(body.into()));
db.set_node_property(n, "published", Value::Bool(published));
}
db.create_text_index("Article", "body").unwrap();
let r = db
.session()
.execute(
"MATCH (doc:Article) \
WHERE doc.published = true \
AND text_score(doc.body, 'rust') > 0.0 \
RETURN doc.title",
)
.expect("scalar AND text should plan and execute");
assert_eq!(strings_col0(&r), vec!["rust guide".to_string()]);
}
#[cfg(feature = "vector-index")]
#[test]
fn test_vector_pushdown_with_remaining() {
let db = GrafeoDB::new_in_memory();
let rows = [
("near-published", vec![0.9f32, 0.1, 0.0], true),
("near-draft", vec![0.9f32, 0.1, 0.0], false),
("far-published", vec![0.0f32, 1.0, 0.0], true),
];
for (title, emb, published) in rows {
let n = db.create_node(&["Doc"]);
db.set_node_property(n, "title", Value::String(title.into()));
db.set_node_property(n, "embedding", Value::Vector(emb.into()));
db.set_node_property(n, "published", Value::Bool(published));
}
db.create_vector_index(
"Doc",
"embedding",
Some(3),
Some("cosine"),
None,
None,
None,
)
.unwrap();
let r = db
.session()
.execute(
"MATCH (d:Doc) \
WHERE cosine_similarity(d.embedding, [0.9, 0.1, 0.0]) > 0.5 \
AND d.published = true \
RETURN d.title",
)
.expect("vector pushdown with remainder should execute");
assert_eq!(strings_col0(&r), vec!["near-published".to_string()]);
}
#[cfg(feature = "vector-index")]
#[test]
fn test_vector_scan_with_similarity_and_distance() {
let db = GrafeoDB::new_in_memory();
let rows: [(&str, Vec<f32>); 3] = [
("same", vec![0.9, 0.1, 0.0]),
("near", vec![0.85, 0.15, 0.0]),
("far", vec![0.0, 1.0, 0.0]),
];
for (title, emb) in rows {
let n = db.create_node(&["Doc"]);
db.set_node_property(n, "title", Value::String(title.into()));
db.set_node_property(n, "embedding", Value::Vector(emb.into()));
}
db.create_vector_index(
"Doc",
"embedding",
Some(3),
Some("cosine"),
None,
None,
None,
)
.unwrap();
let r = db
.session()
.execute(
"MATCH (d:Doc) \
WHERE cosine_similarity(d.embedding, [0.9, 0.1, 0.0]) > 0.8 \
AND euclidean_distance(d.embedding, [0.9, 0.1, 0.0]) < 0.5 \
RETURN d.title",
)
.expect("combined similarity + distance filter should plan and execute");
let titles = strings_col0(&r);
assert!(
!titles.contains(&"far".to_string()),
"'far' must be filtered out by either bound, got {titles:?}"
);
assert!(
titles.contains(&"same".to_string()),
"'same' must survive both bounds, got {titles:?}"
);
}
#[cfg(feature = "text-index")]
#[test]
fn test_plan_text_scan_threshold_only_no_limit() {
let db = GrafeoDB::new_in_memory();
for (title, body) in [
("rust tutorial", "rust tutorial rust rust rust"),
("brief", "rust brief"),
("unrelated", "graphs and queries"),
] {
let n = db.create_node(&["Article"]);
db.set_node_property(n, "title", Value::String(title.into()));
db.set_node_property(n, "body", Value::String(body.into()));
}
db.create_text_index("Article", "body").unwrap();
let r = db
.session()
.execute(
"MATCH (doc:Article) WHERE text_score(doc.body, 'rust') > 0.3 \
RETURN doc.title",
)
.expect("threshold-only text_score should plan");
let titles = strings_col0(&r);
assert!(
!titles.contains(&"unrelated".to_string()),
"unrelated article must not pass threshold, got {titles:?}"
);
}
#[cfg(feature = "vector-index")]
#[test]
fn test_resolve_vector_literal_string_element_falls_through() {
let db = GrafeoDB::new_in_memory();
let n = db.create_node(&["Doc"]);
db.set_node_property(n, "title", Value::String("only".into()));
db.set_node_property(n, "embedding", Value::Vector(vec![0.9f32, 0.1, 0.0].into()));
db.create_vector_index(
"Doc",
"embedding",
Some(3),
Some("cosine"),
None,
None,
None,
)
.unwrap();
let r = db.session().execute(
"MATCH (d:Doc) \
WHERE cosine_similarity(d.embedding, d.title) > 0.0 \
RETURN d.title",
);
match r {
Ok(rs) => {
assert!(rs.row_count() <= 1, "unexpected overflow: {rs:?}");
}
Err(_) => { }
}
}
#[test]
fn test_property_index_equality_pushdown_no_tx() {
let db = social_graph();
db.create_property_index("name");
let r = db
.session()
.execute("MATCH (n:Person) WHERE n.name = 'Alix' RETURN n.city")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Amsterdam".into()));
}
#[test]
fn test_property_equality_no_index_uses_label_scan() {
let db = social_graph();
let r = db
.session()
.execute("MATCH (n:Person) WHERE n.name = 'Mia' RETURN n.city")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Prague".into()));
}
#[test]
fn test_range_pushdown_with_label_intersect() {
let session = social_graph().session();
let r = session
.execute("MATCH (n:Person) WHERE n.age >= 30 RETURN n.name ORDER BY n.name")
.unwrap();
assert_eq!(
strings_col0(&r),
vec![
"Alix".to_string(),
"Jules".to_string(),
"Vincent".to_string()
]
);
}
#[test]
fn test_range_pushdown_reversed_operand_order() {
let session = social_graph().session();
let r = session
.execute("MATCH (n:Person) WHERE 35 < n.age RETURN n.name")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Vincent".into()));
}
#[test]
fn test_between_different_properties_falls_back() {
let session = social_graph().session();
let r = session
.execute(
"MATCH (n:Person) WHERE n.age >= 25 AND n.age > 30 \
RETURN n.name ORDER BY n.name",
)
.unwrap();
assert_eq!(
strings_col0(&r),
vec!["Jules".to_string(), "Vincent".to_string()]
);
}
#[test]
fn test_between_reversed_bound_order() {
let session = social_graph().session();
let r = session
.execute("MATCH (n:Person) WHERE n.age <= 35 AND n.age >= 25 RETURN n.name ORDER BY n.name")
.unwrap();
assert_eq!(
strings_col0(&r),
vec!["Alix".to_string(), "Gus".to_string(), "Jules".to_string()]
);
}
#[test]
fn test_not_exists_standalone_anti_join() {
let session = social_graph().session();
let r = session
.execute(
"MATCH (n:Person) \
WHERE NOT EXISTS { MATCH (n)-[:KNOWS]->() } \
RETURN n.name ORDER BY n.name",
)
.unwrap();
let names = strings_col0(&r);
assert_eq!(
names,
vec![
"Jules".to_string(),
"Mia".to_string(),
"Vincent".to_string(),
],
"NOT EXISTS (outgoing KNOWS) must include everyone without outgoing edges"
);
}
#[test]
fn test_exists_and_inside_nested_and() {
let session = social_graph().session();
let r = session
.execute(
"MATCH (n:Person) \
WHERE n.age >= 25 \
AND n.city <> 'Prague' \
AND EXISTS { MATCH (n)-[:KNOWS]->() } \
RETURN n.name ORDER BY n.name",
)
.unwrap();
let names = strings_col0(&r);
assert!(names.contains(&"Alix".to_string()));
assert!(names.contains(&"Gus".to_string()));
assert!(!names.contains(&"Mia".to_string()));
}
#[test]
fn test_count_comparison_with_remaining_predicate() {
let db = social_graph();
let r = db.session().execute(
"MATCH (n:Person) \
WHERE COUNT { MATCH (n)-[:KNOWS]->() } >= 1 \
AND n.age >= 30 \
RETURN n.name ORDER BY n.name",
);
if let Ok(rs) = r {
let names = strings_col0(&rs);
assert!(names.contains(&"Alix".to_string()));
assert!(!names.contains(&"Mia".to_string())); }
}
#[test]
fn test_count_comparison_reversed_operand() {
let r = social_graph().session().execute(
"MATCH (n:Person) \
WHERE 0 < COUNT { MATCH (n)-[:KNOWS]->() } \
RETURN n.name ORDER BY n.name",
);
if let Ok(rs) = r {
let names = strings_col0(&rs);
assert!(names.contains(&"Alix".to_string()));
assert!(names.contains(&"Gus".to_string()));
}
}
#[test]
fn test_or_with_no_exists_uses_regular_filter() {
let session = social_graph().session();
let r = session
.execute(
"MATCH (n:Person) \
WHERE n.city = 'Amsterdam' OR n.age < 25 \
RETURN n.name ORDER BY n.name",
)
.unwrap();
assert_eq!(
strings_col0(&r),
vec!["Alix".to_string(), "Jules".to_string(), "Mia".to_string()]
);
}
#[test]
fn test_complex_exists_or_scalar_uses_union() {
let session = social_graph().session();
let r = session.execute(
"MATCH (n:Person) \
WHERE EXISTS { MATCH (n)-[:KNOWS]->(m) WHERE m.age > 30 } \
OR n.city = 'Prague' \
RETURN n.name ORDER BY n.name",
);
if let Ok(rs) = r {
let names = strings_col0(&rs);
assert!(names.contains(&"Mia".to_string()));
}
}