selene-db-gql 1.3.0

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

mod exec_common;

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

fn edge_lists_for(table: &selene_gql::BindingTable, name: &str) -> Vec<Option<Vec<u64>>> {
    exec_common::column_values(table, name)
        .into_iter()
        .map(|value| match value {
            Value::List(items) => Some(
                items
                    .into_iter()
                    .map(|item| match item {
                        Value::EdgeRef(id) => id.get(),
                        other => panic!("expected edge ref in group list, got {other:?}"),
                    })
                    .collect(),
            ),
            Value::Null => None,
            other => panic!("expected edge list or null, got {other:?}"),
        })
        .collect()
}

fn int_lists_for(table: &selene_gql::BindingTable, name: &str) -> Vec<Vec<Option<i64>>> {
    exec_common::column_values(table, name)
        .into_iter()
        .map(|value| match value {
            Value::List(items) => items
                .into_iter()
                .map(|item| match item {
                    Value::Int(value) => Some(value),
                    Value::Null => None,
                    other => panic!("expected integer or null in property list, got {other:?}"),
                })
                .collect(),
            other => panic!("expected property list, got {other:?}"),
        })
        .collect()
}

#[test]
fn bounded_repeat_emits_paths_across_hop_range() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a:Person)-[r:KNOWS*1..2]->(b) RETURN a, r, 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(1), Some(2)]);
    assert_eq!(node_ids_for(&table, "b"), vec![Some(2), Some(4), Some(4)]);
    assert_eq!(
        edge_lists_for(&table, "r"),
        vec![Some(vec![1]), Some(vec![1, 2]), Some(vec![2])]
    );
}

#[test]
fn group_variable_property_access_emits_ordered_property_lists() {
    let fixture = ExecFixture::build();
    let plan = planned(
        "MATCH (a:Person)-[r:KNOWS*1..2]->(b) \
         RETURN r.score AS scores, r.missing AS missing",
    );

    let table = execute_plan(&fixture, &plan).expect("repeat property projection executes");

    assert_eq!(
        int_lists_for(&table, "scores"),
        vec![vec![Some(1)], vec![Some(1), Some(2)], vec![Some(2)]]
    );
    assert_eq!(
        int_lists_for(&table, "missing"),
        vec![vec![None], vec![None, None], vec![None]]
    );
}

#[test]
fn zero_hop_group_variable_property_access_emits_empty_list() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a:Person)-[r:KNOWS*0..1]->(b) RETURN r.score AS scores");

    let table = execute_plan(&fixture, &plan).expect("zero-hop property projection executes");

    assert_eq!(
        int_lists_for(&table, "scores"),
        vec![vec![], vec![Some(1)], vec![], vec![Some(2)], vec![]]
    );
}

#[test]
fn exact_repeat_emits_only_matching_depth() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a:Person)-[r:KNOWS{2}]->(b) RETURN a, r, 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)]);
    assert_eq!(node_ids_for(&table, "b"), vec![Some(4)]);
    assert_eq!(edge_lists_for(&table, "r"), vec![Some(vec![1, 2])]);
}

#[test]
fn zero_hop_repeat_binds_empty_edge_list_and_source_as_final_node() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a:Person)-[r:KNOWS*0..1]->(b) RETURN a, r, 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(1), Some(2), Some(2), Some(3)]
    );
    assert_eq!(
        node_ids_for(&table, "b"),
        vec![Some(1), Some(2), Some(2), Some(4), Some(3)]
    );
    assert_eq!(
        edge_lists_for(&table, "r"),
        vec![
            Some(vec![]),
            Some(vec![1]),
            Some(vec![]),
            Some(vec![2]),
            Some(vec![])
        ]
    );
}

#[test]
fn repeat_evaluates_edge_property_predicates_per_hop() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a)-[r:KNOWS*1..2 {score: 2}]->(b) RETURN r");
    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_lists_for(&table, "r"), vec![Some(vec![2])]);
}

#[test]
fn repeat_evaluates_inline_predicates_per_hop() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a:Person)-[r:KNOWS*1..2 WHERE a.age = 42]->(b) RETURN a, r, 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)]);
    assert_eq!(node_ids_for(&table, "b"), vec![Some(4)]);
    assert_eq!(edge_lists_for(&table, "r"), vec![Some(vec![2])]);
}

