use serde_json::value::RawValue;
use serde_json::{Value, json};
use super::support::{Fixture, TestResult, request, values};
const SIX: std::num::NonZeroU64 = std::num::NonZeroU64::MIN.saturating_add(5);
const CHAIN: &str = "match (c:patients)-[:admitted_to]->(w:wards)<-[:staffed_by]-(n:nurses)";
fn expected(fixture: &Fixture) -> Result<Vec<Value>, Box<dyn std::error::Error>> {
Ok(vec![
json!({"c":fixture.value("patients", "p0")?,"w":{"ward":"A"},"n":{"name":"Nia"}}),
json!({"c":fixture.value("patients", "p0")?,"w":{"ward":"A"},"n":{"name":"Noor"}}),
json!({"c":fixture.value("patients", "p0")?,"w":{"ward":"B"},"n":{"name":"Nell"}}),
json!({"c":{"name":"Bea"},"w":{"ward":"A"},"n":{"name":"Nia"}}),
json!({"c":{"name":"Bea"},"w":{"ward":"A"},"n":{"name":"Noor"}}),
])
}
#[test]
fn match_two_hops_emit_exact_depth_first_rows_with_first_origin() -> TestResult {
let fixture = Fixture::new()?;
let answer = fixture.run(&request(CHAIN, SIX), &[])?;
assert_eq!(values(&answer)?, expected(&fixture)?);
let rows = answer
.get("rows")
.and_then(Value::as_array)
.ok_or("rows missing")?;
for (row, id) in rows.iter().zip(["p0", "p0", "p0", "p1", "p1"]) {
assert_eq!(row["origin"]["id"], id);
assert_eq!(row["origin"]["collection"], "patients");
assert_eq!(row["origin"]["version"], fixture.revision("patients", id)?);
assert!(row["origin"]["score"].is_null());
assert_eq!(row["value"].as_object().ok_or("bindings missing")?.len(), 3);
}
assert_eq!(answer["records_read"], 18);
assert!(answer["next"].is_null());
Ok(())
}
#[test]
fn match_denied_middle_drops_its_entire_subtree_without_charging() -> TestResult {
let mut fixture = Fixture::new()?;
fixture.scan(6)?;
let answer = fixture.run(&request(CHAIN, SIX), &[("wards", "w0")])?;
assert_eq!(values(&answer)?, vec![expected(&fixture)?[2].clone()]);
assert_eq!(answer["records_read"], 6);
let all_denied = fixture.run(&request(CHAIN, SIX), &[("wards", "w0"), ("wards", "w1")])?;
assert!(values(&all_denied)?.is_empty());
assert_eq!(all_denied["records_read"], 2);
Ok(())
}
#[test]
fn match_denied_last_record_drops_only_its_rows_without_charging() -> TestResult {
let mut fixture = Fixture::new()?;
fixture.scan(14)?;
let answer = fixture.run(&request(CHAIN, SIX), &[("nurses", "n1")])?;
let all = expected(&fixture)?;
assert_eq!(
values(&answer)?,
vec![all[0].clone(), all[2].clone(), all[3].clone()]
);
assert_eq!(answer["records_read"], 14);
fixture.scan(8)?;
let empty = fixture.run(
&request(CHAIN, SIX),
&[("nurses", "n0"), ("nurses", "n1"), ("nurses", "n2")],
)?;
assert!(values(&empty)?.is_empty());
assert_eq!(empty["records_read"], 8);
Ok(())
}
#[test]
fn match_scan_allowance_charges_every_depth_and_refuses_an_incomplete_chain() -> TestResult {
let mut fixture = Fixture::new()?;
for limit in [1, 3, 4, 5, 6, 7, 8, 17] {
fixture.scan(limit)?;
let refusal = fixture
.run(&request(CHAIN, SIX), &[])
.err()
.ok_or("partial chain accepted")?;
assert_eq!(refusal.code, "query_scan_limit", "limit {limit}");
}
fixture.scan(18)?;
let answer = fixture.run(&request(CHAIN, SIX), &[])?;
assert_eq!(values(&answer)?, expected(&fixture)?);
assert_eq!(answer["records_read"], 18);
Ok(())
}
#[test]
fn match_repeated_binding_across_three_nodes_is_refused_by_name() -> TestResult {
let fixture = Fixture::new()?;
for query in [
"match (c:patients)-[:admitted_to]->(w:wards)<-[:staffed_by]-(c:nurses)",
"match (c:patients)-[:admitted_to]->(w:wards)<-[:staffed_by]-(w:nurses)",
] {
let refusal = fixture
.run(&request(query, SIX), &[])
.err()
.ok_or("repeated binding accepted")?;
assert_eq!(refusal.code, "query_match_binding");
}
Ok(())
}
#[test]
fn match_second_hop_skips_far_labels_and_charges_only_the_edges() -> TestResult {
let mut fixture = Fixture::new()?;
fixture.scan(13)?;
let answer = fixture.run(
&request(
"match (c:patients)-[:admitted_to]->(w:wards)<-[:staffed_by]-(n:patients)",
SIX,
),
&[],
)?;
assert!(values(&answer)?.is_empty());
assert_eq!(answer["records_read"], 13);
Ok(())
}
#[derive(serde::Deserialize)]
struct Answer {
rows: Vec<Row>,
}
#[derive(serde::Deserialize)]
struct Row {
value: Bindings,
}
#[derive(serde::Deserialize)]
struct Bindings {
c: Box<RawValue>,
w: Box<RawValue>,
n: Box<RawValue>,
}
#[test]
fn match_chain_bindings_preserve_every_stored_record_byte() -> TestResult {
let fixture = Fixture::new()?;
let raw = fixture.run_raw(&request(CHAIN, SIX), &[])?;
let answer: Answer = serde_json::from_str(raw.get())?;
assert_eq!(answer.rows.len(), 5);
for (row, patient, ward, nurse) in [
(&answer.rows[0], "p0", "w0", "n0"),
(&answer.rows[1], "p0", "w0", "n1"),
(&answer.rows[2], "p0", "w1", "n2"),
(&answer.rows[3], "p1", "w0", "n0"),
(&answer.rows[4], "p1", "w0", "n1"),
] {
assert_eq!(
row.value.c.get().as_bytes(),
fixture.bytes("patients", patient)?
);
assert_eq!(row.value.w.get().as_bytes(), fixture.bytes("wards", ward)?);
assert_eq!(
row.value.n.get().as_bytes(),
fixture.bytes("nurses", nurse)?
);
}
Ok(())
}