use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
fn tarantino_graph() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let vincent = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Vincent".into())),
("age", Value::Int64(30)),
("city", Value::String("Amsterdam".into())),
],
)
.unwrap();
let jules = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Jules".into())),
("age", Value::Int64(25)),
("city", Value::String("Berlin".into())),
],
)
.unwrap();
let mia = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Mia".into())),
("age", Value::Int64(35)),
("city", Value::String("Paris".into())),
],
)
.unwrap();
session.create_edge(vincent, jules, "KNOWS");
session.create_edge(jules, mia, "KNOWS");
session.create_edge(vincent, mia, "KNOWS");
drop(session);
db
}
#[test]
fn standalone_return_literal_expression() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("RETURN 2 * 3 AS product").unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(6));
}
#[test]
fn standalone_return_string_literal() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute("RETURN 'Amsterdam' AS city, 42 AS answer")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Amsterdam".into()));
assert_eq!(result.rows()[0][1], Value::Int64(42));
}
#[test]
fn return_star_expands_all_user_columns() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Vincent'}) RETURN *")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert!(!result.columns.is_empty());
assert!(
result
.columns
.iter()
.any(|col| col == "n" || !col.starts_with('_')),
"columns: {:?}",
result.columns
);
}
#[test]
fn return_star_skips_internal_underscore_columns() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (p = (a:Person {name: 'Vincent'})-[:KNOWS]->(b:Person)){1,1} RETURN *")
.unwrap();
for col in &result.columns {
assert!(
!col.starts_with('_'),
"internal column leaked into RETURN *: {col}"
);
}
assert!(!result.rows().is_empty());
}
#[test]
fn return_type_of_edge_variable() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN type(r) AS rel_type LIMIT 1")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("KNOWS".into()));
}
#[test]
fn return_length_of_path_variable() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute(
"MATCH (p = (a:Person {name: 'Vincent'})-[:KNOWS]->(b:Person)){1,2} \
RETURN length(p) AS len ORDER BY len",
)
.unwrap();
assert!(!result.rows().is_empty());
let lengths: Vec<i64> = result
.rows()
.iter()
.filter_map(|r| match &r[0] {
Value::Int64(n) => Some(*n),
_ => None,
})
.collect();
assert!(lengths.contains(&1), "missing 1 hop, got {lengths:?}");
assert!(lengths.contains(&2), "missing 2 hop, got {lengths:?}");
}
#[test]
fn return_nodes_of_path_variable() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute(
"MATCH (p = (a:Person {name: 'Vincent'})-[:KNOWS]->(b:Person)){1,1} \
RETURN nodes(p) AS ns",
)
.unwrap();
assert_eq!(result.rows().len(), 2); for row in result.rows() {
match &row[0] {
Value::List(items) => assert_eq!(items.len(), 2, "expected 2 nodes per hop"),
other => panic!("expected list, got {other:?}"),
}
}
}
#[test]
fn return_edges_of_path_variable() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute(
"MATCH (p = (a:Person {name: 'Vincent'})-[:KNOWS]->(b:Person)){1,1} \
RETURN edges(p) AS es",
)
.unwrap();
assert_eq!(result.rows().len(), 2);
for row in result.rows() {
match &row[0] {
Value::List(items) => assert_eq!(items.len(), 1, "expected 1 edge per 1 hop path"),
other => panic!("expected list, got {other:?}"),
}
}
}
#[test]
fn return_length_of_string_expression() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Vincent'}) RETURN size(n.name) AS len")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(7));
}
#[test]
fn return_other_builtin_function_falls_through() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Vincent'}) RETURN head([n.name, 'placeholder']) AS first")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Vincent".into()));
}
#[test]
fn return_literal_constant_in_projection() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Vincent'}) RETURN n.name AS who, 42 AS answer")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Vincent".into()));
assert_eq!(result.rows()[0][1], Value::Int64(42));
}
#[test]
fn return_binary_expression() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Vincent'}) RETURN n.age + 5 AS bumped")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(35));
}
#[test]
fn return_unary_expression() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Vincent'}) RETURN -n.age AS negated")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(-30));
}
#[test]
fn return_list_literal() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Vincent'}) RETURN [n.name, n.city] AS info")
.unwrap();
assert_eq!(result.rows().len(), 1);
match &result.rows()[0][0] {
Value::List(items) => {
assert_eq!(items.len(), 2);
assert_eq!(items[0], Value::String("Vincent".into()));
assert_eq!(items[1], Value::String("Amsterdam".into()));
}
other => panic!("expected list, got {other:?}"),
}
}
#[test]
fn return_index_access() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute("UNWIND [['Vincent', 'Jules', 'Mia']] AS names RETURN names[1] AS pick")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Jules".into()));
}
#[test]
fn return_list_comprehension() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute(
"MATCH (n:Person {name: 'Vincent'}) \
RETURN [x IN [1, 2, 3] WHERE x > 1 | x * 10] AS doubled",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
match &result.rows()[0][0] {
Value::List(items) => {
assert_eq!(items.len(), 2);
assert_eq!(items[0], Value::Int64(20));
assert_eq!(items[1], Value::Int64(30));
}
other => panic!("expected list, got {other:?}"),
}
}
#[test]
fn return_list_predicate_any() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute(
"MATCH (n:Person {name: 'Vincent'}) \
RETURN any(x IN [1, 2, 3] WHERE x > 2) AS has_big",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Bool(true));
}
#[test]
fn return_case_when_expression() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute(
"MATCH (n:Person) \
RETURN n.name, \
CASE WHEN n.age >= 30 THEN 'senior' ELSE 'junior' END AS tier \
ORDER BY n.name",
)
.unwrap();
assert_eq!(result.rows().len(), 3);
let rows = result.rows();
assert_eq!(rows[0][0], Value::String("Jules".into()));
assert_eq!(rows[0][1], Value::String("junior".into()));
assert_eq!(rows[1][0], Value::String("Mia".into()));
assert_eq!(rows[1][1], Value::String("senior".into()));
assert_eq!(rows[2][0], Value::String("Vincent".into()));
assert_eq!(rows[2][1], Value::String("senior".into()));
}
#[test]
fn return_bare_node_variable_emits_resolved_map() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Vincent'}) RETURN n")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert!(
matches!(&result.rows()[0][0], Value::Map(_)),
"expected node resolved to Map, got {:?}",
result.rows()[0][0]
);
}
#[test]
fn return_bare_edge_variable_emits_resolved_map() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN r LIMIT 1")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert!(
matches!(&result.rows()[0][0], Value::Map(_)),
"expected edge resolved to Map, got {:?}",
result.rows()[0][0]
);
}
#[test]
fn return_bare_scalar_variable_pass_through() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute("UNWIND [10, 20, 30] AS x RETURN x ORDER BY x")
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][0], Value::Int64(10));
assert_eq!(result.rows()[1][0], Value::Int64(20));
assert_eq!(result.rows()[2][0], Value::Int64(30));
}
#[test]
fn return_distinct_deduplicates_values() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN DISTINCT n.city")
.unwrap();
assert_eq!(result.rows().len(), 3);
}
#[test]
fn return_distinct_with_duplicates() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Butch".into())),
("city", Value::String("Prague".into())),
],
)
.unwrap();
session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Django".into())),
("city", Value::String("Prague".into())),
],
)
.unwrap();
drop(session);
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN DISTINCT n.city")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Prague".into()));
}
#[test]
fn with_variable_node_passthrough() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Vincent'}) WITH n RETURN n.name")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Vincent".into()));
}
#[test]
fn with_property_access_registers_as_scalar() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person) WITH n.name AS name WHERE name = 'Jules' RETURN name")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Jules".into()));
}
#[test]
fn with_literal_constant() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Vincent'}) WITH n, 123 AS answer RETURN n.name, answer")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Vincent".into()));
assert_eq!(result.rows()[0][1], Value::Int64(123));
}
#[test]
fn with_complex_expression_is_registered_scalar() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Jules'}) WITH n.age + 1 AS plus RETURN plus")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(26));
}
#[test]
fn with_edge_variable_preserved_as_edge_column() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (a:Person)-[r:KNOWS]->(b:Person) WITH r RETURN type(r) AS rel_type LIMIT 1")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("KNOWS".into()));
}
#[test]
fn with_literal_list_and_size() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute(
"UNWIND [1] AS seed \
WITH seed, [1, 2, 3] AS nums \
RETURN size(nums) AS n",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(3));
}
#[test]
fn with_multiple_literal_bindings() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute(
"UNWIND [1] AS seed \
WITH 'Prague' AS city, 100 AS score \
RETURN city, score",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Prague".into()));
assert_eq!(result.rows()[0][1], Value::Int64(100));
}
#[test]
fn let_clause_passes_through_inputs_and_appends() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute(
"MATCH (n:Person {name: 'Vincent'}) \
LET bonus = n.age * 2 \
RETURN n.name, bonus",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Vincent".into()));
assert_eq!(result.rows()[0][1], Value::Int64(60));
}
#[test]
fn limit_restricts_output_count() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN n.name ORDER BY n.name LIMIT 2")
.unwrap();
assert_eq!(result.rows().len(), 2);
}
#[test]
fn skip_offsets_output() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN n.name ORDER BY n.name SKIP 1")
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("Mia".into()));
assert_eq!(result.rows()[1][0], Value::String("Vincent".into()));
}
#[test]
fn skip_then_limit_combines() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN n.name ORDER BY n.name SKIP 1 LIMIT 1")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Mia".into()));
}
#[test]
fn order_by_property_ascending_pre_return_projection() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN n.name ORDER BY n.age")
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][0], Value::String("Jules".into())); assert_eq!(result.rows()[1][0], Value::String("Vincent".into())); assert_eq!(result.rows()[2][0], Value::String("Mia".into())); assert_eq!(result.columns.len(), 1);
}
#[test]
fn order_by_property_descending_pre_return_projection() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN n.name ORDER BY n.age DESC")
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][0], Value::String("Mia".into()));
assert_eq!(result.rows()[1][0], Value::String("Vincent".into()));
assert_eq!(result.rows()[2][0], Value::String("Jules".into()));
}
#[test]
fn order_by_property_already_in_return() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN n.name ORDER BY n.name")
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][0], Value::String("Jules".into()));
assert_eq!(result.rows()[1][0], Value::String("Mia".into()));
assert_eq!(result.rows()[2][0], Value::String("Vincent".into()));
assert_eq!(result.columns.len(), 1);
}
#[test]
fn order_by_with_aliased_property() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN n.name AS who ORDER BY n.name")
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][0], Value::String("Jules".into()));
}
#[test]
fn order_by_on_labels_index_access() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(&["Bot"], [("name", Value::String("Hans".into()))])
.unwrap();
session
.create_node_with_props(&["Person"], [("name", Value::String("Beatrix".into()))])
.unwrap();
drop(session);
let session = db.session();
let result = session
.execute("MATCH (n) RETURN n.name ORDER BY labels(n)[0]")
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("Hans".into()));
assert_eq!(result.rows()[1][0], Value::String("Beatrix".into()));
assert_eq!(result.columns.len(), 1);
}
#[test]
fn order_by_on_edge_type_function() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let a = session
.create_node_with_props(&["Person"], [("name", Value::String("Shosanna".into()))])
.unwrap();
let b = session
.create_node_with_props(&["Person"], [("name", Value::String("Hans".into()))])
.unwrap();
let c = session
.create_node_with_props(&["Person"], [("name", Value::String("Beatrix".into()))])
.unwrap();
session.create_edge(a, b, "LIKES");
session.create_edge(a, c, "ADMIRES");
drop(session);
let session = db.session();
let result = session
.execute(
"MATCH (a:Person {name: 'Shosanna'})-[r]->(b:Person) \
RETURN b.name ORDER BY type(r)",
)
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("Beatrix".into()));
assert_eq!(result.rows()[1][0], Value::String("Hans".into()));
assert_eq!(result.columns.len(), 1);
}
#[test]
fn order_by_nulls_first_explicit() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(
&["Item"],
[
("name", Value::String("first".into())),
("score", Value::Int64(10)),
],
)
.unwrap();
session
.create_node_with_props(&["Item"], [("name", Value::String("missing".into()))])
.unwrap();
drop(session);
let session = db.session();
let result = session
.execute("MATCH (n:Item) RETURN n.name ORDER BY n.score ASC NULLS FIRST")
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("missing".into()));
assert_eq!(result.rows()[1][0], Value::String("first".into()));
}
#[test]
fn order_by_nulls_last_explicit() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(
&["Item"],
[
("name", Value::String("valued".into())),
("score", Value::Int64(42)),
],
)
.unwrap();
session
.create_node_with_props(&["Item"], [("name", Value::String("unknown".into()))])
.unwrap();
drop(session);
let session = db.session();
let result = session
.execute("MATCH (n:Item) RETURN n.name ORDER BY n.score ASC NULLS LAST")
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("valued".into()));
assert_eq!(result.rows()[1][0], Value::String("unknown".into()));
}
#[test]
fn order_by_on_scalar_column_from_with() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute(
"MATCH (n:Person) \
WITH n.name AS name, n.age AS age \
RETURN name, age ORDER BY age",
)
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][0], Value::String("Jules".into()));
assert_eq!(result.rows()[1][0], Value::String("Vincent".into()));
assert_eq!(result.rows()[2][0], Value::String("Mia".into()));
}
#[test]
fn order_by_multiple_keys_mixed_directions() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for (n, c, a) in [
("Vincent", "Amsterdam", 30),
("Jules", "Amsterdam", 40),
("Butch", "Berlin", 25),
("Django", "Berlin", 35),
] {
session
.create_node_with_props(
&["Person"],
[
("name", Value::String(n.into())),
("city", Value::String(c.into())),
("age", Value::Int64(a)),
],
)
.unwrap();
}
drop(session);
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN n.name ORDER BY n.city ASC, n.age DESC")
.unwrap();
assert_eq!(result.rows().len(), 4);
assert_eq!(result.rows()[0][0], Value::String("Jules".into()));
assert_eq!(result.rows()[1][0], Value::String("Vincent".into()));
assert_eq!(result.rows()[2][0], Value::String("Django".into()));
assert_eq!(result.rows()[3][0], Value::String("Butch".into()));
assert_eq!(result.columns.len(), 1);
}
#[test]
fn schema_derivation_preserves_edge_column_type() {
let db = tarantino_graph();
let session = db.session();
let result = session
.execute(
"MATCH (a:Person)-[r:KNOWS]->(b:Person) \
WITH r \
RETURN r LIMIT 1",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert!(
matches!(&result.rows()[0][0], Value::Map(_)),
"edge should be resolved to Map, got {:?}",
result.rows()[0][0]
);
}