mod support {
pub mod parser_hardening;
}
use proptest::prelude::*;
use reddb_server::storage::query::parser::{self, ParseError, ParserLimits};
use support::parser_hardening::{
self as harness, assert_no_panic_on, corpus::graph_dsl_adversarial_inputs, graph_dsl_grammar,
HardenedParser,
};
pub struct GraphDslParser;
impl HardenedParser for GraphDslParser {
type Error = ParseError;
fn parse(input: &str) -> Result<(), Self::Error> {
parser::parse(input).map(|_| ())
}
fn parse_with_limits(input: &str, limits: ParserLimits) -> Result<(), Self::Error> {
let mut p = parser::Parser::with_limits(input, limits)?;
p.parse().map(|_| ())
}
}
#[test]
fn graph_dsl_parser_does_not_panic_on_adversarial_corpus() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
for (name, input) in graph_dsl_adversarial_inputs() {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_no_panic_on::<GraphDslParser>(&input);
}));
if result.is_err() {
panic!("graph DSL adversarial corpus entry {} panicked", name);
}
}
})
.expect("spawn corpus thread");
handle.join().expect("corpus thread panic");
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
max_shrink_iters: 64,
..ProptestConfig::default()
})]
#[test]
fn proptest_match_simple_roundtrips(s in graph_dsl_grammar::match_simple_stmt()) {
harness::roundtrip_property::<GraphDslParser>(&s);
prop_assert!(
GraphDslParser::parse(&s).is_ok(),
"match simple did not parse: {}", s
);
}
#[test]
fn proptest_match_pattern_path_roundtrips(
s in graph_dsl_grammar::match_pattern_path_stmt(),
) {
harness::roundtrip_property::<GraphDslParser>(&s);
prop_assert!(
GraphDslParser::parse(&s).is_ok(),
"match pattern path did not parse: {}", s
);
}
#[test]
fn proptest_match_with_where_roundtrips(
s in graph_dsl_grammar::match_with_where_stmt(),
) {
harness::roundtrip_property::<GraphDslParser>(&s);
prop_assert!(
GraphDslParser::parse(&s).is_ok(),
"match with where did not parse: {}", s
);
}
#[test]
fn proptest_path_query_roundtrips(s in graph_dsl_grammar::path_query_stmt()) {
harness::roundtrip_property::<GraphDslParser>(&s);
prop_assert!(
GraphDslParser::parse(&s).is_ok(),
"path query did not parse: {}", s
);
}
#[test]
fn proptest_graph_traversal_roundtrips(
s in graph_dsl_grammar::graph_traversal_stmt(),
) {
harness::roundtrip_property::<GraphDslParser>(&s);
prop_assert!(
GraphDslParser::parse(&s).is_ok(),
"graph traversal did not parse: {}", s
);
}
#[test]
fn proptest_graph_arbitrary_suffix_no_panic(
prefix in prop_oneof![
Just("MATCH ".to_string()),
Just("PATH ".to_string()),
Just("GRAPH ".to_string()),
],
suffix in ".{0,512}",
) {
let s = format!("{}{}", prefix, suffix);
harness::roundtrip_property::<GraphDslParser>(&s);
}
#[test]
fn proptest_create_node_attempt_no_panic(
s in graph_dsl_grammar::create_node_attempt_stmt(),
) {
harness::roundtrip_property::<GraphDslParser>(&s);
}
#[test]
fn proptest_create_edge_attempt_no_panic(
s in graph_dsl_grammar::create_edge_attempt_stmt(),
) {
harness::roundtrip_property::<GraphDslParser>(&s);
}
#[test]
fn proptest_graph_input_size_limit_enforced(len in 200usize..2000) {
let limits = ParserLimits {
max_input_bytes: 64,
..ParserLimits::default()
};
let body = "x".repeat(len);
let input = format!("MATCH (a) RETURN {}", body);
let r = GraphDslParser::parse_with_limits(&input, limits);
prop_assert!(r.is_err(), "oversized graph input must error");
}
}
use reddb_server::storage::query::ast::{
EdgeDirection, GraphCommand, GraphCommandOrderBy, NodeSelector, QueryExpr,
};
fn parse_query(input: &str) -> QueryExpr {
parser::parse(input)
.unwrap_or_else(|e| panic!("expected ok for {input:?}, got error: {e}"))
.query
}
#[test]
fn match_single_hop_outgoing_parses() {
let q = parse_query("MATCH (a:person)-[r:KNOWS]->(b:person) RETURN a, b");
match q {
QueryExpr::Graph(g) => {
assert_eq!(g.pattern.nodes.len(), 2);
assert_eq!(g.pattern.edges.len(), 1);
assert_eq!(g.pattern.nodes[0].alias, "a");
assert_eq!(g.pattern.nodes[0].node_label.as_deref(), Some("person"));
assert_eq!(g.pattern.edges[0].direction, EdgeDirection::Outgoing);
assert_eq!(g.pattern.edges[0].edge_label.as_deref(), Some("knows"));
assert_eq!(g.return_.len(), 2);
}
other => panic!("expected Graph, got {other:?}"),
}
}
#[test]
fn match_incoming_edge_parses() {
let q = parse_query("MATCH (a)<-[r:FOLLOWS]-(b:person) RETURN b");
match q {
QueryExpr::Graph(g) => {
assert_eq!(g.pattern.edges.len(), 1);
assert_eq!(g.pattern.edges[0].direction, EdgeDirection::Incoming);
}
other => panic!("expected Graph, got {other:?}"),
}
}
#[test]
fn match_multi_hop_pattern_path_parses() {
let q = parse_query(
"MATCH (a:person)-[:WORKS_AT]->(c:company)-[:LOCATED_IN]->(city) RETURN a, c, city",
);
match q {
QueryExpr::Graph(g) => {
assert_eq!(g.pattern.nodes.len(), 3);
assert_eq!(g.pattern.edges.len(), 2);
assert_eq!(g.return_.len(), 3);
}
other => panic!("expected Graph, got {other:?}"),
}
}
#[test]
fn match_with_where_clause_parses() {
let q = parse_query(
"MATCH (a:person)-[r:COLLABORATES]->(b:person) WHERE a.department = 'engineering' \
RETURN a, b",
);
match q {
QueryExpr::Graph(g) => {
assert!(g.filter.is_some(), "WHERE filter must be present");
assert_eq!(
g.pattern.edges[0].edge_label.as_deref(),
Some("collaborates")
);
}
other => panic!("expected Graph, got {other:?}"),
}
}
#[test]
fn match_with_property_filter_parses() {
let q = parse_query("MATCH (a:person {name: 'alice'}) RETURN a");
match q {
QueryExpr::Graph(g) => {
assert_eq!(g.pattern.nodes[0].properties.len(), 1);
assert_eq!(g.pattern.nodes[0].properties[0].name, "name");
}
other => panic!("expected Graph, got {other:?}"),
}
}
#[test]
fn match_with_variable_length_edge_parses() {
let q = parse_query("MATCH (a:person)-[r*1..3]->(b:person) RETURN a, b");
match q {
QueryExpr::Graph(g) => {
assert_eq!(g.pattern.edges[0].min_hops, 1);
assert_eq!(g.pattern.edges[0].max_hops, 3);
}
other => panic!("expected Graph, got {other:?}"),
}
}
#[test]
fn graph_neighborhood_command_parses() {
let q = parse_query("GRAPH NEIGHBORHOOD 'alice' DEPTH 2 DIRECTION outgoing");
match q {
QueryExpr::GraphCommand(GraphCommand::Neighborhood {
source,
depth,
direction,
edge_labels,
}) => {
assert_eq!(source, "alice");
assert_eq!(depth, 2);
assert_eq!(direction, "outgoing");
assert_eq!(edge_labels, None);
}
other => panic!("expected Neighborhood, got {other:?}"),
}
}
#[test]
fn graph_neighborhood_edges_in_filter_parses() {
let q = parse_query("GRAPH NEIGHBORHOOD 'alice' EDGES IN ('EATS', 'KILLS') DEPTH 2");
match q {
QueryExpr::GraphCommand(GraphCommand::Neighborhood {
source,
depth,
edge_labels,
..
}) => {
assert_eq!(source, "alice");
assert_eq!(depth, 2);
assert_eq!(
edge_labels,
Some(vec!["EATS".to_string(), "KILLS".to_string()])
);
}
other => panic!("expected Neighborhood, got {other:?}"),
}
}
#[test]
fn graph_shortest_path_command_parses() {
let q = parse_query("GRAPH SHORTEST_PATH 'alice' TO 'bob' ALGORITHM dijkstra");
match q {
QueryExpr::GraphCommand(GraphCommand::ShortestPath {
source,
target,
algorithm,
..
}) => {
assert_eq!(source, "alice");
assert_eq!(target, "bob");
assert_eq!(algorithm, "dijkstra");
}
other => panic!("expected ShortestPath, got {other:?}"),
}
}
#[test]
fn graph_traverse_command_parses() {
let q = parse_query("GRAPH TRAVERSE 'alice' STRATEGY bfs DEPTH 3");
match q {
QueryExpr::GraphCommand(GraphCommand::Traverse {
source,
strategy,
depth,
edge_labels,
..
}) => {
assert_eq!(source, "alice");
assert_eq!(strategy, "bfs");
assert_eq!(depth, 3);
assert_eq!(edge_labels, None);
}
other => panic!("expected Traverse, got {other:?}"),
}
}
#[test]
fn graph_traverse_edges_in_filter_parses() {
let q = parse_query("GRAPH TRAVERSE FROM 'alice' STRATEGY bfs EDGES IN ('EATS') MAX_DEPTH 3");
match q {
QueryExpr::GraphCommand(GraphCommand::Traverse {
source,
strategy,
depth,
edge_labels,
..
}) => {
assert_eq!(source, "alice");
assert_eq!(strategy, "bfs");
assert_eq!(depth, 3);
assert_eq!(edge_labels, Some(vec!["EATS".to_string()]));
}
other => panic!("expected Traverse, got {other:?}"),
}
}
#[test]
fn graph_traverse_from_strategy_max_depth_form_parses() {
let q = parse_query("GRAPH TRAVERSE FROM 'alice' STRATEGY bfs DIRECTION outgoing MAX_DEPTH 3");
match q {
QueryExpr::GraphCommand(GraphCommand::Traverse {
source,
strategy,
depth,
direction,
..
}) => {
assert_eq!(source, "alice");
assert_eq!(strategy, "bfs");
assert_eq!(depth, 3);
assert_eq!(direction, "outgoing");
}
other => panic!("expected Traverse, got {other:?}"),
}
}
#[test]
fn graph_shortest_path_from_to_form_parses() {
let q = parse_query("GRAPH SHORTEST_PATH FROM 'a' TO 'b' ALGORITHM dijkstra");
match q {
QueryExpr::GraphCommand(GraphCommand::ShortestPath {
source,
target,
algorithm,
..
}) => {
assert_eq!(source, "a");
assert_eq!(target, "b");
assert_eq!(algorithm, "dijkstra");
}
other => panic!("expected ShortestPath, got {other:?}"),
}
}
#[test]
fn graph_shortest_path_limit_parses() {
let q = parse_query("GRAPH SHORTEST_PATH FROM 'a' TO 'b' LIMIT 0");
match q {
QueryExpr::GraphCommand(GraphCommand::ShortestPath {
source,
target,
limit,
..
}) => {
assert_eq!(source, "a");
assert_eq!(target, "b");
assert_eq!(limit, Some(0));
}
other => panic!("expected ShortestPath, got {other:?}"),
}
}
#[test]
fn graph_components_limit_parses() {
let q = parse_query("GRAPH COMPONENTS MODE weak LIMIT 2");
match q {
QueryExpr::GraphCommand(GraphCommand::Components {
mode,
limit,
order_by,
}) => {
assert_eq!(mode, "weak");
assert_eq!(limit, Some(2));
assert!(order_by.is_none());
}
other => panic!("expected Components, got {other:?}"),
}
}
#[test]
fn graph_community_order_by_size_limit_parses() {
let q = parse_query("GRAPH COMMUNITY ALGORITHM louvain ORDER BY size DESC LIMIT 5");
match q {
QueryExpr::GraphCommand(GraphCommand::Community {
algorithm,
max_iterations,
limit,
order_by: Some(GraphCommandOrderBy { metric, ascending }),
}) => {
assert_eq!(algorithm, "louvain");
assert_eq!(max_iterations, 100);
assert_eq!(metric, "size");
assert!(!ascending);
assert_eq!(limit, Some(5));
}
other => panic!("expected Community with ORDER BY size DESC LIMIT 5, got {other:?}"),
}
}
#[test]
fn graph_components_order_by_component_size_asc_limit_parses() {
let q = parse_query("GRAPH COMPONENTS MODE weak ORDER BY component_size ASC LIMIT 2");
match q {
QueryExpr::GraphCommand(GraphCommand::Components {
mode,
limit,
order_by: Some(GraphCommandOrderBy { metric, ascending }),
}) => {
assert_eq!(mode, "weak");
assert_eq!(metric, "component_size");
assert!(ascending);
assert_eq!(limit, Some(2));
}
other => {
panic!("expected Components with ORDER BY component_size ASC LIMIT 2, got {other:?}")
}
}
}
#[test]
fn path_query_with_via_parses() {
let q = parse_query("PATH FROM host('a') TO host('b') VIA [:KNOWS, :FOLLOWS]");
match q {
QueryExpr::Path(p) => {
assert!(matches!(p.from, NodeSelector::ById(ref id) if id == "a"));
assert!(matches!(p.to, NodeSelector::ById(ref id) if id == "b"));
assert_eq!(p.via.len(), 2);
}
other => panic!("expected Path, got {other:?}"),
}
}