use crate::datatypes::Value;
use crate::graph::algorithms::Interrupt;
use crate::graph::cdc::{self, CdcEnrichment};
use crate::graph::constraints::{ConstraintKind, EntityKind};
use crate::graph::dir_graph::DirGraph;
use crate::graph::property_types::DeclaredType;
use crate::graph::session::execute::{execute_mut, ExecuteOptions};
use std::collections::HashMap;
fn run(graph: &mut DirGraph, query: &str) {
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
execute_mut(graph, query, &opts).unwrap_or_else(|e| panic!("{query}: {e}"));
}
fn run_err(graph: &mut DirGraph, query: &str) -> String {
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
match execute_mut(graph, query, &opts) {
Ok(_) => panic!("`{query}` should have been refused"),
Err(error) => error.to_string(),
}
}
fn knows_graph() -> DirGraph {
let mut graph = DirGraph::new();
run(
&mut graph,
"CREATE (a:Person {person_id: 1})-[:KNOWS {since: 2020}]->(b:Person {person_id: 2})",
);
graph
}
fn typed_graph() -> DirGraph {
let mut graph = knows_graph();
graph
.create_rel_property_type_constraint(
"KNOWS",
"since",
DeclaredType::Integer,
&Interrupt::default(),
)
.expect("declaration over clean data");
graph
}
fn required_graph() -> DirGraph {
let mut graph = knows_graph();
graph
.create_rel_not_null_constraint("KNOWS", "since", &Interrupt::default())
.expect("declaration over clean data");
graph
}
fn edge_count(graph: &DirGraph) -> usize {
use crate::graph::storage::GraphRead;
graph.graph.edge_weights().count()
}
#[test]
fn a_refused_create_leaves_the_connection_type_metadata_untouched() {
let mut graph = typed_graph();
let before = graph.connection_type_metadata.get("KNOWS").cloned();
let error = run_err(
&mut graph,
"MATCH (a:Person {person_id: 1}), (b:Person {person_id: 2}) \
CREATE (a)-[:KNOWS {since: 'yesterday'}]->(b)",
);
assert!(error.contains("STRING"), "{error}");
assert!(error.contains("relationship of type 'KNOWS'"), "{error}");
assert_eq!(edge_count(&graph), 1, "the refused edge must not exist");
let after = graph.connection_type_metadata.get("KNOWS").cloned();
let property_types = |info: &Option<crate::graph::schema::ConnectionTypeInfo>| {
info.as_ref().map(|info| {
let mut names: Vec<String> = info.property_types.keys().cloned().collect();
names.sort();
names
})
};
assert_eq!(
property_types(&after),
property_types(&before),
"a refused CREATE must not register a property shape"
);
}
#[test]
fn a_refused_create_does_not_register_a_new_connection_type() {
let mut graph = knows_graph();
graph
.create_rel_not_null_constraint("RATES", "score", &Interrupt::default())
.expect("an empty type is vacuously clean");
let error = run_err(
&mut graph,
"MATCH (a:Person {person_id: 1}), (b:Person {person_id: 2}) CREATE (a)-[:RATES]->(b)",
);
assert!(error.contains("'score'"), "{error}");
assert!(
!graph.connection_type_metadata.contains_key("RATES"),
"the refused type must stay unknown to the schema"
);
assert!(
!graph.has_connection_type("RATES"),
"and unknown to the lightweight cache too"
);
}
#[test]
fn merge_creates_through_the_same_gate() {
let mut graph = typed_graph();
run(&mut graph, "CREATE (:Person {person_id: 3})");
let error = run_err(
&mut graph,
"MATCH (a:Person {person_id: 1}), (c:Person {person_id: 3}) \
MERGE (a)-[:KNOWS {since: 'yesterday'}]->(c)",
);
assert!(error.contains("STRING"), "{error}");
assert_eq!(edge_count(&graph), 1, "the refused edge must not exist");
}
#[test]
fn set_is_refused_by_a_declared_type() {
let mut graph = typed_graph();
let error = run_err(
&mut graph,
"MATCH ()-[r:KNOWS]->() SET r.since = 'yesterday'",
);
assert!(error.contains("STRING"), "{error}");
assert!(error.contains("KNOWS.since"), "{error}");
}
#[test]
fn set_to_null_is_refused_by_presence_and_allowed_by_a_type() {
let mut graph = required_graph();
let error = run_err(&mut graph, "MATCH ()-[r:KNOWS]->() SET r.since = null");
assert!(error.contains("must have the property 'since'"), "{error}");
let mut typed = typed_graph();
run(&mut typed, "MATCH ()-[r:KNOWS]->() SET r.since = null");
}
#[test]
fn map_assignment_is_gated_through_its_desugaring() {
for query in [
"MATCH ()-[r:KNOWS]->() SET r = {since: 'yesterday'}",
"MATCH ()-[r:KNOWS]->() SET r += {since: 'yesterday'}",
] {
let mut graph = typed_graph();
let error = run_err(&mut graph, query);
assert!(error.contains("STRING"), "for `{query}`: {error}");
}
let mut graph = required_graph();
let error = run_err(&mut graph, "MATCH ()-[r:KNOWS]->() SET r = {other: 1}");
assert!(error.contains("'since'"), "{error}");
}
#[test]
fn remove_is_refused_by_presence_and_allowed_by_a_type() {
let mut graph = required_graph();
let error = run_err(&mut graph, "MATCH ()-[r:KNOWS]->() REMOVE r.since");
assert!(error.contains("must have the property 'since'"), "{error}");
assert!(error.contains("relationship"), "{error}");
let mut typed = typed_graph();
run(&mut typed, "MATCH ()-[r:KNOWS]->() REMOVE r.since");
}
#[test]
fn the_violation_reaches_the_caller_typed() {
let mut graph = required_graph();
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
let error = match execute_mut(&mut graph, "MATCH ()-[r:KNOWS]->() REMOVE r.since", &opts) {
Ok(_) => panic!("a required property cannot be removed"),
Err(error) => error,
};
match error {
crate::error::KgError::ConstraintViolation {
kind,
node_type,
properties,
descriptor,
..
} => {
assert_eq!(kind, "NOT NULL");
assert_eq!(node_type, "KNOWS");
assert_eq!(properties, vec!["since".to_string()]);
assert_eq!(descriptor, "KNOWS.since");
}
other => panic!("expected the typed constraint error, got {other:?}"),
}
}
#[test]
fn the_violation_is_parked_under_its_message() {
use super::super::super::parser::parse_cypher;
let mut graph = required_graph();
let parsed = parse_cypher("MATCH ()-[r:KNOWS]->() REMOVE r.since").unwrap();
let error = super::execute_mutable(&mut graph, &parsed, HashMap::new(), Interrupt::default())
.expect_err("a required property cannot be removed");
let violation = graph
.take_constraint_violation_for(&error)
.expect("the violation must be parked under its own message");
assert_eq!(violation.kind, ConstraintKind::NotNull);
assert_eq!(violation.entity, EntityKind::Relationship);
assert_eq!(violation.node_type, "KNOWS");
}
#[test]
fn an_unconstrained_write_resolves_no_relationship_type() {
use super::super::edge_property_write::constrained_edge_type;
let mut graph = knows_graph();
let edge = petgraph::graph::EdgeIndex::new(0);
assert!(
constrained_edge_type(&graph, edge).is_none(),
"a graph that declares nothing must not resolve the edge's type"
);
graph
.create_rel_not_null_constraint("RATES", "score", &Interrupt::default())
.expect("declaration on another type");
assert!(
constrained_edge_type(&graph, edge).is_none(),
"a constraint on another type must not make this edge pay for it"
);
graph
.create_rel_not_null_constraint("KNOWS", "since", &Interrupt::default())
.expect("declaration on this type");
assert_eq!(
constrained_edge_type(&graph, edge).as_deref(),
Some("KNOWS"),
"the type is resolved only once something constrains it"
);
}
#[test]
fn a_refused_write_captures_nothing() {
for enrichment in [CdcEnrichment::Off, CdcEnrichment::Full] {
for (query, constrain) in [
(
"MATCH (a:Person {person_id: 1}), (b:Person {person_id: 2}) \
CREATE (a)-[:KNOWS {since: 'yesterday'}]->(b)",
true,
),
("MATCH ()-[r:KNOWS]->() SET r.since = 'yesterday'", true),
("MATCH ()-[r:KNOWS]->() REMOVE r.since", false),
] {
let mut graph = if constrain {
typed_graph()
} else {
required_graph()
};
cdc::enable(&mut graph, None, enrichment).expect("enable capture");
let from = cdc::status(&graph).expect("enabled").current;
let error = run_err(&mut graph, query);
assert!(!error.is_empty());
cdc::drain_at_commit(&mut graph);
let published = cdc::read(&graph, from, None, &[]).expect("enabled");
assert!(
published.is_empty(),
"`{query}` was refused under {enrichment:?} but published {published:?}"
);
}
}
}
#[test]
fn a_refused_bulk_frame_captures_nothing() {
use crate::datatypes::DataFrame;
use crate::graph::mutation::maintain::add_connections;
let mut graph = required_graph();
cdc::enable(&mut graph, None, CdcEnrichment::Off).expect("enable capture");
let from = cdc::status(&graph).expect("enabled").current;
let frame = DataFrame::from_cypher_rows(
vec!["s".to_string(), "t".to_string()],
vec![vec![Value::Int64(1), Value::Int64(2)]],
)
.unwrap();
let error = add_connections(
&mut graph,
frame,
"KNOWS".to_string(),
"Person".to_string(),
"s".to_string(),
"Person".to_string(),
"t".to_string(),
None,
None,
None,
)
.expect_err("the frame supplies no `since` for a new pair");
assert!(error.contains("'since'"), "{error}");
cdc::drain_at_commit(&mut graph);
let published = cdc::read(&graph, from, None, &[]).expect("enabled");
assert!(
published.is_empty(),
"a refused frame published {published:?}"
);
}