use super::super::{Span, Spanned, Stage};
use super::{Direction, Node, Relation, read};
type TestResult = Result<(), String>;
fn accepted(text: &str, start: usize) -> Result<Spanned<Stage>, String> {
read(text, start).map_err(|error| error.to_string())
}
fn text(value: &str, start: usize, end: usize) -> Spanned<String> {
Spanned {
value: value.to_owned(),
span: Span { start, end },
}
}
fn patients() -> Node {
Node {
binding: text("c", 7, 8),
label: text("patients", 9, 17),
span: Span { start: 6, end: 18 },
}
}
fn wards() -> Node {
Node {
binding: text("w", 36, 37),
label: text("wards", 38, 43),
span: Span { start: 35, end: 44 },
}
}
#[test]
fn match_single_node_keeps_each_exact_span() -> TestResult {
assert_eq!(
accepted("match (c:patients)", 0)?,
Spanned {
value: Stage::Match {
nodes: vec![patients()],
relations: Vec::new(),
},
span: Span { start: 0, end: 18 },
}
);
Ok(())
}
#[test]
fn match_both_arrow_directions_keep_each_exact_span() -> TestResult {
for (query, direction, start, end) in [
(
"match (c:patients)-[:admitted_to]->(w:wards)",
Direction::Outgoing,
21,
32,
),
(
"match (c:patients)<-[:admitted_to]-(w:wards)",
Direction::Incoming,
22,
33,
),
] {
assert_eq!(
accepted(query, 0)?,
Spanned {
value: Stage::Match {
nodes: vec![patients(), wards()],
relations: vec![Relation {
kind: text("admitted_to", start, end),
direction,
span: Span { start: 18, end: 35 },
}],
},
span: Span { start: 0, end: 44 },
},
"{query}"
);
}
Ok(())
}
#[test]
fn match_offsets_are_utf8_bytes_and_stop_before_the_outer_pipe() -> TestResult {
let query =
"\u{2003} match ( _c2 : studio.pages ) - [ : admitted_to ] -> ( w : wards ) | count";
assert_eq!(
accepted(query, 100)?,
Spanned {
value: Stage::Match {
nodes: vec![
Node {
binding: text("_c2", 112, 115),
label: text("studio.pages", 118, 130),
span: Span {
start: 110,
end: 132
},
},
Node {
binding: text("w", 158, 159),
label: text("wards", 162, 167),
span: Span {
start: 156,
end: 169
},
},
],
relations: vec![Relation {
kind: text("admitted_to", 139, 150),
direction: Direction::Outgoing,
span: Span {
start: 133,
end: 155
},
}],
},
span: Span {
start: 104,
end: 169
},
}
);
Ok(())
}
#[test]
fn match_binding_names_have_the_declared_ascii_grammar() -> TestResult {
for binding in ["c", "_", "_patient_2", "c0"] {
let query = format!("match ({binding}:patients)");
let parsed = accepted(&query, 0)?;
let Stage::Match { nodes, relations } = parsed.value else {
return Err("match returned another stage".into());
};
assert_eq!(nodes.len(), 1);
assert_eq!(nodes[0].binding.value, binding);
assert_eq!(nodes[0].binding.span.start, 7);
assert_eq!(nodes[0].binding.span.end, 7 + binding.len());
assert!(relations.is_empty());
}
for binding in ["C", "1c", "c-1", "c.name", "é", "cÉ", ""] {
let query = format!("match ({binding}:patients)");
let refused = read(&query, 0).err().ok_or("invalid binding accepted")?;
assert_eq!(refused.code(), "query_stage_not_available", "{query}");
assert!(refused.to_string().contains("binding name"), "{refused}");
}
Ok(())
}
#[test]
fn match_unsupported_constructs_keep_exact_refusal_spans() -> TestResult {
for (query, construct, start, end) in [
("match (c)", "no label", 8, 9),
("match (c:patients {id: 1})", "property map", 18, 19),
("match (c:patients) WHERE c.id = 1", "WHERE", 19, 24),
(
"match (c:patients)-[r:in]->(w:wards)",
"relation binding",
20,
21,
),
] {
let refused = read(query, 50)
.err()
.ok_or("unsupported construct accepted")?;
assert_eq!(refused.code(), "query_stage_not_available", "{query}");
assert!(refused.to_string().contains(construct), "{refused}");
assert_eq!(
refused.span(),
Span {
start: 50 + start,
end: 50 + end
},
"{query}"
);
}
Ok(())
}
#[test]
fn match_malformed_patterns_never_become_another_language() -> TestResult {
for (query, construct) in [
("match", "labelled node"),
("match(re)", "no label"),
("match (c:)", "no label"),
("match (c:patients", "unclosed node"),
("match (c:patients:other)", "multiple labels"),
("match (c:patients) (w:wards)", "without a relation"),
("match (c:patients), (w:wards)", "multiple patterns"),
("match (c:patients)-[]->(w:wards)", "no kind"),
("match (c:patients)-[:]->(w:wards)", "no kind"),
("match (c:patients)-[:in*]->(w:wards)", "variable-length"),
("match (c:patients)-[:in]-(w:wards)", "undirected"),
("match (c:patients)<-[:in]->(w:wards)", "bidirectional"),
("match (c:patients)-[:in]->", "node pattern"),
("match (c:patients) RETURN c", "suffix"),
("match (c:patients) {id: 1}", "property map"),
] {
let refused = read(query, 0).err().ok_or("malformed match accepted")?;
assert_eq!(refused.code(), "query_stage_not_available", "{query}");
assert!(refused.to_string().contains(construct), "{refused}");
assert!(
query
.get(refused.span().start..refused.span().end)
.is_some()
);
}
Ok(())
}