plexus-engine 0.3.4

Engine integration traits for consuming Plexus plans
Documentation
use super::*;
use plexus_gql::parse_gql_with_metadata;
use plexus_ir::{lower_to_flatbuffer_with_match_graph_refs, lower_to_plan_with_match_graph_refs};

fn gql_to_bytes(gql: &str) -> Vec<u8> {
    let (query, metadata) = parse_gql_with_metadata(gql).expect("gql parse");
    lower_to_flatbuffer_with_match_graph_refs(&query, &metadata.match_graph_refs)
        .expect("bridge lower_to_flatbuffer_with_match_graph_refs")
}

#[test]
fn gql_filter_to_execute_smoke() {
    let gql =
        "MATCH (n:Person)-[r:KNOWS]->(m:Person) FILTER m.age > 30 RETURN n.name AS name ORDER BY name ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(out.rows, vec![vec![Value::String("Alice".to_string())]]);
}

#[test]
fn gql_use_graph_clause_to_execute_smoke() {
    let gql = "USE GRAPH social MATCH (n:Person) RETURN n.name AS name ORDER BY name ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![Value::String("Alice".to_string())],
            vec![Value::String("Bob".to_string())]
        ]
    );
}

#[test]
fn gql_use_graph_clause_with_alias_to_execute_smoke() {
    let gql = "USE GRAPH social AS g MATCH (n:Person) RETURN n.name AS name ORDER BY name ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![Value::String("Alice".to_string())],
            vec![Value::String("Bob".to_string())]
        ]
    );
}

#[test]
fn gql_graph_clause_to_execute_smoke() {
    let gql = "GRAPH social MATCH (n:Person) RETURN n.name AS name ORDER BY name ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![Value::String("Alice".to_string())],
            vec![Value::String("Bob".to_string())]
        ]
    );
}

#[test]
fn gql_graph_clause_with_alias_to_execute_smoke() {
    let gql = "GRAPH social AS g MATCH (n:Person) RETURN n.name AS name ORDER BY name ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![Value::String("Alice".to_string())],
            vec![Value::String("Bob".to_string())]
        ]
    );
}

#[test]
fn gql_let_to_execute_smoke() {
    let gql = "MATCH (n:Person) LET score = n.age * 2 RETURN score ORDER BY score ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(out.rows, vec![vec![Value::Int(60)], vec![Value::Int(80)]]);
}

#[test]
fn gql_multi_let_to_execute_smoke() {
    let gql = "MATCH (n:Person) LET score = n.age * 2, next_age = n.age + 1 RETURN score, next_age ORDER BY score ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![Value::Int(60), Value::Int(31)],
            vec![Value::Int(80), Value::Int(41)]
        ]
    );
}

#[test]
fn gql_offset_to_execute_smoke() {
    let gql = "MATCH (n:Person) RETURN n.name AS name ORDER BY name ASC OFFSET 1 LIMIT 1";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(out.rows, vec![vec![Value::String("Bob".to_string())]]);
}

#[test]
fn gql_next_clause_chain_to_execute_smoke() {
    let gql = "MATCH (n:Person) RETURN n AS n NEXT RETURN n.name AS name ORDER BY name ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![Value::String("Alice".to_string())],
            vec![Value::String("Bob".to_string())]
        ]
    );
}

#[test]
fn gql_next_match_clause_chain_to_execute_smoke() {
    let gql = "MATCH (n:Person) RETURN n AS n NEXT MATCH (n)-[:KNOWS]->(m:Person) RETURN n.name AS src, m.name AS dst ORDER BY src ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![
                Value::String("Alice".to_string()),
                Value::String("Bob".to_string())
            ],
            vec![
                Value::String("Bob".to_string()),
                Value::String("Alice".to_string())
            ]
        ]
    );
}

#[test]
fn gql_for_clause_to_execute_smoke() {
    let gql =
        "MATCH (n:Person) WITH COLLECT(n.name) AS names FOR x IN names RETURN x ORDER BY x ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![Value::String("Alice".to_string())],
            vec![Value::String("Bob".to_string())]
        ]
    );
}

#[test]
fn gql_multi_for_clause_to_execute_smoke() {
    let gql = "MATCH (n:Person) WITH COLLECT(n.name) AS names, COLLECT(n.age) AS ages FOR x IN names, y IN ages RETURN x, y ORDER BY x ASC, y ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![Value::String("Alice".to_string()), Value::Int(30)],
            vec![Value::String("Alice".to_string()), Value::Int(40)],
            vec![Value::String("Bob".to_string()), Value::Int(30)],
            vec![Value::String("Bob".to_string()), Value::Int(40)]
        ]
    );
}

#[test]
fn gql_set_clause_to_execute_smoke() {
    let gql = "MATCH (n:Person) FILTER n.name = 'Alice' SET n.age = 31 RETURN n.age AS age";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(out.rows, vec![vec![Value::Int(31)]]);
    let alice = engine
        .graph
        .nodes
        .iter()
        .find(|n| n.props.get("name") == Some(&Value::String("Alice".to_string())))
        .expect("alice exists");
    assert_eq!(alice.props.get("age"), Some(&Value::Int(31)));
}

#[test]
fn gql_delete_clause_to_execute_smoke() {
    let gql = "MATCH (n:Person)-[r:KNOWS]->(m:Person) FILTER n.name = 'Alice' DELETE r RETURN n.name AS name";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let initial_rels = engine.graph.rels.len();
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(out.rows, vec![vec![Value::String("Alice".to_string())]]);
    assert_eq!(engine.graph.rels.len(), initial_rels - 1);
}

