use super::*;
#[test]
fn test_parse_simple_for_return() {
let query = parse("FOR doc IN users RETURN doc").unwrap();
assert_eq!(query.for_clauses.len(), 1);
assert!(query.return_clause.is_some());
}
#[test]
fn test_parse_for_filter_return() {
let query = parse("FOR doc IN users FILTER doc.age > 18 RETURN doc").unwrap();
assert_eq!(query.filter_clauses.len(), 1);
assert!(query.return_clause.is_some());
}
#[test]
fn test_parse_for_sort_limit_return() {
let query = parse("FOR doc IN users SORT doc.name ASC LIMIT 10 RETURN doc").unwrap();
assert!(query.sort_clause.is_some());
assert!(query.limit_clause.is_some());
}
#[test]
fn test_parse_insert() {
let query = parse("INSERT { name: \"Alice\" } INTO users").unwrap();
assert!(query
.body_clauses
.iter()
.any(|c| matches!(c, BodyClause::Insert(_))));
}
#[test]
fn test_parse_update() {
let query = parse("FOR doc IN users UPDATE doc WITH { active: true } IN users").unwrap();
assert!(query
.body_clauses
.iter()
.any(|c| matches!(c, BodyClause::Update(_))));
}
#[test]
fn test_parse_remove() {
let query = parse("FOR doc IN users REMOVE doc IN users").unwrap();
assert!(query
.body_clauses
.iter()
.any(|c| matches!(c, BodyClause::Remove(_))));
}
#[test]
fn test_parse_collect() {
let query = parse("FOR doc IN users COLLECT city = doc.city RETURN city").unwrap();
assert!(query
.body_clauses
.iter()
.any(|c| matches!(c, BodyClause::Collect(_))));
}
#[test]
fn test_parse_let_clause() {
let query = parse("LET x = 5 RETURN x").unwrap();
assert_eq!(query.let_clauses.len(), 1);
}
#[test]
fn test_parse_let_multiple_bindings() {
let query = parse("LET a = 1, b = 2, c = 3 RETURN a + b + c").unwrap();
assert_eq!(query.let_clauses.len(), 3);
assert_eq!(query.let_clauses[0].variable, "a");
assert_eq!(query.let_clauses[1].variable, "b");
assert_eq!(query.let_clauses[2].variable, "c");
}
#[test]
fn test_parse_let_multiple_in_body() {
let query = parse("FOR doc IN users LET x = doc.a, y = doc.b RETURN {x, y}").unwrap();
let let_count = query
.body_clauses
.iter()
.filter(|c| matches!(c, BodyClause::Let(_)))
.count();
assert_eq!(let_count, 2);
}
#[test]
fn test_parse_return_arithmetic() {
let query = parse("RETURN 1 + 2 * 3").unwrap();
assert!(query.return_clause.is_some());
let ret = query.return_clause.unwrap();
assert!(matches!(ret.expression, Expression::BinaryOp { .. }));
}
#[test]
fn test_parse_error_incomplete() {
let result = parse("FOR doc IN");
assert!(result.is_err());
}
#[test]
fn test_parse_error_invalid_token() {
let result = parse("FOR 123 IN users");
assert!(result.is_err());
}
#[test]
fn test_parse_sort_desc() {
let query = parse("FOR doc IN users SORT doc.age DESC RETURN doc").unwrap();
let sort = query.sort_clause.unwrap();
assert_eq!(sort.fields.len(), 1);
assert!(!sort.fields[0].1);
}
#[test]
fn test_parse_multiple_filters() {
let query = parse("FOR doc IN users FILTER doc.age > 18 FILTER doc.active RETURN doc").unwrap();
assert_eq!(query.filter_clauses.len(), 2);
}
#[test]
fn test_parse_nested_for() {
let query = parse("FOR a IN users FOR b IN orders RETURN { user: a, order: b }").unwrap();
assert_eq!(query.for_clauses.len(), 2);
}
#[test]
fn test_parse_not_in() {
let query = parse("FOR x IN collection FILTER x.id NOT IN [1, 2, 3] RETURN x").unwrap();
if let BodyClause::Filter(filter) = &query.body_clauses[1] {
if let Expression::BinaryOp { op, .. } = &filter.expression {
assert_eq!(*op, BinaryOperator::NotIn);
} else {
panic!("Expected BinaryOp::NotIn");
}
} else {
panic!("Expected FilterClause");
}
}
#[test]
fn test_parse_create_stream() {
let input = r#"
CREATE STREAM high_value_txns AS
FOR txn IN transactions
WINDOW TUMBLING (SIZE "1m")
FILTER txn.amount > 1000
RETURN txn
"#;
let mut parser = Parser::new(input).unwrap();
let query = parser.parse().unwrap();
assert!(query.create_stream_clause.is_some());
assert_eq!(query.create_stream_clause.unwrap().name, "high_value_txns");
assert!(query.window_clause.is_some());
assert_eq!(query.window_clause.unwrap().duration, "1m");
assert_eq!(query.for_clauses.len(), 1);
assert_eq!(query.for_clauses[0].collection, "transactions");
}
#[test]
fn test_any_syntax() {
let query = parse("FOR doc IN collection FILTER ANY member IN doc.members RETURN doc");
assert!(
query.is_ok(),
"Failed to parse ANY syntax: {:?}",
query.err()
);
}
#[test]
fn test_any_satisfies_syntax() {
let query = parse(
"FOR doc IN collection FILTER ANY member IN doc.members SATISFIES member.age > 10 RETURN doc",
);
assert!(
query.is_ok(),
"Failed to parse ANY ... SATISFIES syntax: {:?}",
query.err()
);
}
#[test]
fn test_cte_simple() {
let query = parse("WITH temp AS (FOR doc IN coll RETURN doc) FOR t IN temp RETURN t");
assert!(query.is_ok(), "Failed to parse CTE: {:?}", query.err());
let query = query.unwrap();
assert!(query.with_clause.is_some());
let with = query.with_clause.unwrap();
assert_eq!(with.ctes.len(), 1);
assert_eq!(with.ctes[0].name, "temp");
}
#[test]
fn test_cte_multiple() {
let query = parse(
"WITH a AS (FOR x IN coll RETURN x), b AS (FOR y IN coll2 RETURN y) FOR t IN a RETURN t",
);
assert!(
query.is_ok(),
"Failed to parse multiple CTEs: {:?}",
query.err()
);
let query = query.unwrap();
assert!(query.with_clause.is_some());
let with = query.with_clause.unwrap();
assert_eq!(with.ctes.len(), 2);
assert_eq!(with.ctes[0].name, "a");
assert_eq!(with.ctes[1].name, "b");
}
#[test]
fn test_cte_with_columns() {
let query =
parse("WITH temp(col1, col2) AS (FOR doc IN coll RETURN doc) FOR t IN temp RETURN t");
assert!(
query.is_ok(),
"Failed to parse CTE with columns: {:?}",
query.err()
);
let query = query.unwrap();
assert!(query.with_clause.is_some());
let with = query.with_clause.unwrap();
assert_eq!(with.ctes[0].columns, vec!["col1", "col2"]);
}
#[test]
fn test_no_cte() {
let query = parse("FOR doc IN coll RETURN doc").unwrap();
assert!(query.with_clause.is_none());
}
#[test]
fn test_recursive_cte() {
let query = parse(
"WITH RECURSIVE tree AS (FOR d IN nodes FILTER d._key == @root RETURN d._key \
UNION ALL FOR n IN nodes FILTER n.parent IN tree RETURN n._key) \
FOR x IN tree RETURN x",
);
assert!(
query.is_ok(),
"Failed to parse recursive CTE: {:?}",
query.err()
);
let query = query.unwrap();
let with = query.with_clause.unwrap();
assert_eq!(with.ctes.len(), 1);
assert!(with.ctes[0].recursive);
assert_eq!(with.ctes[0].query.set_operations.len(), 1);
}
#[test]
fn test_return_distinct() {
let query = parse("FOR doc IN coll RETURN DISTINCT doc.city").unwrap();
let rc = query.return_clause.unwrap();
assert!(rc.distinct);
let query = parse("FOR doc IN coll RETURN doc.city").unwrap();
assert!(!query.return_clause.unwrap().distinct);
}
#[test]
fn test_set_operations() {
for (sql, expected) in [
(
"FOR a IN c1 RETURN a.x UNION FOR b IN c2 RETURN b.y",
"Union",
),
(
"FOR a IN c1 RETURN a.x UNION ALL FOR b IN c2 RETURN b.y",
"UnionAll",
),
(
"FOR a IN c1 RETURN a.x INTERSECT FOR b IN c2 RETURN b.y",
"Intersect",
),
(
"FOR a IN c1 RETURN a.x EXCEPT FOR b IN c2 RETURN b.y",
"Except",
),
] {
let query = parse(sql).unwrap_or_else(|e| panic!("Failed to parse {sql}: {e:?}"));
assert_eq!(query.set_operations.len(), 1, "{sql}");
let op_name = format!("{:?}", query.set_operations[0].op);
assert_eq!(op_name, expected, "{sql}");
}
}
#[test]
fn test_set_operation_chain_is_flat_and_left_to_right() {
let query =
parse("FOR a IN c1 RETURN a.x EXCEPT FOR b IN c2 RETURN b.x EXCEPT FOR c IN c3 RETURN c.x")
.unwrap();
assert_eq!(query.set_operations.len(), 2);
assert!(query.set_operations[0].query.set_operations.is_empty());
}
#[test]
fn test_intersect_binds_tighter_than_union() {
let query = parse(
"FOR a IN c1 RETURN a.x UNION FOR b IN c2 RETURN b.x INTERSECT FOR c IN c3 RETURN c.x",
)
.unwrap();
assert_eq!(query.set_operations.len(), 1);
assert!(matches!(query.set_operations[0].op, SetOperator::Union));
let nested = &query.set_operations[0].query.set_operations;
assert_eq!(nested.len(), 1);
assert!(matches!(nested[0].op, SetOperator::Intersect));
}
#[test]
fn test_parenthesized_left_operand() {
let query = parse(
"(FOR a IN c1 RETURN a.x UNION FOR b IN c2 RETURN b.x) INTERSECT FOR c IN c3 RETURN c.x",
)
.unwrap();
assert_eq!(query.set_operations.len(), 2);
assert!(matches!(query.set_operations[0].op, SetOperator::Union));
assert!(matches!(query.set_operations[1].op, SetOperator::Intersect));
assert!(query.set_operations[0].query.set_operations.is_empty());
}
#[test]
fn test_offset_without_limit_has_no_count() {
let query = parse("FOR d IN coll OFFSET 5 RETURN d").unwrap();
let limit = query.limit_clause.expect("OFFSET produces a limit clause");
assert!(limit.count.is_none());
let query = parse("FOR d IN coll LIMIT 10 OFFSET 5 RETURN d").unwrap();
let limit = query.limit_clause.expect("limit clause");
assert_eq!(
limit.count,
Some(Expression::Literal(serde_json::json!(10)))
);
assert_eq!(limit.offset, Expression::Literal(serde_json::json!(5)));
}
#[test]
fn test_has_mutations_sees_nested_blocks() {
let query =
parse("FOR a IN c1 RETURN a.x UNION FOR d IN c2 REMOVE d IN c2 RETURN d._key").unwrap();
assert!(query.has_mutations());
let query =
parse("WITH gone AS (FOR d IN c2 REMOVE d IN c2 RETURN d._key) FOR x IN gone RETURN x")
.unwrap();
assert!(query.has_mutations());
let query = parse("FOR a IN c1 RETURN a.x UNION FOR b IN c2 RETURN b.x").unwrap();
assert!(!query.has_mutations());
}
#[test]
fn test_set_operations_parenthesized_operand() {
let query =
parse("FOR a IN c1 RETURN a.x UNION (FOR b IN c2 FILTER b.z > 1 RETURN b.y)").unwrap();
assert_eq!(query.set_operations.len(), 1);
assert!(matches!(query.set_operations[0].op, SetOperator::Union));
}
#[test]
fn test_collect_keep() {
let query = parse(
"FOR u IN users COLLECT city = u.city INTO groups KEEP name, age SORT city RETURN city",
)
.unwrap();
let collect = query
.body_clauses
.iter()
.find_map(|c| match c {
BodyClause::Collect(cc) => Some(cc.clone()),
_ => None,
})
.expect("COLLECT clause");
assert_eq!(collect.keep_vars, vec!["name", "age"]);
}
#[test]
fn test_parse_collect_with_aggregate_count() {
let query =
parse("FOR u IN users COLLECT city = u.city AGGREGATE count = COUNT() RETURN count");
assert!(query.is_ok(), "Failed to parse: {:?}", query.err());
}
#[test]
fn test_parse_collect_aggregate_no_group_var() {
let query = parse("FOR u IN users COLLECT AGGREGATE count = COUNT() RETURN count");
assert!(query.is_ok(), "Failed to parse: {:?}", query.err());
}
#[test]
fn test_unary_chain_is_depth_limited() {
for chain in ["!".repeat(500), "~".repeat(500), "- ".repeat(500)] {
let query = format!("RETURN {}5", chain);
let err = parse(&query).expect_err("chained unary should hit the depth limit");
assert!(
err.to_string().contains("too deep"),
"expected the depth guard, got: {}",
err
);
}
}
#[test]
fn test_short_unary_chains_still_parse() {
assert!(parse("RETURN -5").is_ok());
assert!(parse("RETURN - -5").is_ok());
assert!(parse("RETURN !!true").is_ok());
assert!(parse("RETURN ~~1").is_ok());
assert!(parse("FOR u IN users FILTER !u.deleted RETURN -u.score").is_ok());
let siblings = (0..200)
.map(|i| format!("-{}", i))
.collect::<Vec<_>>()
.join(", ");
assert!(
parse(&format!("RETURN [{}]", siblings)).is_ok(),
"sequential unary operands must not accumulate depth"
);
}
#[test]
fn test_has_mutations_sees_expression_level_writes() {
for query in [
"RETURN (FOR e IN c INSERT {} INTO c)",
"FOR d IN c LET y = (FOR e IN c REMOVE e IN c) RETURN y",
"FOR d IN c FILTER (FOR e IN c INSERT {} INTO c) RETURN d",
"FOR d IN c SORT (FOR e IN c REMOVE e IN c) RETURN d",
"FOR d IN c RETURN {nested: (FOR e IN c INSERT {} INTO c)}",
"FOR d IN c RETURN [1, (FOR e IN c INSERT {} INTO c)]",
"RETURN LENGTH((FOR e IN c INSERT {} INTO c))",
"RETURN true ? (FOR e IN c INSERT {} INTO c) : 1",
"RETURN CREATE_VIEW(\"v\", {})",
"RETURN DROP_VIEW(\"v\")",
"RETURN CREATE_GRAPH(\"g\", {})",
"RETURN DROP_GRAPH(\"g\")",
"RETURN LENGTH([DROP_GRAPH(\"g\")])",
"RETURN ROW_POLICY(\"orders\", null)",
"RETURN ROW_POLICY(\"orders\", \"doc.owner == CURRENT_USER\")",
"RETURN APPLY(\"DROP_GRAPH\", [\"prod\"])",
"RETURN CALL(\"drop_view\", \"v\")",
"RETURN APPLY(\"ROW_POLICY\", [\"orders\", null])",
"RETURN CALL(\"APPLY\", \"DROP_GRAPH\", [\"g\"])",
"FOR d IN c RETURN CALL(d.fn, 1)",
"RETURN APPLY(@fn, [])",
] {
let parsed = parse(query).unwrap_or_else(|e| panic!("parse {query}: {e}"));
assert!(parsed.has_mutations(), "must require Write: {}", query);
}
}
#[test]
fn test_has_mutations_leaves_reads_alone() {
for query in [
"FOR d IN users RETURN d",
"FOR d IN users FILTER d.age > 25 SORT d.age DESC LIMIT 10 RETURN d",
"FOR d IN users RETURN (FOR o IN orders FILTER o.user == d._key RETURN o)",
"RETURN LENGTH(users)",
"FOR d IN c COLLECT g = d.kind AGGREGATE n = COUNT() RETURN {g, n}",
"WITH t AS (FOR d IN c RETURN d) FOR x IN t RETURN x",
"FOR d IN c RETURN CONCAT(d.a, d.b)",
"RETURN ROW_POLICY(\"orders\")",
"RETURN CALL(\"ABS\", -3)",
"RETURN APPLY(\"CONCAT\", [\"a\", \"b\"])",
] {
let parsed = parse(query).unwrap_or_else(|e| panic!("parse {query}: {e}"));
assert!(!parsed.has_mutations(), "must stay a read: {}", query);
}
}
#[test]
fn test_has_mutations_still_sees_clause_level_writes() {
for query in [
"INSERT {a: 1} INTO c",
"FOR d IN c UPDATE d WITH {x: 1} IN c",
"FOR d IN c REMOVE d IN c",
"FOR d IN c RETURN d UNION (FOR e IN c INSERT {} INTO c)",
] {
let parsed = parse(query).unwrap_or_else(|e| panic!("parse {query}: {e}"));
assert!(parsed.has_mutations(), "must require Write: {}", query);
}
}
#[test]
fn test_parse_let_after_limit() {
let query = parse("FOR c IN companies SORT c.name ASC LIMIT 0, 50 LET n = 1 RETURN n").unwrap();
assert_eq!(query.post_limit_lets.len(), 1);
assert_eq!(query.post_limit_lets[0].variable, "n");
assert!(query.limit_clause.is_some());
assert!(query.return_clause.is_some());
}
#[test]
fn test_parse_let_after_limit_keeps_body_lets_apart() {
let query = parse("FOR c IN companies LET a = 1 LIMIT 10 LET b = 2 RETURN [a, b]").unwrap();
assert_eq!(query.post_limit_lets.len(), 1);
assert_eq!(query.post_limit_lets[0].variable, "b");
assert!(query
.body_clauses
.iter()
.any(|clause| matches!(clause, BodyClause::Let(l) if l.variable == "a")));
}
#[test]
fn test_parse_several_lets_after_limit() {
let query = parse("FOR c IN users LIMIT 5 LET a = 1 LET b = 2 RETURN [a, b]").unwrap();
assert_eq!(query.post_limit_lets.len(), 2);
}
#[test]
fn test_parse_let_after_limit_with_subquery() {
let query = parse(
"FOR c IN companies SORT c.name ASC LIMIT 0, 50 \
LET dus = (FOR o IN orders FILTER o.company_id == c._key RETURN o.total) \
RETURN MERGE(c, { \"encours\": SUM(dus) })",
)
.unwrap();
assert_eq!(query.post_limit_lets.len(), 1);
assert_eq!(query.post_limit_lets[0].variable, "dus");
}
#[test]
fn test_parse_let_after_standalone_offset() {
let query = parse("FOR c IN users OFFSET 10 LET n = 1 RETURN n").unwrap();
assert_eq!(query.post_limit_lets.len(), 1);
}