uqa-graph 0.3.0

Graph store, RPQ, Cypher (lexer/parser/AST/compiler), graph algorithms
Documentation
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

use super::*;

#[test]
fn parse_simple_match_return() {
    let q = parse_cypher("MATCH (n:Person) RETURN n.name").unwrap();
    assert_eq!(q.clauses.len(), 2);
    match &q.clauses[0] {
        CypherClause::Match(m) => {
            assert!(!m.optional);
            assert_eq!(m.patterns.len(), 1);
            let elements = &m.patterns[0].elements;
            assert_eq!(elements.len(), 1);
            if let PathElement::Node(np) = &elements[0] {
                assert_eq!(np.variable.as_deref(), Some("n"));
                assert_eq!(np.labels, vec!["Person".to_string()]);
            } else {
                panic!("expected node pattern");
            }
        }
        _ => panic!("expected MATCH"),
    }
    match &q.clauses[1] {
        CypherClause::Return(r) => {
            assert_eq!(r.items.len(), 1);
            if let CypherExpr::PropertyAccess(pa) = &r.items[0].expr {
                assert_eq!(pa.variable, "n");
                assert_eq!(pa.keys, vec!["name".to_string()]);
            } else {
                panic!("expected PropertyAccess");
            }
        }
        _ => panic!("expected RETURN"),
    }
}

#[test]
fn parse_match_with_relationship() {
    let q = parse_cypher("MATCH (a)-[r:KNOWS]->(b) RETURN b").unwrap();
    if let CypherClause::Match(m) = &q.clauses[0] {
        let path = &m.patterns[0];
        assert_eq!(path.elements.len(), 3);
        if let PathElement::Rel(rp) = &path.elements[1] {
            assert_eq!(rp.variable.as_deref(), Some("r"));
            assert_eq!(rp.types, vec!["KNOWS".to_string()]);
            assert_eq!(rp.direction, RelDirection::Right);
        } else {
            panic!("expected rel pattern");
        }
    } else {
        panic!("expected MATCH");
    }
}

#[test]
fn parse_variable_length_path() {
    let q = parse_cypher("MATCH (a)-[*1..3]->(b) RETURN b").unwrap();
    if let CypherClause::Match(m) = &q.clauses[0] {
        if let PathElement::Rel(rp) = &m.patterns[0].elements[1] {
            assert_eq!(rp.min_hops, Some(1));
            assert_eq!(rp.max_hops, Some(3));
        } else {
            panic!();
        }
    }
}

#[test]
fn parse_where_with_comparison_and_logic() {
    let q = parse_cypher("MATCH (n) WHERE n.age > 18 AND n.name = 'a' RETURN n").unwrap();
    if let CypherClause::Match(m) = &q.clauses[0] {
        assert!(m.r#where.is_some());
        if let CypherExpr::BinaryOp(bo) = m.r#where.as_ref().unwrap() {
            assert_eq!(bo.op, "AND");
        } else {
            panic!();
        }
    }
}

#[test]
fn parse_function_call_and_distinct() {
    let q = parse_cypher("MATCH (n) RETURN count(DISTINCT n.id)").unwrap();
    if let CypherClause::Return(r) = &q.clauses[1] {
        if let CypherExpr::FunctionCall(fc) = &r.items[0].expr {
            assert_eq!(fc.name, "count");
            assert!(fc.distinct);
        } else {
            panic!();
        }
    }
}

#[test]
fn parse_create_with_property_map() {
    let q = parse_cypher("CREATE (n:Person {name: 'alice', age: 30})").unwrap();
    if let CypherClause::Create(c) = &q.clauses[0] {
        if let PathElement::Node(np) = &c.patterns[0].elements[0] {
            let props = np.properties.as_ref().unwrap();
            assert!(props.contains_key("name"));
            assert!(props.contains_key("age"));
        } else {
            panic!();
        }
    }
}

#[test]
fn parse_unwind_and_with() {
    let q = parse_cypher("UNWIND [1,2,3] AS x WITH x WHERE x > 1 RETURN x").unwrap();
    assert_eq!(q.clauses.len(), 3);
    if let CypherClause::Unwind(u) = &q.clauses[0] {
        assert_eq!(u.variable, "x");
    } else {
        panic!();
    }
    if let CypherClause::With(w) = &q.clauses[1] {
        assert!(w.r#where.is_some());
    } else {
        panic!();
    }
}

#[test]
fn parse_optional_match_and_set() {
    let q = parse_cypher("OPTIONAL MATCH (n) SET n.flag = true RETURN n").unwrap();
    if let CypherClause::Match(m) = &q.clauses[0] {
        assert!(m.optional);
    } else {
        panic!();
    }
    if let CypherClause::Set(s) = &q.clauses[1] {
        assert_eq!(s.items[0].operator, SetOperator::Assign);
    } else {
        panic!();
    }
}

#[test]
fn parse_detach_delete() {
    let q = parse_cypher("MATCH (n) DETACH DELETE n").unwrap();
    if let CypherClause::Delete(d) = &q.clauses[1] {
        assert!(d.detach);
    } else {
        panic!();
    }
}

#[test]
fn parse_in_list_and_starts_with() {
    let q =
        parse_cypher("MATCH (n) WHERE n.name STARTS WITH 'a' AND n.id IN [1, 2] RETURN n").unwrap();
    if let CypherClause::Match(m) = &q.clauses[0] {
        assert!(m.r#where.is_some());
    } else {
        panic!();
    }
}