#[test]
fn repeat_inline_predicates_see_current_group_binding() {
    let fixture = ExecFixture::build();
    let plan = planned("MATCH (a:Person)-[r:KNOWS*1..2 WHERE r IS NOT NULL]->(b) RETURN a, r, 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(1), Some(2)]);
    assert_eq!(node_ids_for(&table, "b"), vec![Some(2), Some(4), Some(4)]);
    assert_eq!(
        edge_lists_for(&table, "r"),
        vec![Some(vec![1]), Some(vec![1, 2]), Some(vec![2])]
    );
}

#[test]
fn repeat_composes_under_optional_outer_join() {
    let fixture = ExecFixture::build();
    let plan =
        planned("MATCH (a:Person) OPTIONAL MATCH (a)-[r:KNOWS{2}]->(b:Person) RETURN a, b, r");

    let table = execute_plan(&fixture, &plan).expect("optional repeat executes");

    assert_eq!(node_ids_for(&table, "a"), vec![Some(1), Some(2), Some(3)]);
    assert_eq!(node_ids_for(&table, "b"), vec![None, None, None]);
    assert_eq!(edge_lists_for(&table, "r"), vec![None, None, None]);
}

#[test]
fn repeat_checks_cancellation_during_traversal() {
    let root_label = db_string("Root");
    let target_label = db_string("Target");
    let edge_label = db_string("K");
    let graph = SharedGraph::new(GraphId::new(6201));
    {
        let mut txn = graph.begin_write();
        let mut mutator = txn.mutator();
        let root = mutator
            .create_node(LabelSet::single(root_label), props([]))
            .expect("root inserts");
        for _ in 0..1100 {
            let target = mutator
                .create_node(LabelSet::single(target_label.clone()), props([]))
                .expect("target inserts");
            mutator
                .create_edge(edge_label.clone(), root, target, props([]))
                .expect("edge inserts");
        }
        txn.commit().expect("fixture commits");
    }

    let plan = planned("MATCH (a:Root)-[:K*1..1]->(b) RETURN b");
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");
    let caps = ImplDefinedCaps::default();
    let token = CancellationToken::new();
    token.cancel();
    let ctx = TxContext::read_only(
        graph.read(),
        &caps,
        &EmptyProcedureRegistry,
        graph.index_providers(),
    )
    .with_plan_metadata(&plan.expr_ids, &plan.subqueries)
    .with_resource_limits(Some(&token), None, None, None);

    let err = selene_gql::execute_pattern(pattern, &ctx).expect_err("repeat observes token");

    assert!(matches!(err, ExecutorError::Cancelled { .. }));
    assert_eq!(err.gqlstatus().as_str(), "5GQL2");
}

#[test]
fn repeat_checks_cancellation_between_source_rows_without_adjacent_edges() {
    let root_label = db_string("Root");
    let graph = SharedGraph::new(GraphId::new(6202));
    {
        let mut txn = graph.begin_write();
        let mut mutator = txn.mutator();
        for _ in 0..1100 {
            mutator
                .create_node(LabelSet::single(root_label.clone()), props([]))
                .expect("root inserts");
        }
        txn.commit().expect("fixture commits");
    }

    let plan = planned("MATCH (a:Root)-[:K*1..1]->(b) RETURN b");
    let pattern = plan.pattern_plan.as_ref().expect("pattern plan");
    let caps = ImplDefinedCaps::default();
    let token = CancellationToken::new();
    token.cancel();
    let ctx = TxContext::read_only(
        graph.read(),
        &caps,
        &EmptyProcedureRegistry,
        graph.index_providers(),
    )
    .with_plan_metadata(&plan.expr_ids, &plan.subqueries)
    .with_resource_limits(Some(&token), None, None, None);

    let err = selene_gql::execute_pattern(pattern, &ctx).expect_err("repeat observes token");

    assert!(matches!(err, ExecutorError::Cancelled { .. }));
    assert_eq!(err.gqlstatus().as_str(), "5GQL2");
}

#[test]
fn quantifier_max_exceeding_cap_is_rejected_before_execution() {
    let err = selene_gql::plan(
        &selene_gql::analyze(
            selene_gql::parse("MATCH (a)-[:K*1..101]->(b) RETURN a").expect("parses"),
            &EmptyProcedureRegistry,
            None,
        )
        .expect("analyzes"),
        &EmptyProcedureRegistry,
    )
    .expect_err("cap rejects");

    assert_eq!(err.gqlstatus().as_str(), "5GQL1");
}