#[test]
fn gql_insert_clause_to_execute_smoke() {
    let gql = "MATCH (a:Person) FILTER a.name = 'Alice' INSERT (n:Person) SET n.name = 'Eve', n.age = 22 RETURN n.name AS name";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let initial_nodes = engine.graph.nodes.len();
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(out.rows, vec![vec![Value::String("Eve".to_string())]]);
    assert_eq!(engine.graph.nodes.len(), initial_nodes + 1);
    assert!(engine.graph.nodes.iter().any(|n| {
        n.labels.contains("Person")
            && n.props.get("name") == Some(&Value::String("Eve".to_string()))
            && n.props.get("age") == Some(&Value::Int(22))
    }));
}

#[test]
fn gql_next_after_with_to_execute_smoke() {
    let gql = "MATCH (n:Person) WITH n NEXT MATCH (n)-[:KNOWS]->(m:Person) RETURN n.name AS src, m.name AS dst ORDER BY src ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![
                Value::String("Alice".to_string()),
                Value::String("Bob".to_string())
            ],
            vec![
                Value::String("Bob".to_string()),
                Value::String("Alice".to_string())
            ]
        ]
    );
}

#[test]
fn gql_graph_alias_in_expression_to_execute_smoke() {
    let gql = "GRAPH social AS g MATCH (g.n:Person) RETURN g.n.name AS name ORDER BY name ASC";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(
        out.rows,
        vec![
            vec![Value::String("Alice".to_string())],
            vec![Value::String("Bob".to_string())]
        ]
    );
}

#[test]
fn gql_detach_delete_clause_to_execute_smoke() {
    let gql = "MATCH (n:Person) FILTER n.name = 'Alice' DETACH DELETE n RETURN 1 AS deleted";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let initial_node_count = engine.graph.nodes.len();
    let initial_rel_count = engine.graph.rels.len();
    let out = execute_serialized(&mut engine, &bytes).expect("execute");
    assert_eq!(out.rows, vec![vec![Value::Int(1)]]);
    assert_eq!(engine.graph.nodes.len(), initial_node_count - 1);
    assert!(engine.graph.rels.len() < initial_rel_count);
    assert!(!engine
        .graph
        .nodes
        .iter()
        .any(|n| n.props.get("name") == Some(&Value::String("Alice".to_string()))));
}

#[test]
fn gql_multi_graph_switching_rejected_by_engine_capability() {
    // Multi-graph queries produce plans with multiple distinct graph_refs.
    // The reference engine declares supports_multi_graph=false, so execution must fail.
    let (query, metadata) = parse_gql_with_metadata(
        "USE GRAPH social MATCH (n:Person) RETURN n AS n NEXT USE GRAPH other MATCH (m:Person) RETURN n.name, m.name",
    )
    .expect("gql parse");
    assert_eq!(
        metadata.match_graph_refs,
        vec![Some("social".to_string()), Some("other".to_string())]
    );
    let bytes = lower_to_flatbuffer_with_match_graph_refs(&query, &metadata.match_graph_refs)
        .expect("lower");
    let mut engine = InMemoryEngine::new(fixture_graph());
    let err =
        execute_serialized(&mut engine, &bytes).expect_err("must fail: no supports_multi_graph");
    let msg = err.to_string();
    assert!(msg.contains("supports_multi_graph=false"), "{msg}");
}

#[test]
fn gql_standalone_insert_executes_via_const_row() {
    // Standalone INSERT (no preceding MATCH) must succeed via the ConstRow seed.
    let gql = "INSERT (n:Person) SET n.name = 'Charlie' RETURN n.name AS name";
    let bytes = gql_to_bytes(gql);

    let mut engine = InMemoryEngine::new(fixture_graph());
    let initial_nodes = engine.graph.nodes.len();
    let out = execute_serialized(&mut engine, &bytes).expect("standalone INSERT must execute");
    assert_eq!(out.rows, vec![vec![Value::String("Charlie".to_string())]]);
    assert_eq!(engine.graph.nodes.len(), initial_nodes + 1);
    assert!(engine
        .graph
        .nodes
        .iter()
        .any(|n| n.labels.contains("Person")
            && n.props.get("name") == Some(&Value::String("Charlie".to_string()))));
}

#[test]
fn gql_graph_param_variable_rejected_by_engine_capability() {
    // $g is accepted by the parser but must be rejected by capability validation
    // on engines that declare supports_graph_params=false.
    let (query, metadata) =
        parse_gql_with_metadata("USE GRAPH $g MATCH (n:Person) RETURN n.name").expect("gql parse");
    assert_eq!(metadata.selected_graph, Some("$g".to_string()));

    let plan =
        lower_to_plan_with_match_graph_refs(&query, &metadata.match_graph_refs).expect("lower");

    let plan_semver: PlanSemver = (&plan.version).into();
    let mut caps = EngineCapabilities::full(VersionRange::new(plan_semver, plan_semver));
    caps.supports_graph_ref = true;
    caps.supports_graph_params = false;

    let err = validate_plan_against_capabilities(&plan, &caps)
        .expect_err("must fail: supports_graph_params=false");
    let msg = err.to_string();
    assert!(msg.contains("supports_graph_params"), "{msg}");
}