use super::*;
fn as_regular_single_part(doc: Document) -> SinglePartQuery {
let Statement::Query(Query::Regular(rq)) = doc.statement else {
panic!("expected regular query");
};
let SingleQuery::SinglePart(sp) = rq.head else {
panic!("expected single-part query");
};
sp
}
fn as_regular_multi_part(doc: Document) -> MultiPartQuery {
let Statement::Query(Query::Regular(rq)) = doc.statement else {
panic!("expected regular query");
};
let SingleQuery::MultiPart(mp) = rq.head else {
panic!("expected multi-part query");
};
mp
}
fn as_standalone_call(doc: Document) -> StandaloneCall {
let Statement::Query(Query::StandaloneCall(call)) = doc.statement else {
panic!("expected standalone CALL");
};
call
}
fn first_match_clause(sp: &SinglePartQuery) -> &Match {
let Some(first) = sp.reading_clauses.first() else {
panic!("expected at least one reading clause");
};
let ReadingClause::Match(m) = first else {
panic!("expected first reading clause to be MATCH");
};
m
}
fn first_return_expr(sp: &SinglePartQuery) -> &Expr {
let ret = sp.return_clause.as_ref().expect("expected RETURN clause");
let Some(first) = ret.body.items.first() else {
panic!("expected at least one projection item");
};
let ProjectionItem::Expr { expr, .. } = first else {
panic!("expected first projection item to be an expression");
};
expr
}
#[test]
fn parse_basic_match_return() {
let doc = parse_query("MATCH (n) RETURN n").unwrap();
let sp = as_regular_single_part(doc);
assert_eq!(sp.reading_clauses.len(), 1);
assert!(sp.updating_clauses.is_empty());
assert!(sp.return_clause.is_some());
let m = first_match_clause(&sp);
assert!(!m.optional);
assert_eq!(m.pattern.parts.len(), 1);
assert!(m.where_.is_none());
match first_return_expr(&sp) {
Expr::Variable(v) => assert_eq!(v.name, "n"),
other => panic!("expected RETURN variable n, got {other:?}"),
}
}
#[test]
fn parse_match_with_label() {
let doc = parse_query("MATCH (n:User) RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { head, chain, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
assert!(chain.is_empty());
assert_eq!(head.variable.as_ref().map(|v| v.name.as_str()), Some("n"));
assert_eq!(head.labels.len(), 1);
assert_eq!(head.labels[0][0], "User");
assert!(head.properties.is_none());
}
#[test]
fn parse_match_multiple_labels() {
let doc = parse_query("MATCH (n:User:Admin) RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { head, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
assert_eq!(head.labels.len(), 2);
assert_eq!(head.labels[0][0], "User");
assert_eq!(head.labels[1][0], "Admin");
}
#[test]
fn parse_optional_match() {
let doc = parse_query("OPTIONAL MATCH (n) RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
assert!(m.optional);
assert!(m.where_.is_none());
}
#[test]
fn parse_match_relationship() {
let doc = parse_query("MATCH (a)-[:FOLLOWS]->(b) RETURN a, b").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { head, chain, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
assert_eq!(head.variable.as_ref().map(|v| v.name.as_str()), Some("a"));
assert_eq!(chain.len(), 1);
let rel = chain[0]
.relationship
.detail
.as_ref()
.expect("expected relationship detail");
assert_eq!(rel.types.len(), 1);
assert_eq!(rel.types[0], "FOLLOWS");
assert!(matches!(chain[0].relationship.direction, Direction::Right));
assert_eq!(
chain[0].node.variable.as_ref().map(|v| v.name.as_str()),
Some("b")
);
}
#[test]
fn parse_match_left_relationship() {
let doc = parse_query("MATCH (a)<-[:FOLLOWS]-(b) RETURN a, b").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { chain, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
assert_eq!(chain.len(), 1);
assert!(matches!(chain[0].relationship.direction, Direction::Left));
}
#[test]
fn parse_match_undirected_relationship() {
let doc = parse_query("MATCH (a)-[:KNOWS]-(b) RETURN a, b").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { chain, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
assert_eq!(chain.len(), 1);
assert!(matches!(
chain[0].relationship.direction,
Direction::Undirected
));
}
#[test]
fn parse_relationship_with_variable_and_range() {
let doc = parse_query("MATCH (a)-[r:FOLLOWS*1..3]->(b) RETURN r").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { chain, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
let rel = chain[0]
.relationship
.detail
.as_ref()
.expect("expected relationship detail");
assert_eq!(rel.variable.as_ref().map(|v| v.name.as_str()), Some("r"));
assert_eq!(rel.types.len(), 1);
assert_eq!(rel.types[0], "FOLLOWS");
let range = rel.range.as_ref().expect("expected range");
assert_eq!(range.start, Some(1));
assert_eq!(range.end, Some(3));
}
#[test]
fn parse_relationship_range_upper_only() {
let doc = parse_query("MATCH (a)-[:FOLLOWS*..3]->(b) RETURN a").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { chain, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
let range = chain[0]
.relationship
.detail
.as_ref()
.and_then(|d| d.range.as_ref())
.expect("expected range");
assert_eq!(range.start, None);
assert_eq!(range.end, Some(3));
}
#[test]
fn parse_relationship_range_lower_only() {
let doc = parse_query("MATCH (a)-[:FOLLOWS*3..]->(b) RETURN a").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { chain, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
let range = chain[0]
.relationship
.detail
.as_ref()
.and_then(|d| d.range.as_ref())
.expect("expected range");
assert_eq!(range.start, Some(3));
assert_eq!(range.end, None);
}
#[test]
fn parse_relationship_range_unbounded() {
let doc = parse_query("MATCH (a)-[:FOLLOWS*]->(b) RETURN a").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { chain, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
let range = chain[0]
.relationship
.detail
.as_ref()
.and_then(|d| d.range.as_ref())
.expect("expected range");
assert_eq!(range.start, None);
assert_eq!(range.end, None);
}
#[test]
fn parse_pattern_binding() {
let doc = parse_query("MATCH p = (a)-[:FOLLOWS]->(b) RETURN p").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
assert_eq!(m.pattern.parts.len(), 1);
assert_eq!(
m.pattern.parts[0].binding.as_ref().map(|v| v.name.as_str()),
Some("p")
);
match first_return_expr(&sp) {
Expr::Variable(v) => assert_eq!(v.name, "p"),
other => panic!("expected RETURN variable p, got {other:?}"),
}
}
#[test]
fn parse_parenthesized_pattern_element() {
let doc = parse_query("MATCH ((n)) RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
match &m.pattern.parts[0].element {
PatternElement::Parenthesized(inner, _) => match inner.as_ref() {
PatternElement::NodeChain { head, chain, .. } => {
assert!(chain.is_empty());
assert_eq!(head.variable.as_ref().map(|v| v.name.as_str()), Some("n"));
}
other => panic!("expected node chain inside parentheses, got {other:?}"),
},
other => panic!("expected parenthesized pattern element, got {other:?}"),
}
}
#[test]
fn parse_where_expression() {
let doc = parse_query("MATCH (n) WHERE 1 + 2 > 2 RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let where_ = m.where_.as_ref().expect("expected WHERE clause");
match where_ {
Expr::Binary { lhs, op, rhs, .. } => {
assert!(matches!(op, BinaryOp::Gt));
match lhs.as_ref() {
Expr::Binary {
lhs: add_lhs,
op: add_op,
rhs: add_rhs,
..
} => {
assert!(matches!(add_op, BinaryOp::Add));
assert!(matches!(add_lhs.as_ref(), Expr::Integer(1, _)));
assert!(matches!(add_rhs.as_ref(), Expr::Integer(2, _)));
}
other => panic!("expected lhs to be addition expression, got {other:?}"),
}
assert!(matches!(rhs.as_ref(), Expr::Integer(2, _)));
}
other => panic!("expected binary WHERE expression, got {other:?}"),
}
}
#[test]
fn parse_where_boolean_precedence() {
let doc = parse_query("MATCH (n) WHERE NOT n.active AND n.age >= 18 RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let where_ = m.where_.as_ref().expect("expected WHERE clause");
match where_ {
Expr::Binary { lhs, op, rhs, .. } => {
assert!(matches!(op, BinaryOp::And));
match lhs.as_ref() {
Expr::Unary {
op: UnaryOp::Not,
expr,
..
} => match expr.as_ref() {
Expr::Property { key, .. } => assert_eq!(key, "active"),
other => panic!("expected property under NOT, got {other:?}"),
},
other => panic!("expected NOT expression on lhs, got {other:?}"),
}
match rhs.as_ref() {
Expr::Binary {
lhs: cmp_lhs,
op: cmp_op,
rhs: cmp_rhs,
..
} => {
assert!(matches!(cmp_op, BinaryOp::Ge));
assert!(matches!(cmp_rhs.as_ref(), Expr::Integer(18, _)));
match cmp_lhs.as_ref() {
Expr::Property { key, .. } => assert_eq!(key, "age"),
other => panic!("expected property on comparison lhs, got {other:?}"),
}
}
other => panic!("expected comparison expression on rhs, got {other:?}"),
}
}
other => panic!("expected binary WHERE expression, got {other:?}"),
}
}
#[test]
fn parse_where_parenthesized_expression() {
let doc = parse_query("MATCH (n) WHERE (1 + 2) * 3 > 5 RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let where_ = m.where_.as_ref().expect("expected WHERE clause");
match where_ {
Expr::Binary { lhs, op, rhs, .. } => {
assert!(matches!(op, BinaryOp::Gt));
assert!(matches!(rhs.as_ref(), Expr::Integer(5, _)));
match lhs.as_ref() {
Expr::Binary {
lhs: mul_lhs,
op: mul_op,
rhs: mul_rhs,
..
} => {
assert!(matches!(mul_op, BinaryOp::Mul));
assert!(matches!(mul_rhs.as_ref(), Expr::Integer(3, _)));
match mul_lhs.as_ref() {
Expr::Binary {
lhs: add_lhs,
op: add_op,
rhs: add_rhs,
..
} => {
assert!(matches!(add_op, BinaryOp::Add));
assert!(matches!(add_lhs.as_ref(), Expr::Integer(1, _)));
assert!(matches!(add_rhs.as_ref(), Expr::Integer(2, _)));
}
other => {
panic!("expected addition under multiplication, got {other:?}")
}
}
}
other => panic!("expected multiplication lhs, got {other:?}"),
}
}
other => panic!("expected binary WHERE expression, got {other:?}"),
}
}
#[test]
fn parse_where_in_operator() {
let doc = parse_query("MATCH (n) WHERE n.age IN [1, 2, 3] RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let where_ = first_match_clause(&sp).where_.as_ref().unwrap();
match where_ {
Expr::Binary { lhs, op, rhs, .. } => {
assert!(matches!(op, BinaryOp::In));
assert!(matches!(lhs.as_ref(), Expr::Property { key, .. } if key == "age"));
assert!(matches!(rhs.as_ref(), Expr::List(_, _)));
}
other => panic!("expected IN expression, got {other:?}"),
}
}
#[test]
fn parse_where_contains_operator() {
let doc = parse_query("MATCH (n) WHERE n.name CONTAINS 'al' RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let where_ = first_match_clause(&sp).where_.as_ref().unwrap();
match where_ {
Expr::Binary { op, rhs, .. } => {
assert!(matches!(op, BinaryOp::Contains));
assert!(matches!(rhs.as_ref(), Expr::String(s, _) if s == "al"));
}
other => panic!("expected CONTAINS expression, got {other:?}"),
}
}
#[test]
fn parse_where_starts_with_operator() {
let doc = parse_query("MATCH (n) WHERE n.name STARTS WITH 'a' RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let where_ = first_match_clause(&sp).where_.as_ref().unwrap();
match where_ {
Expr::Binary { op, rhs, .. } => {
assert!(matches!(op, BinaryOp::StartsWith));
assert!(matches!(rhs.as_ref(), Expr::String(s, _) if s == "a"));
}
other => panic!("expected STARTS WITH expression, got {other:?}"),
}
}
#[test]
fn parse_where_ends_with_operator() {
let doc = parse_query("MATCH (n) WHERE n.name ENDS WITH 'z' RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let where_ = first_match_clause(&sp).where_.as_ref().unwrap();
match where_ {
Expr::Binary { op, rhs, .. } => {
assert!(matches!(op, BinaryOp::EndsWith));
assert!(matches!(rhs.as_ref(), Expr::String(s, _) if s == "z"));
}
other => panic!("expected ENDS WITH expression, got {other:?}"),
}
}
#[test]
fn parse_where_is_null() {
let doc = parse_query("MATCH (n) WHERE n.name IS NULL RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let where_ = first_match_clause(&sp).where_.as_ref().unwrap();
match where_ {
Expr::Binary { lhs, op, rhs, .. } => {
assert!(matches!(op, BinaryOp::IsNull));
assert!(matches!(lhs.as_ref(), Expr::Property { key, .. } if key == "name"));
assert!(matches!(rhs.as_ref(), Expr::Null(_)));
}
other => panic!("expected IS NULL expression, got {other:?}"),
}
}
#[test]
fn parse_where_is_not_null() {
let doc = parse_query("MATCH (n) WHERE n.name IS NOT NULL RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let where_ = first_match_clause(&sp).where_.as_ref().unwrap();
match where_ {
Expr::Binary { lhs, op, rhs, .. } => {
assert!(matches!(op, BinaryOp::IsNotNull));
assert!(matches!(lhs.as_ref(), Expr::Property { key, .. } if key == "name"));
assert!(matches!(rhs.as_ref(), Expr::Null(_)));
}
other => panic!("expected IS NOT NULL expression, got {other:?}"),
}
}
#[test]
fn parse_limit() {
let doc = parse_query("MATCH (n) RETURN n LIMIT 10").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert!(matches!(ret.body.limit, Some(Expr::Integer(10, _))));
assert!(ret.body.skip.is_none());
}
#[test]
fn parse_distinct_return() {
let doc = parse_query("MATCH (n) RETURN DISTINCT n").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert!(ret.body.distinct);
assert_eq!(ret.body.items.len(), 1);
}
#[test]
fn parse_return_star() {
let doc = parse_query("MATCH (n) RETURN *").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert_eq!(ret.body.items.len(), 1);
assert!(matches!(ret.body.items[0], ProjectionItem::Star { .. }));
}
#[test]
fn parse_return_star_and_expr() {
let doc = parse_query("MATCH (n) RETURN *, n.name").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert_eq!(ret.body.items.len(), 2);
assert!(matches!(ret.body.items[0], ProjectionItem::Star { .. }));
assert!(matches!(
&ret.body.items[1],
ProjectionItem::Expr {
expr: Expr::Property { key, .. },
..
} if key == "name"
));
}
#[test]
fn parse_multiple_projection_items() {
let doc = parse_query("MATCH (n) RETURN n, n.name AS name").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert_eq!(ret.body.items.len(), 2);
match &ret.body.items[0] {
ProjectionItem::Expr { expr, alias, .. } => {
assert!(alias.is_none());
assert!(matches!(expr, Expr::Variable(_)));
}
other => panic!("expected projection expr, got {other:?}"),
}
match &ret.body.items[1] {
ProjectionItem::Expr { expr, alias, .. } => {
assert_eq!(alias.as_ref().map(|v| v.name.as_str()), Some("name"));
match expr {
Expr::Property { key, .. } => assert_eq!(key, "name"),
other => panic!("expected property projection, got {other:?}"),
}
}
other => panic!("expected projection expr, got {other:?}"),
}
}
#[test]
fn parse_order_skip_limit() {
let doc = parse_query("MATCH (n) RETURN n ORDER BY n.name DESC SKIP 5 LIMIT 10").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert_eq!(ret.body.order.len(), 1);
assert!(matches!(ret.body.order[0].direction, SortDirection::Desc));
assert!(matches!(ret.body.skip, Some(Expr::Integer(5, _))));
assert!(matches!(ret.body.limit, Some(Expr::Integer(10, _))));
match &ret.body.order[0].expr {
Expr::Property { key, .. } => assert_eq!(key, "name"),
other => panic!("expected ORDER BY property lookup, got {other:?}"),
}
}
#[test]
fn parse_order_multiple_sort_items() {
let doc = parse_query("MATCH (n) RETURN n ORDER BY n.last ASC, n.first DESC").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert_eq!(ret.body.order.len(), 2);
assert!(matches!(ret.body.order[0].direction, SortDirection::Asc));
assert!(matches!(ret.body.order[1].direction, SortDirection::Desc));
}
#[test]
fn parse_create_clause() {
let doc = parse_query("CREATE (n:User {name: 'alice'}) RETURN n").unwrap();
let sp = as_regular_single_part(doc);
assert_eq!(sp.updating_clauses.len(), 1);
let UpdatingClause::Create(create) = &sp.updating_clauses[0] else {
panic!("expected CREATE clause");
};
let PatternElement::NodeChain { head, chain, .. } = &create.pattern.parts[0].element else {
panic!("expected node chain");
};
assert!(chain.is_empty());
assert_eq!(head.labels[0][0], "User");
assert!(head.properties.is_some());
}
#[test]
fn parse_create_without_return() {
let doc = parse_query("CREATE (n:User)").unwrap();
let sp = as_regular_single_part(doc);
assert_eq!(sp.updating_clauses.len(), 1);
assert!(sp.return_clause.is_none());
}
#[test]
fn parse_merge_clause() {
let doc = parse_query("MERGE (n:User {id: 1}) RETURN n").unwrap();
let sp = as_regular_single_part(doc);
assert_eq!(sp.updating_clauses.len(), 1);
let UpdatingClause::Merge(merge) = &sp.updating_clauses[0] else {
panic!("expected MERGE clause");
};
let PatternElement::NodeChain { head, chain, .. } = &merge.pattern_part.element else {
panic!("expected node chain");
};
assert!(chain.is_empty());
assert_eq!(head.labels[0][0], "User");
assert!(head.properties.is_some());
}
#[test]
fn parse_merge_with_actions() {
let doc = parse_query(
"MERGE (n:User {id: 1}) ON MATCH SET n.name = 'alice' ON CREATE SET n:New RETURN n",
)
.unwrap();
let sp = as_regular_single_part(doc);
let UpdatingClause::Merge(merge) = &sp.updating_clauses[0] else {
panic!("expected MERGE clause");
};
assert_eq!(merge.actions.len(), 2);
assert!(merge.actions[0].on_match);
assert!(!merge.actions[1].on_match);
assert_eq!(merge.actions[0].set.items.len(), 1);
assert_eq!(merge.actions[1].set.items.len(), 1);
}
#[test]
fn parse_delete_clause() {
let doc = parse_query("MATCH (n) DELETE n").unwrap();
let sp = as_regular_single_part(doc);
let UpdatingClause::Delete(delete) = &sp.updating_clauses[0] else {
panic!("expected DELETE clause");
};
assert!(!delete.detach);
assert_eq!(delete.expressions.len(), 1);
assert!(matches!(delete.expressions[0], Expr::Variable(_)));
}
#[test]
fn parse_detach_delete_clause() {
let doc = parse_query("MATCH (n) DETACH DELETE n").unwrap();
let sp = as_regular_single_part(doc);
let UpdatingClause::Delete(delete) = &sp.updating_clauses[0] else {
panic!("expected DELETE clause");
};
assert!(delete.detach);
assert_eq!(delete.expressions.len(), 1);
}
#[test]
fn parse_set_variable_clause() {
let doc = parse_query("MATCH (n) SET n = {name: 'alice'} RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let UpdatingClause::Set(set) = &sp.updating_clauses[0] else {
panic!("expected SET clause");
};
assert_eq!(set.items.len(), 1);
match &set.items[0] {
SetItem::SetVariable {
variable, value, ..
} => {
assert_eq!(variable.name, "n");
assert!(matches!(value, Expr::Map(_, _)));
}
other => panic!("expected SetVariable, got {other:?}"),
}
}
#[test]
fn parse_set_property_clause() {
let doc = parse_query("MATCH (n) SET n.name = 'alice' RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let UpdatingClause::Set(set) = &sp.updating_clauses[0] else {
panic!("expected SET clause");
};
assert_eq!(set.items.len(), 1);
match &set.items[0] {
SetItem::SetProperty { target, value, .. } => {
assert!(matches!(target, Expr::Property { key, .. } if key == "name"));
assert!(matches!(value, Expr::String(s, _) if s == "alice"));
}
other => panic!("expected SetProperty, got {other:?}"),
}
}
#[test]
fn parse_set_mutate_variable_clause() {
let doc = parse_query("MATCH (n) SET n += {age: 42} RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let UpdatingClause::Set(set) = &sp.updating_clauses[0] else {
panic!("expected SET clause");
};
assert_eq!(set.items.len(), 1);
match &set.items[0] {
SetItem::MutateVariable {
variable, value, ..
} => {
assert_eq!(variable.name, "n");
assert!(matches!(value, Expr::Map(_, _)));
}
other => panic!("expected MutateVariable, got {other:?}"),
}
}
#[test]
fn parse_set_labels_clause() {
let doc = parse_query("MATCH (n) SET n:User:Admin RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let UpdatingClause::Set(set) = &sp.updating_clauses[0] else {
panic!("expected SET clause");
};
assert_eq!(set.items.len(), 1);
match &set.items[0] {
SetItem::SetLabels {
variable, labels, ..
} => {
assert_eq!(variable.name, "n");
assert_eq!(labels, &vec!["User".to_string(), "Admin".to_string()]);
}
other => panic!("expected SetLabels, got {other:?}"),
}
}
#[test]
fn parse_remove_labels_clause() {
let doc = parse_query("MATCH (n) REMOVE n:User:Admin RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let UpdatingClause::Remove(remove) = &sp.updating_clauses[0] else {
panic!("expected REMOVE clause");
};
assert_eq!(remove.items.len(), 1);
match &remove.items[0] {
RemoveItem::Labels {
variable, labels, ..
} => {
assert_eq!(variable.name, "n");
assert_eq!(labels, &vec!["User".to_string(), "Admin".to_string()]);
}
other => panic!("expected RemoveItem::Labels, got {other:?}"),
}
}
#[test]
fn parse_remove_property_clause() {
let doc = parse_query("MATCH (n) REMOVE n.name RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let UpdatingClause::Remove(remove) = &sp.updating_clauses[0] else {
panic!("expected REMOVE clause");
};
assert_eq!(remove.items.len(), 1);
match &remove.items[0] {
RemoveItem::Property { expr, .. } => {
assert!(matches!(expr, Expr::Property { key, .. } if key == "name"));
}
other => panic!("expected RemoveItem::Property, got {other:?}"),
}
}
#[test]
fn parse_node_properties_map() {
let doc = parse_query("MATCH (n:User {name: 'alice', age: 42}) RETURN n").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { head, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
match head.properties.as_ref().expect("expected node properties") {
Expr::Map(items, _) => {
assert_eq!(items.len(), 2);
assert_eq!(items[0].0, "name");
assert!(matches!(items[0].1, Expr::String(_, _)));
assert_eq!(items[1].0, "age");
assert!(matches!(items[1].1, Expr::Integer(42, _)));
}
other => panic!("expected map literal properties, got {other:?}"),
}
}
#[test]
fn parse_relationship_properties_map() {
let doc = parse_query("MATCH (a)-[:FOLLOWS {since: 2020}]->(b) RETURN a").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let PatternElement::NodeChain { chain, .. } = &m.pattern.parts[0].element else {
panic!("expected node chain");
};
let rel = chain[0]
.relationship
.detail
.as_ref()
.expect("expected relationship detail");
match rel
.properties
.as_ref()
.expect("expected relationship properties")
{
Expr::Map(items, _) => {
assert_eq!(items.len(), 1);
assert_eq!(items[0].0, "since");
assert!(matches!(items[0].1, Expr::Integer(2020, _)));
}
other => panic!("expected relationship map properties, got {other:?}"),
}
}
#[test]
fn parse_unwind_clause() {
let doc = parse_query("UNWIND [1, 2, 3] AS n RETURN n").unwrap();
let sp = as_regular_single_part(doc);
assert_eq!(sp.reading_clauses.len(), 1);
let ReadingClause::Unwind(unwind) = &sp.reading_clauses[0] else {
panic!("expected UNWIND clause");
};
assert_eq!(unwind.alias.name, "n");
match &unwind.expr {
Expr::List(items, _) => {
assert_eq!(items.len(), 3);
assert!(matches!(items[0], Expr::Integer(1, _)));
assert!(matches!(items[1], Expr::Integer(2, _)));
assert!(matches!(items[2], Expr::Integer(3, _)));
}
other => panic!("expected list expression, got {other:?}"),
}
}
#[test]
fn parse_unary_operators() {
let doc = parse_query("RETURN -1, +2").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert_eq!(ret.body.items.len(), 2);
match &ret.body.items[0] {
ProjectionItem::Expr { expr, .. } => match expr {
Expr::Unary {
op: UnaryOp::Neg,
expr,
..
} => assert!(matches!(expr.as_ref(), Expr::Integer(1, _))),
other => panic!("expected unary negation, got {other:?}"),
},
other => panic!("expected projection expr, got {other:?}"),
}
match &ret.body.items[1] {
ProjectionItem::Expr { expr, .. } => match expr {
Expr::Unary {
op: UnaryOp::Pos,
expr,
..
} => assert!(matches!(expr.as_ref(), Expr::Integer(2, _))),
other => panic!("expected unary positive, got {other:?}"),
},
other => panic!("expected projection expr, got {other:?}"),
}
}
#[test]
fn parse_power_operator() {
let doc = parse_query("RETURN 2 ^ 3 ^ 4").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::Binary { lhs, op, rhs, .. } => {
assert!(matches!(op, BinaryOp::Pow));
assert!(matches!(rhs.as_ref(), Expr::Integer(4, _)));
assert!(matches!(
lhs.as_ref(),
Expr::Binary {
op: BinaryOp::Pow,
..
}
));
}
other => panic!("expected power expression, got {other:?}"),
}
}
#[test]
fn parse_function_call_and_alias() {
let doc = parse_query("MATCH (n) RETURN count(n) AS c").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert_eq!(ret.body.items.len(), 1);
let ProjectionItem::Expr { expr, alias, .. } = &ret.body.items[0] else {
panic!("expected projection expr");
};
assert_eq!(alias.as_ref().map(|v| v.name.as_str()), Some("c"));
match expr {
Expr::FunctionCall {
name,
distinct,
args,
..
} => {
assert_eq!(name, &vec!["count".to_string()]);
assert!(!distinct);
assert_eq!(args.len(), 1);
assert!(matches!(args[0], Expr::Variable(_)));
}
other => panic!("expected function call, got {other:?}"),
}
}
#[test]
fn parse_distinct_function_call() {
let doc = parse_query("MATCH (n) RETURN count(DISTINCT n) AS c").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
let ProjectionItem::Expr { expr, .. } = &ret.body.items[0] else {
panic!("expected projection expr");
};
match expr {
Expr::FunctionCall {
name,
distinct,
args,
..
} => {
assert_eq!(name, &vec!["count".to_string()]);
assert!(*distinct);
assert_eq!(args.len(), 1);
}
other => panic!("expected function call, got {other:?}"),
}
}
#[test]
fn parse_namespaced_function_call() {
let doc = parse_query("RETURN my.ns.func(1, 2)").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::FunctionCall { name, args, .. } => {
assert_eq!(
name,
&vec!["my".to_string(), "ns".to_string(), "func".to_string()]
);
assert_eq!(args.len(), 2);
assert!(matches!(args[0], Expr::Integer(1, _)));
assert!(matches!(args[1], Expr::Integer(2, _)));
}
other => panic!("expected namespaced function call, got {other:?}"),
}
}
#[test]
fn parse_parenthesized_type_cast() {
let doc = parse_query("RETURN ('42' AS INTEGER) AS n").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::TypeCast {
expr,
target,
try_cast,
..
} => {
assert!(!try_cast);
assert!(matches!(expr.as_ref(), Expr::String(s, _) if s == "42"));
assert!(matches!(
target,
LiteralTypeExpr::Named { name, .. } if name == "INTEGER"
));
}
other => panic!("expected type cast expression, got {other:?}"),
}
}
#[test]
fn parse_duckdb_cast_call_expression() {
let doc = parse_query("RETURN CAST('42' AS INTEGER) AS n").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::TypeCast {
expr,
target,
try_cast,
..
} => {
assert!(!try_cast);
assert!(matches!(expr.as_ref(), Expr::String(s, _) if s == "42"));
assert!(matches!(
target,
LiteralTypeExpr::Named { name, .. } if name == "INTEGER"
));
}
other => panic!("expected type cast expression, got {other:?}"),
}
}
#[test]
fn parse_duckdb_try_cast_call_expression() {
let doc = parse_query("RETURN TRY_CAST('bad' AS INTEGER) AS n").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::TypeCast {
expr,
target,
try_cast,
..
} => {
assert!(try_cast);
assert!(matches!(expr.as_ref(), Expr::String(s, _) if s == "bad"));
assert!(matches!(
target,
LiteralTypeExpr::Named { name, .. } if name == "INTEGER"
));
}
other => panic!("expected try cast expression, got {other:?}"),
}
}
#[test]
fn parse_duckdb_postfix_type_cast() {
let doc = parse_query("RETURN '42'::INTEGER AS n").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::TypeCast {
expr,
target,
try_cast,
..
} => {
assert!(!try_cast);
assert!(matches!(expr.as_ref(), Expr::String(s, _) if s == "42"));
assert!(matches!(
target,
LiteralTypeExpr::Named { name, .. } if name == "INTEGER"
));
}
other => panic!("expected postfix type cast expression, got {other:?}"),
}
}
#[test]
fn parse_vector_type_cast() {
let doc = parse_query("RETURN ([1, 2, 3] AS VECTOR<INTEGER>(3)) AS v").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::TypeCast { expr, target, .. } => {
assert!(matches!(expr.as_ref(), Expr::List(_, _)));
assert!(matches!(
target,
LiteralTypeExpr::Vector {
coordinate,
dimension: 3,
..
} if coordinate == "INTEGER"
));
}
other => panic!("expected vector type cast expression, got {other:?}"),
}
}
#[test]
fn parse_type_cast_keeps_inner_expression_shape() {
let doc = parse_query("RETURN (1 + 2 AS STRING) AS s").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::TypeCast { expr, target, .. } => {
assert!(matches!(
expr.as_ref(),
Expr::Binary {
op: BinaryOp::Add,
..
}
));
assert!(matches!(
target,
LiteralTypeExpr::Named { name, .. } if name == "STRING"
));
}
other => panic!("expected type cast expression, got {other:?}"),
}
}
#[test]
fn parse_temporal_keyword_type_cast() {
let doc = parse_query("RETURN ('2024-01-15T10:30:00' AS LOCAL DATETIME) AS dt").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::TypeCast { target, .. } => {
assert!(matches!(
target,
LiteralTypeExpr::Named { name, .. } if name == "LOCAL_DATETIME"
));
}
other => panic!("expected type cast expression, got {other:?}"),
}
}
#[test]
fn parse_nested_list_type_cast() {
let doc = parse_query("RETURN ([[1], [2]] AS LIST<LIST<INTEGER>>) AS xs").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::TypeCast { expr, target, .. } => {
assert!(matches!(expr.as_ref(), Expr::List(_, _)));
let LiteralTypeExpr::List { inner, .. } = target else {
panic!("expected outer LIST target, got {target:?}");
};
let LiteralTypeExpr::List { inner, .. } = inner.as_ref() else {
panic!("expected inner LIST target, got {inner:?}");
};
assert!(matches!(
inner.as_ref(),
LiteralTypeExpr::Named { name, .. } if name == "INTEGER"
));
}
other => panic!("expected type cast expression, got {other:?}"),
}
}
#[test]
fn parse_vector_type_cast_canonicalizes_runtime_aliases() {
let doc = parse_query("RETURN ([1.0, 2.0] AS VECTOR<FLOAT>(2)) AS v").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::TypeCast { target, .. } => {
assert!(matches!(
target,
LiteralTypeExpr::Vector {
coordinate,
dimension: 2,
..
} if coordinate == "FLOAT64"
));
}
other => panic!("expected vector type cast expression, got {other:?}"),
}
}
#[test]
fn parse_parameter_and_property_lookup() {
let doc = parse_query("MATCH (n) WHERE n.age >= $minAge RETURN n.name").unwrap();
let sp = as_regular_single_part(doc);
let m = first_match_clause(&sp);
let where_ = m.where_.as_ref().expect("expected WHERE clause");
match where_ {
Expr::Binary { lhs, op, rhs, .. } => {
assert!(matches!(op, BinaryOp::Ge));
match lhs.as_ref() {
Expr::Property { key, .. } => assert_eq!(key, "age"),
other => panic!("expected property lookup on lhs, got {other:?}"),
}
match rhs.as_ref() {
Expr::Parameter(name, _) => assert_eq!(name, "minAge"),
other => panic!("expected parameter on rhs, got {other:?}"),
}
}
other => panic!("expected binary WHERE expression, got {other:?}"),
}
let ret = sp.return_clause.expect("expected RETURN clause");
let ProjectionItem::Expr { expr, .. } = &ret.body.items[0] else {
panic!("expected projection expr");
};
match expr {
Expr::Property { key, .. } => assert_eq!(key, "name"),
other => panic!("expected property lookup in RETURN, got {other:?}"),
}
}
#[test]
fn parse_numeric_parameter() {
let doc = parse_query("RETURN $1").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::Parameter(name, _) => assert_eq!(name, "1"),
other => panic!("expected numeric parameter, got {other:?}"),
}
}
#[test]
fn parse_map_and_list_literals() {
let doc = parse_query("RETURN {name: 'alice', nums: [1, 2, 3]}").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::Map(items, _) => {
assert_eq!(items.len(), 2);
assert_eq!(items[0].0, "name");
assert!(matches!(items[0].1, Expr::String(_, _)));
assert_eq!(items[1].0, "nums");
match &items[1].1 {
Expr::List(values, _) => {
assert_eq!(values.len(), 3);
assert!(matches!(values[0], Expr::Integer(1, _)));
assert!(matches!(values[1], Expr::Integer(2, _)));
assert!(matches!(values[2], Expr::Integer(3, _)));
}
other => panic!("expected nested list, got {other:?}"),
}
}
other => panic!("expected map literal, got {other:?}"),
}
}
#[test]
fn parse_case_expression() {
let doc = parse_query("RETURN CASE WHEN 1 = 1 THEN 'yes' ELSE 'no' END").unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::Case {
input,
alternatives,
else_expr,
..
} => {
assert!(input.is_none());
assert_eq!(alternatives.len(), 1);
assert!(else_expr.is_some());
}
other => panic!("expected CASE expression, got {other:?}"),
}
}
#[test]
fn parse_case_expression_with_input() {
let doc = parse_query("RETURN CASE n.age WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'other' END")
.unwrap();
let sp = as_regular_single_part(doc);
match first_return_expr(&sp) {
Expr::Case {
input,
alternatives,
else_expr,
..
} => {
assert!(input.is_some());
assert_eq!(alternatives.len(), 2);
assert!(else_expr.is_some());
}
other => panic!("expected CASE expression, got {other:?}"),
}
}
#[test]
fn parse_literals() {
let doc = parse_query("RETURN 42, 3.14, true, false, null, 'x'").unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert_eq!(ret.body.items.len(), 6);
match &ret.body.items[0] {
ProjectionItem::Expr { expr, .. } => assert!(matches!(expr, Expr::Integer(42, _))),
other => panic!("expected projection expr, got {other:?}"),
}
match &ret.body.items[1] {
ProjectionItem::Expr { expr, .. } => assert!(matches!(expr, Expr::Float(_, _))),
other => panic!("expected projection expr, got {other:?}"),
}
match &ret.body.items[2] {
ProjectionItem::Expr { expr, .. } => assert!(matches!(expr, Expr::Bool(true, _))),
other => panic!("expected projection expr, got {other:?}"),
}
match &ret.body.items[3] {
ProjectionItem::Expr { expr, .. } => assert!(matches!(expr, Expr::Bool(false, _))),
other => panic!("expected projection expr, got {other:?}"),
}
match &ret.body.items[4] {
ProjectionItem::Expr { expr, .. } => assert!(matches!(expr, Expr::Null(_))),
other => panic!("expected projection expr, got {other:?}"),
}
match &ret.body.items[5] {
ProjectionItem::Expr { expr, .. } => match expr {
Expr::String(s, _) => assert_eq!(s, "x"),
other => panic!("expected string literal, got {other:?}"),
},
other => panic!("expected projection expr, got {other:?}"),
}
}
#[test]
fn parse_string_escapes() {
let doc = parse_query(r#"RETURN "a\nb", 'it\'s', "\\""#).unwrap();
let sp = as_regular_single_part(doc);
let ret = sp.return_clause.expect("expected RETURN clause");
assert_eq!(ret.body.items.len(), 3);
match &ret.body.items[0] {
ProjectionItem::Expr {
expr: Expr::String(s, _),
..
} => assert_eq!(s, "a\nb"),
other => panic!("expected escaped string, got {other:?}"),
}
match &ret.body.items[1] {
ProjectionItem::Expr {
expr: Expr::String(s, _),
..
} => assert_eq!(s, "it's"),
other => panic!("expected escaped string, got {other:?}"),
}
match &ret.body.items[2] {
ProjectionItem::Expr {
expr: Expr::String(s, _),
..
} => assert_eq!(s, "\\"),
other => panic!("expected escaped string, got {other:?}"),
}
}
#[test]
fn parse_union_query() {
let doc = parse_query("MATCH (a) RETURN a UNION MATCH (b) RETURN b").unwrap();
let Statement::Query(Query::Regular(rq)) = doc.statement else {
panic!("expected regular query");
};
assert_eq!(rq.unions.len(), 1);
assert!(!rq.unions[0].all);
let SingleQuery::SinglePart(head) = rq.head else {
panic!("expected single-part head");
};
let head_ret = head.return_clause.expect("expected head return");
assert_eq!(head_ret.body.items.len(), 1);
let SingleQuery::SinglePart(union_q) = &rq.unions[0].query else {
panic!("expected single-part union");
};
let union_ret = union_q
.return_clause
.as_ref()
.expect("expected union return");
assert_eq!(union_ret.body.items.len(), 1);
}
#[test]
fn parse_union_all_query() {
let doc = parse_query("MATCH (a) RETURN a UNION ALL MATCH (b) RETURN b").unwrap();
let Statement::Query(Query::Regular(rq)) = doc.statement else {
panic!("expected regular query");
};
assert_eq!(rq.unions.len(), 1);
assert!(rq.unions[0].all);
}
#[test]
fn parse_with_clause_in_multi_part_query() {
let doc = parse_query("MATCH (n) WITH n RETURN n").unwrap();
let mp = as_regular_multi_part(doc);
assert_eq!(mp.parts.len(), 1);
assert_eq!(mp.parts[0].reading_clauses.len(), 1);
assert!(mp.parts[0].updating_clauses.is_empty());
assert_eq!(mp.parts[0].with_clause.body.items.len(), 1);
assert!(mp.parts[0].with_clause.where_.is_none());
assert!(mp.tail.return_clause.is_some());
}
#[test]
fn parse_with_where_clause_in_multi_part_query() {
let doc = parse_query("MATCH (n) WITH n WHERE n.age >= 18 RETURN n").unwrap();
let mp = as_regular_multi_part(doc);
let where_ = mp.parts[0]
.with_clause
.where_
.as_ref()
.expect("expected WITH WHERE clause");
match where_ {
Expr::Binary { op, .. } => assert!(matches!(op, BinaryOp::Ge)),
other => panic!("expected binary expression, got {other:?}"),
}
}
#[test]
fn parse_multi_part_with_update() {
let doc = parse_query("MATCH (n) SET n:Seen WITH n RETURN n").unwrap();
let mp = as_regular_multi_part(doc);
assert_eq!(mp.parts.len(), 1);
assert_eq!(mp.parts[0].reading_clauses.len(), 1);
assert_eq!(mp.parts[0].updating_clauses.len(), 1);
assert!(mp.tail.return_clause.is_some());
}
#[test]
fn parse_standalone_call_explicit() {
let doc = parse_query("CALL db.labels()").unwrap();
let call = as_standalone_call(doc);
match call.procedure {
ProcedureInvocationKind::Explicit(proc_) => {
assert_eq!(
proc_.name.parts,
vec!["db".to_string(), "labels".to_string()]
);
assert!(proc_.args.is_empty());
}
other => panic!("expected explicit procedure invocation, got {other:?}"),
}
assert!(call.yield_items.is_empty());
assert!(!call.yield_all);
}
#[test]
fn parse_standalone_call_implicit() {
let doc = parse_query("CALL db.labels").unwrap();
let call = as_standalone_call(doc);
match call.procedure {
ProcedureInvocationKind::Implicit(name) => {
assert_eq!(name.parts, vec!["db".to_string(), "labels".to_string()]);
}
other => panic!("expected implicit procedure name, got {other:?}"),
}
}
#[test]
fn parse_standalone_call_yield_all() {
let doc = parse_query("CALL db.labels() YIELD *").unwrap();
let call = as_standalone_call(doc);
assert!(call.yield_all);
assert!(call.yield_items.is_empty());
}
#[test]
fn parse_standalone_call_yield_items() {
let doc = parse_query("CALL db.labels() YIELD label, value AS v").unwrap();
let call = as_standalone_call(doc);
assert!(!call.yield_all);
assert_eq!(call.yield_items.len(), 2);
assert_eq!(call.yield_items[0].field, None);
assert_eq!(call.yield_items[0].alias.name, "label");
assert_eq!(call.yield_items[1].field.as_deref(), Some("value"));
assert_eq!(call.yield_items[1].alias.name, "v");
}
#[test]
fn parse_in_query_call() {
let doc = parse_query("CALL db.labels() YIELD label RETURN label").unwrap();
let sp = as_regular_single_part(doc);
assert_eq!(sp.reading_clauses.len(), 1);
let ReadingClause::InQueryCall(call) = &sp.reading_clauses[0] else {
panic!("expected in-query CALL");
};
assert_eq!(
call.procedure.name.parts,
vec!["db".to_string(), "labels".to_string()]
);
assert_eq!(call.yield_items.len(), 1);
assert!(call.where_.is_none());
}
#[test]
fn parse_in_query_call_with_where() {
let doc =
parse_query("CALL db.labels() YIELD label WHERE label IS NOT NULL RETURN label").unwrap();
let sp = as_regular_single_part(doc);
let ReadingClause::InQueryCall(call) = &sp.reading_clauses[0] else {
panic!("expected in-query CALL");
};
let where_ = call.where_.as_ref().expect("expected WHERE on CALL");
match where_ {
Expr::Binary { op, .. } => assert!(matches!(op, BinaryOp::IsNotNull)),
other => panic!("expected binary WHERE expression, got {other:?}"),
}
}
#[test]
fn parse_semicolon() {
let doc = parse_query("MATCH (n) RETURN n;").unwrap();
let sp = as_regular_single_part(doc);
assert!(sp.return_clause.is_some());
}
fn as_schema_command(doc: Document) -> SchemaCommand {
let Statement::Schema(cmd) = doc.statement else {
panic!("expected schema command");
};
cmd
}
#[test]
fn parse_create_range_index_default_kind() {
let doc =
parse_query("CREATE INDEX node_range_index_name FOR (n:Person) ON (n.surname)").unwrap();
let SchemaCommand::CreateIndex(ci) = as_schema_command(doc) else {
panic!("expected create index");
};
assert_eq!(ci.kind, IndexKind::Range);
assert_eq!(ci.entity, IndexEntityKind::Node);
assert_eq!(ci.label.as_deref(), Some("Person"));
assert_eq!(ci.properties, vec!["surname"]);
assert!(!ci.if_not_exists);
assert!(matches!(
ci.name,
Some(IndexNameSpec::Literal(ref n)) if n == "node_range_index_name"
));
}
#[test]
fn parse_create_text_index_relationship() {
let doc =
parse_query("CREATE TEXT INDEX rel_text_index_name FOR ()-[r:KNOWS]-() ON (r.interest)")
.unwrap();
let SchemaCommand::CreateIndex(ci) = as_schema_command(doc) else {
panic!("expected create index");
};
assert_eq!(ci.kind, IndexKind::Text);
assert_eq!(ci.entity, IndexEntityKind::Relationship);
assert_eq!(ci.label.as_deref(), Some("KNOWS"));
assert_eq!(ci.properties, vec!["interest"]);
}
#[test]
fn parse_create_lookup_index_node() {
let doc =
parse_query("CREATE LOOKUP INDEX node_label_lookup_index FOR (n) ON EACH node.labels(n)")
.unwrap();
let SchemaCommand::CreateIndex(ci) = as_schema_command(doc) else {
panic!("expected create index");
};
assert_eq!(ci.kind, IndexKind::Lookup);
assert!(ci.label.is_none());
assert!(ci.properties.is_empty());
}
#[test]
fn parse_create_index_if_not_exists() {
let doc = parse_query(
"CREATE INDEX node_range_index_name IF NOT EXISTS FOR (n:Person) ON (n.surname)",
)
.unwrap();
let SchemaCommand::CreateIndex(ci) = as_schema_command(doc) else {
panic!("expected create index");
};
assert!(ci.if_not_exists);
}
#[test]
fn parse_create_composite_index() {
let doc = parse_query("CREATE INDEX comp FOR (n:Person) ON (n.age, n.country)").unwrap();
let SchemaCommand::CreateIndex(ci) = as_schema_command(doc) else {
panic!("expected create index");
};
assert_eq!(ci.properties, vec!["age", "country"]);
}
#[test]
fn parse_create_index_with_parameter_name() {
let doc = parse_query("CREATE INDEX $name FOR (n:Person) ON (n.firstname)").unwrap();
let SchemaCommand::CreateIndex(ci) = as_schema_command(doc) else {
panic!("expected create index");
};
assert!(matches!(
ci.name,
Some(IndexNameSpec::Parameter(ref n)) if n == "name"
));
}
#[test]
fn parse_show_indexes() {
let doc = parse_query("SHOW INDEXES").unwrap();
assert!(matches!(
doc.statement,
Statement::Schema(SchemaCommand::ShowIndexes(_))
));
}
#[test]
fn parse_drop_index_named() {
let doc = parse_query("DROP INDEX my_index").unwrap();
let Statement::Schema(SchemaCommand::DropIndex(di)) = doc.statement else {
panic!("expected drop index");
};
assert!(matches!(di.name, IndexNameSpec::Literal(ref n) if n == "my_index"));
assert!(!di.if_exists);
}
#[test]
fn parse_drop_index_if_exists() {
let doc = parse_query("DROP INDEX my_index IF EXISTS").unwrap();
let Statement::Schema(SchemaCommand::DropIndex(di)) = doc.statement else {
panic!("expected drop index");
};
assert!(di.if_exists);
}
#[test]
fn parse_create_unique_constraint_node_single() {
let doc =
parse_query("CREATE CONSTRAINT book_isbn FOR (book:Book) REQUIRE book.isbn IS UNIQUE")
.unwrap();
let SchemaCommand::CreateConstraint(cc) = as_schema_command(doc) else {
panic!("expected create constraint");
};
assert!(matches!(cc.name, ConstraintNameSpec::Literal(ref n) if n == "book_isbn"));
assert_eq!(cc.entity, IndexEntityKind::Node);
assert_eq!(cc.label, "Book");
assert_eq!(cc.properties, vec!["isbn"]);
assert!(matches!(cc.kind, ConstraintKind::Unique));
assert!(!cc.if_not_exists);
}
#[test]
fn parse_create_unique_constraint_composite_relationship() {
let doc = parse_query(
"CREATE CONSTRAINT prequels FOR ()-[seq:SEQUEL_OF]-() REQUIRE (seq.order, seq.author) IS UNIQUE",
)
.unwrap();
let SchemaCommand::CreateConstraint(cc) = as_schema_command(doc) else {
panic!("expected create constraint");
};
assert_eq!(cc.entity, IndexEntityKind::Relationship);
assert_eq!(cc.label, "SEQUEL_OF");
assert_eq!(cc.properties, vec!["order", "author"]);
assert!(matches!(cc.kind, ConstraintKind::Unique));
}
#[test]
fn parse_create_existence_constraint_node() {
let doc =
parse_query("CREATE CONSTRAINT author_name FOR (a:Author) REQUIRE a.name IS NOT NULL")
.unwrap();
let SchemaCommand::CreateConstraint(cc) = as_schema_command(doc) else {
panic!("expected create constraint");
};
assert!(matches!(cc.kind, ConstraintKind::Existence));
assert_eq!(cc.properties, vec!["name"]);
}
#[test]
fn parse_create_existence_constraint_composite_rejected() {
let err = parse_query("CREATE CONSTRAINT bad FOR (a:Author) REQUIRE (a.x, a.y) IS NOT NULL")
.unwrap_err();
assert!(format!("{err}").contains("exactly one property"));
}
#[test]
fn parse_create_node_key_constraint_composite() {
let doc = parse_query(
"CREATE CONSTRAINT actor_fullname FOR (a:Actor) REQUIRE (a.firstname, a.surname) IS NODE KEY",
)
.unwrap();
let SchemaCommand::CreateConstraint(cc) = as_schema_command(doc) else {
panic!("expected create constraint");
};
assert!(matches!(cc.kind, ConstraintKind::NodeKey));
assert_eq!(cc.properties, vec!["firstname", "surname"]);
}
#[test]
fn parse_create_relationship_key_constraint() {
let doc = parse_query(
"CREATE CONSTRAINT ownership FOR ()-[o:OWNS]-() REQUIRE o.ownershipId IS RELATIONSHIP KEY",
)
.unwrap();
let SchemaCommand::CreateConstraint(cc) = as_schema_command(doc) else {
panic!("expected create constraint");
};
assert!(matches!(cc.kind, ConstraintKind::RelationshipKey));
}
#[test]
fn parse_create_property_type_constraint_string() {
let doc =
parse_query("CREATE CONSTRAINT movie_title FOR (m:Movie) REQUIRE m.title IS :: STRING")
.unwrap();
let SchemaCommand::CreateConstraint(cc) = as_schema_command(doc) else {
panic!("expected create constraint");
};
let ConstraintKind::PropertyType(t) = cc.kind else {
panic!("expected property type constraint");
};
assert_eq!(t.alternatives.len(), 1);
assert!(matches!(
t.alternatives[0],
PropertyTypeTerm::Scalar(ScalarType::String)
));
}
#[test]
fn parse_create_property_type_constraint_union() {
let doc = parse_query(
"CREATE CONSTRAINT t FOR (m:Movie) REQUIRE m.tagline IS :: STRING | LIST<STRING NOT NULL>",
)
.unwrap();
let SchemaCommand::CreateConstraint(cc) = as_schema_command(doc) else {
panic!("expected create constraint");
};
let ConstraintKind::PropertyType(t) = cc.kind else {
panic!("expected property type constraint");
};
assert_eq!(t.alternatives.len(), 2);
assert!(matches!(
t.alternatives[0],
PropertyTypeTerm::Scalar(ScalarType::String)
));
assert!(matches!(
t.alternatives[1],
PropertyTypeTerm::List { not_null: true, .. }
));
}
#[test]
fn parse_create_property_type_constraint_vector() {
let doc = parse_query(
"CREATE CONSTRAINT v FOR (n:Movie) REQUIRE n.embedding IS :: VECTOR<INT32>(42)",
)
.unwrap();
let SchemaCommand::CreateConstraint(cc) = as_schema_command(doc) else {
panic!("expected create constraint");
};
let ConstraintKind::PropertyType(t) = cc.kind else {
panic!("expected property type constraint");
};
assert!(matches!(
t.alternatives[0],
PropertyTypeTerm::Vector {
coord: VectorCoordType::Int32,
dimension: 42,
}
));
}
#[test]
fn parse_create_constraint_if_not_exists() {
let doc =
parse_query("CREATE CONSTRAINT x IF NOT EXISTS FOR (n:Book) REQUIRE n.isbn IS UNIQUE")
.unwrap();
let SchemaCommand::CreateConstraint(cc) = as_schema_command(doc) else {
panic!("expected create constraint");
};
assert!(cc.if_not_exists);
}
#[test]
fn parse_create_constraint_parameterized_name() {
let doc = parse_query("CREATE CONSTRAINT $name FOR (n:Book) REQUIRE n.isbn IS UNIQUE").unwrap();
let SchemaCommand::CreateConstraint(cc) = as_schema_command(doc) else {
panic!("expected create constraint");
};
assert!(matches!(cc.name, ConstraintNameSpec::Parameter(ref n) if n == "name"));
}
#[test]
fn parse_drop_constraint() {
let doc = parse_query("DROP CONSTRAINT my_constraint").unwrap();
let Statement::Schema(SchemaCommand::DropConstraint(dc)) = doc.statement else {
panic!("expected drop constraint");
};
assert!(matches!(dc.name, ConstraintNameSpec::Literal(ref n) if n == "my_constraint"));
assert!(!dc.if_exists);
}
#[test]
fn parse_drop_constraint_if_exists() {
let doc = parse_query("DROP CONSTRAINT my_constraint IF EXISTS").unwrap();
let Statement::Schema(SchemaCommand::DropConstraint(dc)) = doc.statement else {
panic!("expected drop constraint");
};
assert!(dc.if_exists);
}
#[test]
fn parse_show_constraints() {
let doc = parse_query("SHOW CONSTRAINTS").unwrap();
assert!(matches!(
doc.statement,
Statement::Schema(SchemaCommand::ShowConstraints(_))
));
}
#[test]
fn parse_invalid_node_key_on_relationship_rejected() {
let err =
parse_query("CREATE CONSTRAINT x FOR ()-[r:R]-() REQUIRE r.a IS NODE KEY").unwrap_err();
assert!(format!("{err}").contains("NODE KEY can only be used on nodes"));
}
#[test]
fn parse_invalid_composite_must_be_parenthesized() {
let err = parse_query("CREATE CONSTRAINT x FOR (n:L) REQUIRE n.a, n.b IS UNIQUE").unwrap_err();
let _ = err;
}