use super::*;
use crate::catalog::build_catalog;
use crate::query::parser::parse_query;
use crate::query::typecheck::{CheckedQuery, typecheck_query, typecheck_query_decl};
use crate::schema::parser::parse_schema;
fn setup() -> Catalog {
let schema = parse_schema(
r#"
node Person { name: String age: I32? }
node Company { name: String }
edge Knows: Person -> Person { since: Date? }
edge WorksAt: Person -> Company
"#,
)
.unwrap();
build_catalog(&schema).unwrap()
}
#[test]
fn test_lower_basic() {
let catalog = setup();
let qf = parse_query(
r#"
query q($name: String) {
match {
$p: Person { name: $name }
$p knows $f
}
return { $f.name, $f.age }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 2); assert_eq!(ir.return_exprs.len(), 2);
}
#[test]
fn test_lower_negation() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person
not { $p worksAt $_ }
}
return { $p.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 2); assert!(matches!(&ir.pipeline[1], IROp::AntiJoin { .. }));
}
#[test]
fn test_lower_mutation_update() {
let catalog = setup();
let qf = parse_query(
r#"
query q($name: String, $age: I32) {
update Person set { age: $age } where name = $name
}
"#,
)
.unwrap();
let checked = typecheck_query_decl(&catalog, &qf.queries[0]).unwrap();
assert!(matches!(checked, CheckedQuery::Mutation(_)));
let ir = lower_mutation_query(&qf.queries[0]).unwrap();
match &ir.ops[0] {
MutationOpIR::Update {
type_name,
assignments,
predicate,
} => {
assert_eq!(type_name, "Person");
assert_eq!(assignments.len(), 1);
assert_eq!(assignments[0].property, "age");
assert_eq!(predicate.property, "name");
}
_ => panic!("expected update mutation op"),
}
}
#[test]
fn test_lower_bounded_traversal() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person
$p knows{1,3} $f
}
return { $f.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
let expand = ir
.pipeline
.iter()
.find_map(|op| match op {
IROp::Expand {
min_hops, max_hops, ..
} => Some((*min_hops, *max_hops)),
_ => None,
})
.expect("expected expand op");
assert_eq!(expand.0, 1);
assert_eq!(expand.1, Some(3));
}
#[test]
fn test_lower_now_uses_reserved_runtime_param() {
let catalog = setup();
let qf = parse_query(
r#"
query stamp() {
match { $p: Person }
return { now() as ts }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert!(matches!(
ir.return_exprs[0].expr,
IRExpr::Param(ref name) if name == NOW_PARAM_NAME
));
}
#[test]
fn test_lower_mutation_now_uses_reserved_runtime_param() {
let catalog = build_catalog(
&parse_schema(
r#"
node Event {
slug: String @key
updated_at: DateTime?
}
"#,
)
.unwrap(),
)
.unwrap();
let qf = parse_query(
r#"
query stamp() {
update Event set { updated_at: now() } where updated_at = now()
}
"#,
)
.unwrap();
let checked = typecheck_query_decl(&catalog, &qf.queries[0]).unwrap();
assert!(matches!(checked, CheckedQuery::Mutation(_)));
let ir = lower_mutation_query(&qf.queries[0]).unwrap();
match &ir.ops[0] {
MutationOpIR::Update {
assignments,
predicate,
..
} => {
assert!(matches!(
assignments[0].value,
IRExpr::Param(ref name) if name == NOW_PARAM_NAME
));
assert!(matches!(
predicate.value,
IRExpr::Param(ref name) if name == NOW_PARAM_NAME
));
}
_ => panic!("expected update mutation op"),
}
}
#[test]
fn test_lower_multi_mutation() {
let catalog = setup();
let qf = parse_query(
r#"
query q($name: String, $age: I32, $friend: String) {
insert Person { name: $name, age: $age }
insert Knows { from: $name, to: $friend }
}
"#,
)
.unwrap();
let checked = typecheck_query_decl(&catalog, &qf.queries[0]).unwrap();
assert!(matches!(checked, CheckedQuery::Mutation(_)));
let ir = lower_mutation_query(&qf.queries[0]).unwrap();
assert_eq!(ir.ops.len(), 2);
assert!(
matches!(&ir.ops[0], MutationOpIR::Insert { type_name, .. } if type_name == "Person")
);
assert!(
matches!(&ir.ops[1], MutationOpIR::Insert { type_name, .. } if type_name == "Knows")
);
}
#[test]
fn test_lower_traversal_with_destination_binding() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person
$p worksAt $c
$c: Company { name: "Acme" }
}
return { $p.name, $c.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 2);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "p"));
assert!(matches!(
&ir.pipeline[1],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "p" && dst_var == "c" && dst_filters.len() == 1
));
}
#[test]
fn test_lower_chain_defers_all_intermediate_bindings() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person { name: "Alice" }
$p knows $f
$f: Person { name: "Bob" }
$f worksAt $c
$c: Company { name: "Acme" }
}
return { $c.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 3);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "p"));
assert!(matches!(
&ir.pipeline[1],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "p" && dst_var == "f" && dst_filters.len() == 1
));
assert!(matches!(
&ir.pipeline[2],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "f" && dst_var == "c" && dst_filters.len() == 1
));
}
#[test]
fn test_lower_reverse_traversal_defers_source_binding() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$c: Company { name: "Acme" }
$p worksAt $c
$p: Person { name: "Alice" }
}
return { $p.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 2);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "c"));
assert!(matches!(
&ir.pipeline[1],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "c" && dst_var == "p" && dst_filters.len() == 1
));
}
#[test]
fn test_lower_independent_bindings_still_cross_join() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person
$c: Company
}
return { $p.name, $c.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 2);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "p"));
assert!(matches!(&ir.pipeline[1], IROp::NodeScan { variable, .. } if variable == "c"));
}
#[test]
fn test_lower_destination_binding_without_filters() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person
$p worksAt $c
$c: Company
}
return { $p.name, $c.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 2);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "p"));
assert!(matches!(
&ir.pipeline[1],
IROp::Expand { src_var, dst_var, .. }
if src_var == "p" && dst_var == "c"
));
}
#[test]
fn test_lower_out_of_order_traversals() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person
$f worksAt $c
$p knows $f
$f: Person
$c: Company { name: "Acme" }
}
return { $c.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 3);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "p"));
assert!(matches!(
&ir.pipeline[1],
IROp::Expand { src_var, dst_var, .. }
if src_var == "p" && dst_var == "f"
));
assert!(matches!(
&ir.pipeline[2],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "f" && dst_var == "c" && dst_filters.len() == 1
));
}
#[test]
fn test_lower_wildcard_does_not_bridge_components() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person
$p knows $_
$c: Company
}
return { $p.name, $c.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 3);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "p"));
assert!(matches!(&ir.pipeline[1], IROp::NodeScan { variable, .. } if variable == "c"));
assert!(matches!(
&ir.pipeline[2],
IROp::Expand { src_var, dst_var, .. }
if src_var == "p" && dst_var == "_"
));
}
#[test]
fn test_lower_fan_out_topology() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person { name: "Alice" }
$p knows $f
$f: Person { name: "Bob" }
$p worksAt $c
$c: Company { name: "Acme" }
}
return { $f.name, $c.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 3);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "p"));
assert!(matches!(
&ir.pipeline[1],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "p" && dst_var == "f" && dst_filters.len() == 1
));
assert!(matches!(
&ir.pipeline[2],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "p" && dst_var == "c" && dst_filters.len() == 1
));
}
#[test]
fn test_lower_fan_in_topology() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$a: Person { name: "Alice" }
$a knows $c
$b: Person { name: "Bob" }
$b knows $c
$c: Person
}
return { $a.name, $b.name, $c.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 3);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "a"));
assert!(matches!(
&ir.pipeline[1],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "a" && dst_var == "c" && dst_filters.is_empty()
));
assert!(matches!(
&ir.pipeline[2],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "c" && dst_var == "b" && dst_filters.len() == 1
));
}
#[test]
fn test_lower_cycle_with_deferred_binding() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$a: Person
$a knows $b
$b: Person { name: "Bob" }
$b knows $a
}
return { $a.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 4);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "a"));
assert!(matches!(
&ir.pipeline[1],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "a" && dst_var == "b" && dst_filters.len() == 1
));
assert!(matches!(
&ir.pipeline[2],
IROp::Expand { src_var, dst_var, dst_filters, .. }
if src_var == "b" && dst_var.starts_with("__temp_") && dst_filters.is_empty()
));
assert!(matches!(&ir.pipeline[3], IROp::Filter(_)));
}
#[test]
fn test_lower_multiple_filters_on_deferred_binding() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person
$p knows $f
$f: Person { name: "Bob", age: 25 }
}
return { $f.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 2);
assert!(matches!(
&ir.pipeline[1],
IROp::Expand { dst_filters, .. }
if dst_filters.len() == 2
));
}
#[test]
fn test_lower_param_filter_on_deferred_binding() {
let catalog = setup();
let qf = parse_query(
r#"
query q($company: String) {
match {
$p: Person
$p worksAt $c
$c: Company { name: $company }
}
return { $p.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 2);
assert!(matches!(
&ir.pipeline[1],
IROp::Expand { dst_filters, .. }
if dst_filters.len() == 1
));
if let IROp::Expand { dst_filters, .. } = &ir.pipeline[1] {
assert!(matches!(&dst_filters[0].right, IRExpr::Param(name) if name == "company"));
}
}
#[test]
fn test_lower_negation_with_inner_binding() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person
not {
$p worksAt $c
$c: Company { name: "Acme" }
}
}
return { $p.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
assert_eq!(ir.pipeline.len(), 2);
assert!(matches!(&ir.pipeline[0], IROp::NodeScan { variable, .. } if variable == "p"));
let IROp::AntiJoin { inner, .. } = &ir.pipeline[1] else {
panic!("expected AntiJoin");
};
assert_eq!(inner.len(), 3);
assert!(matches!(&inner[0], IROp::NodeScan { variable, .. } if variable == "c"));
assert!(matches!(&inner[1], IROp::Expand { .. }));
assert!(matches!(&inner[2], IROp::Filter(_)));
}