haematite-cli 0.21.1

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use crate::serve::query::parser::{Plan, Source, Span, Stage, pipeline, pipeline_with};

type TestResult = Result<(), String>;

fn parsed(text: &str) -> Result<Plan, String> {
    pipeline(text).map_err(|error| error.to_string())
}

#[test]
fn match_source_forms_are_accepted_with_exact_stage_spans() -> TestResult {
    for (text, expected) in [
        ("match (c:patients)", Span { start: 0, end: 18 }),
        (
            "match (c:patients)-[:admitted_to]->(w:wards)",
            Span { start: 0, end: 44 },
        ),
        (
            "match (c:patients)<-[:admitted_to]-(w:wards)",
            Span { start: 0, end: 44 },
        ),
        (
            "match (c:patients)-[:in]->(w:wards)-[:in]->(h:hospitals)",
            Span { start: 0, end: 56 },
        ),
    ] {
        let plan = parsed(text)?;
        assert_eq!(plan.source, Source::All, "{text}");
        assert_eq!(plan.stages.len(), 1, "{text}");
        assert_eq!(plan.stages[0].span, expected, "{text}");
    }
    Ok(())
}

#[test]
fn match_follows_only_its_named_collection() -> TestResult {
    let plan = parsed("patients | match (c:patients)")?;
    assert_eq!(
        plan.source,
        Source::Collection {
            name: "patients".to_owned(),
            arguments: Vec::new(),
        }
    );
    assert_eq!(plan.stages.len(), 1);
    assert_eq!(plan.stages[0].span, Span { start: 11, end: 29 });
    for (text, construct) in [
        ("wards | match (c:patients)", "first node's label"),
        ("patients | limit(1) | match (c:patients)", "immediately"),
        ("'.' | match (c:patients)", "immediately"),
        ("match (c:patients) | match (w:wards)", "immediately"),
    ] {
        let error = pipeline(text).err().ok_or("misplaced match accepted")?;
        assert_eq!(error.code(), "query_stage_not_available", "{text}");
        assert!(error.to_string().contains(construct), "{error}");
    }
    Ok(())
}

#[test]
fn match_refuses_each_unsupported_construct_by_name() -> TestResult {
    for (text, construct) in [
        ("match (c)", "no label"),
        ("match (c:patients)-[:in]->(w)", "no label"),
        ("match (c:patients {id: 1})", "property map"),
        ("match (c:patients) WHERE c.id = 1", "WHERE"),
        ("match (c:patients) where c.id = 1", "WHERE"),
        ("match (C:patients)", "binding name"),
        ("match (c-1:patients)", "binding name"),
        ("match (c:patients)-[r:in]->(w:wards)", "relation binding"),
        ("match (c:patients)-[:in]-(w:wards)", "undirected"),
        ("match (c:patients)<-[:in]->(w:wards)", "bidirectional"),
    ] {
        let error = pipeline(text).err().ok_or("unsupported match accepted")?;
        assert_eq!(error.code(), "query_stage_not_available", "{text}");
        assert!(error.to_string().contains(construct), "{error}");
        assert!(text.get(error.span().start..error.span().end).is_some());
    }
    Ok(())
}

#[test]
fn quoted_match_remains_jq_with_its_exact_span() -> TestResult {
    for (text, span) in [
        ("'match(re)'", Span { start: 0, end: 11 }),
        ("patients | 'match(re)'", Span { start: 11, end: 22 }),
    ] {
        let plan = parsed(text)?;
        assert_eq!(plan.stages.len(), 1);
        assert_eq!(plan.stages[0].value, Stage::Jq("match(re)".to_owned()));
        assert_eq!(plan.stages[0].span, span);
    }
    Ok(())
}

#[test]
fn match_pipeline_preserves_following_stages_and_collection_arguments() -> TestResult {
    let plan = parsed("patients(ward: \"4B\") | match (c:patients) | count")?;
    let Source::Collection { name, arguments } = plan.source else {
        return Err("match lost its collection source".to_owned());
    };
    assert_eq!(name, "patients");
    assert_eq!(arguments.len(), 1);
    assert_eq!(arguments[0].field, "ward");
    assert_eq!(arguments[0].value, serde_json::json!("4B"));
    assert_eq!(plan.stages.len(), 2);
    let mut words = Vec::new();
    pipeline_with(
        "patients(ward: \"4B\") | match (c:patients) | count",
        |word, _| {
            words.push(word.to_owned());
        },
    )
    .map_err(|error| error.to_string())?;
    assert_eq!(words, vec!["patients", "match", "count"]);
    assert_eq!(plan.stages[0].span, Span { start: 23, end: 41 });
    assert_eq!(plan.stages[1].value, Stage::Count);
    assert_eq!(plan.stages[1].span, Span { start: 44, end: 49 });
    Ok(())
}

#[test]
fn match_query_check_reports_the_stage_and_construct_byte_ranges() -> TestResult {
    use crate::serve::query::check::{CheckRequest, check};
    use serde_json::json;

    let checked = check(&CheckRequest {
        query: "match (c:patients) | count".to_owned(),
    });
    assert_eq!(
        checked,
        json!({
            "stages": [
                {"word": "match", "span": {"start": 0, "end": 18}},
                {"word": "count", "span": {"start": 21, "end": 26}},
            ],
            "refusal": null,
        })
    );
    let query = "patients | match (c:patients) WHERE c.id = 1";
    let refused = pipeline(query).err().ok_or("WHERE was accepted")?;
    let checked = check(&CheckRequest {
        query: query.to_owned(),
    });
    assert_eq!(
        checked["stages"],
        json!([{"word": "patients", "span": {"start": 0, "end": 8}}])
    );
    assert_eq!(checked["refusal"]["code"], "query_stage_not_available");
    assert_eq!(checked["refusal"]["message"], refused.to_string());
    assert_eq!(checked["refusal"]["span"], json!({"start": 30, "end": 35}));
    Ok(())
}