selene-db-gql 1.3.0

ISO/IEC 39075:2024 GQL parser, planner, optimizer, and executor for selene-db.
Documentation
//! Executor expand operator tests.

mod exec_common;

use exec_common::{
    ExecFixture, db_string, edge_ids_for, execute_pattern, node_ids_for, planned, props,
};
use selene_core::{GraphId, LabelSet};
use selene_gql::{EmptyProcedureRegistry, ImplDefinedCaps, TxContext};
use selene_graph::SharedGraph;

#[test]
fn expand_outgoing_emits_one_row_per_outgoing_edge() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a)-[:KNOWS]->(b) RETURN a, b");
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");
    let ctx = fixture.context_caps(&plan);

    let table = execute_pattern(pattern, &ctx);

    assert_eq!(node_ids_for(&table, "a"), vec![Some(1), Some(2)]);
    assert_eq!(node_ids_for(&table, "b"), vec![Some(2), Some(4)]);
}

#[test]
fn expand_incoming_emits_one_row_per_incoming_edge() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a)<-[:KNOWS]-(b) RETURN a, b");
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");
    let ctx = fixture.context_caps(&plan);

    let table = execute_pattern(pattern, &ctx);

    assert_eq!(node_ids_for(&table, "a"), vec![Some(2), Some(4)]);
    assert_eq!(node_ids_for(&table, "b"), vec![Some(1), Some(2)]);
}

#[test]
fn expand_both_includes_outgoing_and_incoming() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a)-[:KNOWS]-(b) RETURN a, b");
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");
    let ctx = fixture.context_caps(&plan);

    let table = execute_pattern(pattern, &ctx);
    let mut pairs = node_ids_for(&table, "a")
        .into_iter()
        .zip(node_ids_for(&table, "b"))
        .collect::<Vec<_>>();
    pairs.sort();

    assert_eq!(
        pairs,
        vec![
            (Some(1), Some(2)),
            (Some(2), Some(1)),
            (Some(2), Some(4)),
            (Some(4), Some(2)),
        ]
    );
}

#[test]
fn expand_both_dedups_self_loops_to_one_row() {
    let loop_label = db_string("LoopNode");
    let edge_label = db_string("LOOPS");
    let graph = SharedGraph::new(GraphId::new(3201));
    {
        let mut txn = graph.begin_write();
        let mut mutator = txn.mutator();
        let node = mutator
            .create_node(LabelSet::single(loop_label), props([]))
            .expect("node inserts");
        mutator
            .create_edge(edge_label, node, node, props([]))
            .expect("self-loop inserts");
        txn.commit().expect("fixture commits");
    }
    let plan = planned("MATCH (a:LoopNode)-[e:LOOPS]-(b) RETURN a, e, b");
    let caps = ImplDefinedCaps::default();
    let ctx = TxContext::read_only(
        graph.read(),
        &caps,
        &EmptyProcedureRegistry,
        graph.index_providers(),
    );
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");

    let table = execute_pattern(pattern, &ctx);

    assert_eq!(table.row_count(), 1);
    assert_eq!(node_ids_for(&table, "a"), vec![Some(1)]);
    assert_eq!(node_ids_for(&table, "b"), vec![Some(1)]);
    assert_eq!(edge_ids_for(&table, "e"), vec![Some(1)]);
}

#[test]
fn expand_chains_through_anonymous_intermediate_node() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a:Person)-[:KNOWS]->()-[:KNOWS]->(b) RETURN a, b");
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");
    let ctx = fixture.context_caps(&plan);

    let table = execute_pattern(pattern, &ctx);
    let mut pairs = node_ids_for(&table, "a")
        .into_iter()
        .zip(node_ids_for(&table, "b"))
        .collect::<Vec<_>>();
    pairs.sort();

    assert_eq!(pairs, vec![(Some(1), Some(4))]);
}

#[test]
fn expand_filters_edges_by_label_predicate() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a)-[:MISSING]->(b) RETURN a, b");
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");
    let ctx = fixture.context_caps(&plan);

    let table = execute_pattern(pattern, &ctx);

    assert!(table.is_empty());
}

#[test]
fn expand_evaluates_edge_residual_predicates() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a)-[e:KNOWS {score: 2}]->(b) RETURN e");
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");
    let ctx = fixture.context_caps(&plan);

    let table = execute_pattern(pattern, &ctx);

    assert_eq!(edge_ids_for(&table, "e"), vec![Some(2)]);
}

#[test]
fn expand_evaluates_right_binding_residual_predicates() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a)-[:KNOWS]->(b {score: 99}) RETURN b");
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");
    let ctx = fixture.context_caps(&plan);

    let table = execute_pattern(pattern, &ctx);

    assert_eq!(node_ids_for(&table, "b"), vec![Some(4)]);
}

#[test]
fn expand_drops_edge_when_residual_evaluates_to_null() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a)-[e:KNOWS]->(b) WHERE e.missing RETURN e");
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");
    let ctx = fixture.context_caps(&plan);

    let table = execute_pattern(pattern, &ctx);

    assert!(table.is_empty());
}