#[path = "matching_support.rs"]
mod support;
use std::num::NonZeroU64;
use serde_json::{Value, json};
use support::{Fixture, TestResult, request, values};
const FOUR: NonZeroU64 = NonZeroU64::MIN.saturating_add(3);
const HOP: &str = "match (c:patients)-[:admitted_to]->(w:wards)";
#[derive(serde::Deserialize)]
struct Answer {
rows: Vec<Row>,
}
#[derive(serde::Deserialize)]
struct Row {
value: Bindings,
}
#[derive(serde::Deserialize)]
struct Bindings {
c: Box<serde_json::value::RawValue>,
w: Box<serde_json::value::RawValue>,
}
#[test]
fn match_one_hop_returns_one_value_per_edge_in_origin_then_far_id_order() -> TestResult {
let fixture = Fixture::new()?;
let answer = fixture.run(&request(HOP, FOUR), &[])?;
let rows = answer
.get("rows")
.and_then(Value::as_array)
.ok_or("rows missing")?;
assert_eq!(
values(&answer)?,
vec![
json!({"c":fixture.value("patients", "p0")?,"w":{"ward":"A"}}),
json!({"c":fixture.value("patients", "p0")?,"w":{"ward":"B"}}),
json!({"c":{"name":"Bea"},"w":{"ward":"A"}}),
]
);
assert_eq!(
rows.iter()
.map(|row| &row["origin"]["id"])
.collect::<Vec<_>>(),
vec![&json!("p0"), &json!("p0"), &json!("p1")]
);
for row in rows {
assert_eq!(row["origin"]["collection"], "patients");
assert!(row["origin"]["score"].is_null());
assert_eq!(
row["origin"]["version"],
fixture.revision(
"patients",
row["origin"]["id"].as_str().ok_or("id missing")?
)?
);
}
assert_eq!(answer["records_read"], 8);
assert_eq!(answer["stages"], json!(["match"]));
assert!(answer["next"].is_null());
Ok(())
}
#[test]
fn match_incoming_uses_the_first_node_origin_and_filters_kind_and_far_label() -> TestResult {
let fixture = Fixture::new()?;
let answer = fixture.run(
&request("match (w:wards)<-[:admitted_to]-(c:patients)", FOUR),
&[],
)?;
let rows = answer
.get("rows")
.and_then(Value::as_array)
.ok_or("rows missing")?;
assert_eq!(
values(&answer)?,
vec![
json!({"w":{"ward":"A"},"c":fixture.value("patients", "p0")?}),
json!({"w":{"ward":"A"},"c":{"name":"Bea"}}),
json!({"w":{"ward":"B"},"c":fixture.value("patients", "p0")?}),
]
);
assert_eq!(
rows.iter()
.map(|row| &row["origin"]["id"])
.collect::<Vec<_>>(),
vec![&json!("w0"), &json!("w0"), &json!("w1")]
);
assert!(
rows.iter()
.all(|row| row["origin"]["collection"] == "wards")
);
Ok(())
}
#[test]
fn match_denied_far_record_and_its_edge_are_absent_and_not_charged() -> TestResult {
let mut fixture = Fixture::new()?;
fixture.scan(6)?;
let answer = fixture.run(&request(HOP, FOUR), &[("wards", "w1")])?;
assert_eq!(values(&answer)?.len(), 2);
assert_eq!(answer["records_read"], 6);
assert!(!serde_json::to_string(&answer)?.contains("\"ward\":\"B\""));
let all_denied = fixture.run(&request(HOP, FOUR), &[("wards", "w0"), ("wards", "w1")])?;
assert!(values(&all_denied)?.is_empty());
assert_eq!(all_denied["records_read"], 2);
Ok(())
}
#[test]
fn match_denied_first_record_never_expands_its_edges() -> TestResult {
let fixture = Fixture::new()?;
let answer = fixture.run(&request(HOP, FOUR), &[("patients", "p0")])?;
assert_eq!(
values(&answer)?,
vec![json!({"c":{"name":"Bea"},"w":{"ward":"A"}})]
);
assert_eq!(answer["records_read"], 4);
Ok(())
}
#[test]
fn match_each_label_must_name_a_collection_even_when_the_source_is_empty() -> TestResult {
let fixture = Fixture::new()?;
for text in [
"match (c:absent)",
"match (c:patients)-[:admitted_to]->(w:absent)",
"match (c:empty)-[:admitted_to]->(w:absent)",
"match (c:empty)-[:admitted_to]->(w:wards)<-[:staffed_by]-(n:absent)",
] {
let failure = fixture
.run(&request(text, FOUR), &[])
.err()
.ok_or("missing label accepted")?;
assert_eq!(failure.code, "collection_missing", "{text}");
assert!(failure.message.contains("absent"));
}
Ok(())
}
#[test]
fn match_raw_bindings_are_byte_identical_to_both_stored_records() -> TestResult {
let fixture = Fixture::new()?;
let raw = fixture.run_raw(&request(HOP, FOUR), &[])?;
let answer: Answer = serde_json::from_str(raw.get())?;
assert_eq!(answer.rows.len(), 3);
assert_eq!(
answer.rows[0].value.c.get().as_bytes(),
fixture.bytes("patients", "p0")?
);
assert_eq!(
answer.rows[0].value.w.get().as_bytes(),
fixture.bytes("wards", "w0")?
);
assert_eq!(
answer.rows[1].value.w.get().as_bytes(),
fixture.bytes("wards", "w1")?
);
let one = fixture.run_raw(&request("match (c:patients)", FOUR), &[])?;
assert!(
one.get()
.contains(std::str::from_utf8(&fixture.bytes("patients", "p0")?)?)
);
Ok(())
}
#[test]
fn match_scan_bound_charges_edges_and_far_reads_and_never_returns_a_partial_hop() -> TestResult {
let mut fixture = Fixture::new()?;
fixture.scan(7)?;
let failure = fixture
.run(&request(HOP, FOUR), &[])
.err()
.ok_or("incomplete match accepted")?;
assert_eq!(failure.code, "query_scan_limit");
fixture.scan(8)?;
assert_eq!(fixture.run(&request(HOP, FOUR), &[])?["records_read"], 8);
let small = fixture
.run(&request(HOP, NonZeroU64::MIN), &[])
.err()
.ok_or("partial hop output page accepted")?;
assert_eq!(small.code, "query_output_limit");
Ok(())
}
#[test]
fn match_one_node_pages_and_keeps_the_record_revision() -> TestResult {
let fixture = Fixture::new()?;
let mut ask = request("match (c:patients)", NonZeroU64::MIN);
let first = fixture.run(&ask, &[])?;
assert_eq!(
values(&first)?,
vec![json!({"c":fixture.value("patients", "p0")?})]
);
ask.continuation = Some(
first
.get("next")
.and_then(Value::as_str)
.ok_or("next missing")?
.to_owned(),
);
let second = fixture.run(&ask, &[])?;
assert_eq!(values(&second)?, vec![json!({"c":{"name":"Bea"}})]);
assert_eq!(second["records_read"], 2);
assert!(second["next"].is_null());
Ok(())
}
#[test]
fn match_limit_count_and_jq_receive_individual_bindings() -> TestResult {
let fixture = Fixture::new()?;
let answer = fixture.run(&request(&format!("{HOP} | limit(2) | count"), FOUR), &[])?;
assert_eq!(values(&answer)?, vec![json!(2)]);
let jq = fixture.run(
&request(&format!("{HOP} | '.c.name' | limit(2)"), FOUR),
&[],
)?;
assert_eq!(values(&jq)?, vec![json!("Ada"), json!("Ada")]);
let scores = fixture.run(&request(&format!("{HOP} | '$score'"), FOUR), &[])?;
assert_eq!(
values(&scores)?,
vec![json!(null), json!(null), json!(null)]
);
assert!(
fixture
.inputs()?
.iter()
.all(|frame| frame["bindings"]["score"].is_null())
);
let empty = fixture.run(
&request("match (c:empty)-[:admitted_to]->(w:wards) | count", FOUR),
&[],
)?;
assert_eq!(values(&empty)?, vec![json!(0)]);
Ok(())
}
#[test]
fn match_execution_library_defaults_run_the_complete_graph_contract() -> TestResult {
let fixture = Fixture::new()?;
let group: Value = serde_json::from_str(include_str!("../library/matching.json"))?;
assert_eq!(group["group"], "Executing labelled matches");
let entries = group
.get("entries")
.and_then(Value::as_array)
.ok_or("entries missing")?;
assert_eq!(entries.len(), 2);
for entry in entries {
let mut text = entry
.get("query")
.and_then(Value::as_str)
.ok_or("query missing")?
.to_owned();
let change = entry
.get("change")
.and_then(Value::as_str)
.ok_or("change missing")?;
for (name, value) in entry
.get("defaults")
.and_then(Value::as_object)
.ok_or("defaults missing")?
{
let token = format!("${{{name}}}");
assert!(text.contains(&token));
assert!(change.contains(&token));
text = text.replace(&token, value.as_str().ok_or("default is not text")?);
}
assert!(!text.contains("${"));
let answer = fixture.run(&request(&text, FOUR), &[])?;
assert_eq!(values(&answer)?.len(), 2);
assert!(answer["next"].is_null());
}
Ok(())
}
#[path = "matching_chain_tests.rs"]
mod chain_tests;