use std::fmt;
use serde::{Deserialize, Serialize};
use crate::datatypes::values::Value;
pub type UniqueConstraintKey = (String, Vec<String>);
pub fn normalize_properties(properties: &[String]) -> Vec<String> {
let mut sorted = properties.to_vec();
sorted.sort();
sorted.dedup();
sorted
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum EntityKind {
#[default]
Node,
Relationship,
}
impl EntityKind {
pub fn is_node(&self) -> bool {
matches!(self, EntityKind::Node)
}
pub fn keyword(self) -> &'static str {
match self {
EntityKind::Node => "NODE",
EntityKind::Relationship => "RELATIONSHIP",
}
}
pub(crate) fn subject(self, type_name: &str) -> String {
match self {
EntityKind::Node => format!("a node with label '{type_name}'"),
EntityKind::Relationship => format!("a relationship of type '{type_name}'"),
}
}
pub(crate) fn noun(self, count: usize) -> &'static str {
match (self, count) {
(EntityKind::Node, 1) => "node",
(EntityKind::Node, _) => "nodes",
(EntityKind::Relationship, 1) => "relationship",
(EntityKind::Relationship, _) => "relationships",
}
}
pub(crate) fn type_noun(self) -> &'static str {
match self {
EntityKind::Node => "node type",
EntityKind::Relationship => "relationship type",
}
}
pub(crate) fn duplicate_advice(self) -> &'static str {
match self {
EntityKind::Node => "Use MERGE to upsert an existing node instead of CREATE.",
EntityKind::Relationship => {
"MATCH the existing relationship and SET its properties instead of creating a \
second one."
}
}
}
pub(crate) fn drop_presence_advice(self) -> &'static str {
match self {
EntityKind::Node => "or drop the requirement from the node type's schema.",
EntityKind::Relationship => "or drop the constraint.",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConstraintKind {
Unique,
NotNull,
NodeKey,
PropertyType,
}
impl ConstraintKind {
pub fn keyword(&self) -> &'static str {
match self {
ConstraintKind::Unique => "UNIQUE",
ConstraintKind::NotNull => "NOT NULL",
ConstraintKind::NodeKey => "NODE KEY",
ConstraintKind::PropertyType => "PROPERTY TYPE",
}
}
pub fn keyword_for(&self, entity: EntityKind) -> &'static str {
match (self, entity) {
(ConstraintKind::NodeKey, EntityKind::Relationship) => "RELATIONSHIP KEY",
_ => self.keyword(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConstraintFailure {
Duplicate { values: Vec<Value> },
Missing { property: String },
Preexisting {
duplicate_tuples: usize,
sample: Vec<Value>,
},
PreexistingMissing { nodes: usize },
TypeMismatch {
property: String,
expected: String,
actual: String,
},
PreexistingTypeMismatch {
property: String,
expected: String,
actual: String,
nodes: usize,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NamedConstraint {
pub kind: ConstraintKind,
#[serde(default, skip_serializing_if = "EntityKind::is_node")]
pub entity: EntityKind,
pub node_type: String,
pub properties: Vec<String>,
}
pub(crate) type ConstraintResult<T> = Result<T, Box<ConstraintViolation>>;
#[derive(Debug, Clone, PartialEq)]
pub struct ConstraintViolation {
pub kind: ConstraintKind,
pub entity: EntityKind,
pub node_type: String,
pub properties: Vec<String>,
pub failure: ConstraintFailure,
}
impl ConstraintViolation {
#[must_use]
pub fn on_entity(mut self, entity: EntityKind) -> Self {
self.entity = entity;
self
}
pub fn duplicate(
kind: ConstraintKind,
node_type: impl Into<String>,
properties: Vec<String>,
values: Vec<Value>,
) -> Self {
Self {
kind,
entity: EntityKind::Node,
node_type: node_type.into(),
properties,
failure: ConstraintFailure::Duplicate { values },
}
}
pub fn missing(
kind: ConstraintKind,
node_type: impl Into<String>,
property: impl Into<String>,
) -> Self {
let property = property.into();
Self {
kind,
entity: EntityKind::Node,
node_type: node_type.into(),
properties: vec![property.clone()],
failure: ConstraintFailure::Missing { property },
}
}
pub fn preexisting(
kind: ConstraintKind,
node_type: impl Into<String>,
properties: Vec<String>,
duplicate_tuples: usize,
sample: Vec<Value>,
) -> Self {
Self {
kind,
entity: EntityKind::Node,
node_type: node_type.into(),
properties,
failure: ConstraintFailure::Preexisting {
duplicate_tuples,
sample,
},
}
}
pub fn type_mismatch(
node_type: impl Into<String>,
property: impl Into<String>,
expected: impl Into<String>,
actual: impl Into<String>,
) -> Self {
let property = property.into();
Self {
kind: ConstraintKind::PropertyType,
entity: EntityKind::Node,
node_type: node_type.into(),
properties: vec![property.clone()],
failure: ConstraintFailure::TypeMismatch {
property,
expected: expected.into(),
actual: actual.into(),
},
}
}
pub fn preexisting_type_mismatch(
node_type: impl Into<String>,
property: impl Into<String>,
expected: impl Into<String>,
actual: impl Into<String>,
nodes: usize,
) -> Self {
let property = property.into();
Self {
kind: ConstraintKind::PropertyType,
entity: EntityKind::Node,
node_type: node_type.into(),
properties: vec![property.clone()],
failure: ConstraintFailure::PreexistingTypeMismatch {
property,
expected: expected.into(),
actual: actual.into(),
nodes,
},
}
}
pub fn duplicate_tuple_count(&self) -> Option<usize> {
match &self.failure {
ConstraintFailure::Preexisting {
duplicate_tuples, ..
} => Some(*duplicate_tuples),
_ => None,
}
}
pub fn sample_values(&self) -> &[Value] {
match &self.failure {
ConstraintFailure::Duplicate { values } => values,
ConstraintFailure::Preexisting { sample, .. } => sample,
_ => &[],
}
}
pub fn is_declaration_failure(&self) -> bool {
matches!(
self.failure,
ConstraintFailure::Preexisting { .. }
| ConstraintFailure::PreexistingMissing { .. }
| ConstraintFailure::PreexistingTypeMismatch { .. }
)
}
pub fn preexisting_missing(
kind: ConstraintKind,
node_type: impl Into<String>,
property: impl Into<String>,
nodes: usize,
) -> Self {
Self {
kind,
entity: EntityKind::Node,
node_type: node_type.into(),
properties: vec![property.into()],
failure: ConstraintFailure::PreexistingMissing { nodes },
}
}
pub fn descriptor(&self) -> String {
descriptor(&self.node_type, &self.properties)
}
}
pub fn descriptor(node_type: &str, properties: &[String]) -> String {
match properties {
[single] => format!("{node_type}.{single}"),
many => format!("{node_type}.({})", many.join(", ")),
}
}
fn render_value(value: &Value) -> String {
match value {
Value::String(text) => format!("'{text}'"),
other => other.to_string(),
}
}
fn render_pairs(properties: &[String], values: &[Value]) -> String {
properties
.iter()
.zip(values.iter())
.map(|(property, value)| format!("'{property}' = {}", render_value(value)))
.collect::<Vec<_>>()
.join(", ")
}
impl fmt::Display for ConstraintViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let entity = self.entity;
let kind = self.kind.keyword_for(entity);
let descriptor = self.descriptor();
let subject = entity.subject(&self.node_type);
let population = entity.noun(2);
match &self.failure {
ConstraintFailure::Duplicate { values } => {
let plural = if self.properties.len() == 1 {
"property"
} else {
"properties"
};
write!(
f,
"{subject} and {plural} {} already exists — \
the {kind} constraint on {descriptor} rejects the duplicate. {}",
render_pairs(&self.properties, values),
entity.duplicate_advice(),
)
}
ConstraintFailure::Missing { property } => write!(
f,
"{subject} must have the property '{property}' — \
the {kind} constraint on {descriptor} rejects the write. \
Supply a non-null '{property}', {}",
entity.drop_presence_advice(),
),
ConstraintFailure::Preexisting {
duplicate_tuples,
sample,
} => {
let plural = if *duplicate_tuples == 1 {
"value"
} else {
"values"
};
write!(
f,
"cannot declare a {kind} constraint on {descriptor}: the existing data \
already has {duplicate_tuples} duplicate {plural} \
(for example {}). Deduplicate the {} before declaring the \
constraint.",
render_pairs(&self.properties, sample),
entity.type_noun(),
)
}
ConstraintFailure::PreexistingMissing { nodes } => {
write!(
f,
"cannot declare a {kind} constraint on {descriptor}: {nodes} existing \
{} of type '{}' have no value for it. Populate or delete those \
{population} before declaring the constraint.",
entity.noun(*nodes),
self.node_type,
)
}
ConstraintFailure::TypeMismatch {
property,
expected,
actual,
} => write!(
f,
"{subject} must have a value of type {expected} for the property \
'{property}', but the write supplies {actual} — the {kind} constraint on \
{descriptor} rejects it. Supply a value of type {expected}, or drop the \
constraint.",
),
ConstraintFailure::PreexistingTypeMismatch {
property,
expected,
actual,
nodes,
} => {
write!(
f,
"cannot declare a {kind} constraint on {descriptor}: {nodes} existing \
{} of type '{}' hold a value for '{property}' that is not \
{expected} (for example {actual}). Convert or delete those {population} \
before declaring the constraint.",
entity.noun(*nodes),
self.node_type,
)
}
}
}
}
impl std::error::Error for ConstraintViolation {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn descriptor_distinguishes_single_from_composite() {
assert_eq!(descriptor("Person", &["email".to_string()]), "Person.email");
assert_eq!(
descriptor("Person", &["first".to_string(), "last".to_string()]),
"Person.(first, last)"
);
}
#[test]
fn duplicate_message_names_the_value_and_the_upsert_route() {
let violation = ConstraintViolation::duplicate(
ConstraintKind::Unique,
"Person",
vec!["email".to_string()],
vec![Value::String("a@b.c".to_string())],
);
let message = violation.to_string();
assert!(message.contains("label 'Person'"), "{message}");
assert!(message.contains("'email' = 'a@b.c'"), "{message}");
assert!(
message.contains("UNIQUE constraint on Person.email"),
"{message}"
);
assert!(message.contains("MERGE"), "{message}");
assert!(!violation.is_declaration_failure());
}
#[test]
fn missing_message_names_the_property() {
let violation = ConstraintViolation::missing(ConstraintKind::NotNull, "Person", "email");
let message = violation.to_string();
assert!(
message.contains("must have the property 'email'"),
"{message}"
);
assert!(
message.contains("NOT NULL constraint on Person.email"),
"{message}"
);
}
#[test]
fn preexisting_is_flagged_as_a_declaration_failure() {
let violation = ConstraintViolation::preexisting(
ConstraintKind::Unique,
"Person",
vec!["email".to_string()],
2,
vec![Value::String("dup".to_string())],
);
assert!(violation.is_declaration_failure());
let message = violation.to_string();
assert!(message.contains("2 duplicate values"), "{message}");
assert!(message.contains("Deduplicate"), "{message}");
}
#[test]
fn type_mismatch_message_names_property_expected_and_actual() {
let violation = ConstraintViolation::type_mismatch("Person", "age", "INTEGER", "STRING");
let message = violation.to_string();
assert!(message.contains("'age'"), "{message}");
assert!(message.contains("INTEGER"), "{message}");
assert!(message.contains("STRING"), "{message}");
assert!(
message.contains("PROPERTY TYPE constraint on Person.age"),
"{message}"
);
assert!(!violation.is_declaration_failure());
assert_eq!(violation.kind, ConstraintKind::PropertyType);
}
#[test]
fn preexisting_type_mismatch_is_flagged_as_a_declaration_failure() {
let violation =
ConstraintViolation::preexisting_type_mismatch("Person", "age", "INTEGER", "STRING", 3);
assert!(violation.is_declaration_failure());
let message = violation.to_string();
assert!(message.contains("3 existing nodes"), "{message}");
assert!(message.contains("not INTEGER"), "{message}");
assert!(message.contains("STRING"), "{message}");
assert!(message.contains("Convert or delete"), "{message}");
}
#[test]
fn a_named_constraint_written_before_entity_existed_loads_as_a_node_one() {
let older = r#"{"kind":"Unique","node_type":"Person","properties":["email"]}"#;
let declared: NamedConstraint = serde_json::from_str(older).expect("older file must load");
assert_eq!(declared.entity, EntityKind::Node);
assert_eq!(declared.node_type, "Person");
assert_eq!(declared.properties, vec!["email".to_string()]);
assert_eq!(declared.kind, ConstraintKind::Unique);
}
#[test]
fn a_named_constraint_persists_under_the_node_type_key_for_either_entity() {
let declared = NamedConstraint {
kind: ConstraintKind::NodeKey,
entity: EntityKind::Relationship,
node_type: "KNOWS".to_string(),
properties: vec!["since".to_string()],
};
let json = serde_json::to_string(&declared).unwrap();
assert!(json.contains(r#""node_type":"KNOWS""#), "{json}");
assert_eq!(
serde_json::from_str::<NamedConstraint>(&json).unwrap(),
declared
);
}
#[test]
fn a_node_constraint_serializes_exactly_as_it_did_before_entity_existed() {
let declared = NamedConstraint {
kind: ConstraintKind::Unique,
entity: EntityKind::Node,
node_type: "Person".to_string(),
properties: vec!["email".to_string()],
};
assert_eq!(
serde_json::to_string(&declared).unwrap(),
r#"{"kind":"Unique","node_type":"Person","properties":["email"]}"#
);
}
#[test]
fn a_key_constraint_is_named_for_the_entity_it_is_on() {
assert_eq!(
ConstraintKind::NodeKey.keyword_for(EntityKind::Node),
"NODE KEY"
);
assert_eq!(
ConstraintKind::NodeKey.keyword_for(EntityKind::Relationship),
"RELATIONSHIP KEY"
);
for kind in [
ConstraintKind::Unique,
ConstraintKind::NotNull,
ConstraintKind::PropertyType,
] {
assert_eq!(
kind.keyword_for(EntityKind::Relationship),
kind.keyword(),
"{kind:?}"
);
}
}
fn assert_reads_as_a_relationship(message: &str) {
assert!(
!message.contains("node"),
"node vocabulary leaked: {message}"
);
assert!(
message.contains("relationship"),
"not in relationship words: {message}"
);
}
#[test]
fn a_relationship_duplicate_advises_matching_rather_than_merging() {
let message = ConstraintViolation::duplicate(
ConstraintKind::Unique,
"KNOWS",
vec!["since".to_string()],
vec![Value::String("2020".to_string())],
)
.on_entity(EntityKind::Relationship)
.to_string();
assert_reads_as_a_relationship(&message);
assert!(
message.contains("a relationship of type 'KNOWS'"),
"{message}"
);
assert!(message.contains("'since' = '2020'"), "{message}");
assert!(
message.contains("UNIQUE constraint on KNOWS.since"),
"{message}"
);
assert!(!message.contains("MERGE"), "{message}");
assert!(
message.contains("MATCH the existing relationship"),
"{message}"
);
}
#[test]
fn a_relationship_presence_failure_points_at_the_constraint() {
let message = ConstraintViolation::missing(ConstraintKind::NotNull, "KNOWS", "since")
.on_entity(EntityKind::Relationship)
.to_string();
assert_reads_as_a_relationship(&message);
assert!(
message.contains("must have the property 'since'"),
"{message}"
);
assert!(message.contains("or drop the constraint."), "{message}");
assert!(!message.contains("schema"), "{message}");
}
#[test]
fn a_relationship_type_mismatch_reads_in_relationship_words() {
let message = ConstraintViolation::type_mismatch("KNOWS", "since", "INTEGER", "STRING")
.on_entity(EntityKind::Relationship)
.to_string();
assert_reads_as_a_relationship(&message);
assert!(message.contains("value of type INTEGER"), "{message}");
assert!(message.contains("supplies STRING"), "{message}");
assert!(
message.contains("PROPERTY TYPE constraint on KNOWS.since"),
"{message}"
);
}
#[test]
fn relationship_declaration_failures_count_relationships() {
let duplicates = ConstraintViolation::preexisting(
ConstraintKind::Unique,
"KNOWS",
vec!["since".to_string()],
2,
vec![Value::String("2020".to_string())],
)
.on_entity(EntityKind::Relationship)
.to_string();
assert_reads_as_a_relationship(&duplicates);
assert!(duplicates.contains("2 duplicate values"), "{duplicates}");
assert!(
duplicates.contains("Deduplicate the relationship type"),
"{duplicates}"
);
let one_absent =
ConstraintViolation::preexisting_missing(ConstraintKind::NotNull, "KNOWS", "since", 1)
.on_entity(EntityKind::Relationship)
.to_string();
assert_reads_as_a_relationship(&one_absent);
assert!(
one_absent.contains("1 existing relationship of type 'KNOWS'"),
"{one_absent}"
);
assert!(
one_absent.contains("delete those relationships"),
"{one_absent}"
);
let many_absent =
ConstraintViolation::preexisting_missing(ConstraintKind::NodeKey, "KNOWS", "since", 4)
.on_entity(EntityKind::Relationship)
.to_string();
assert!(
many_absent.contains("4 existing relationships of type 'KNOWS'"),
"{many_absent}"
);
assert!(
many_absent.contains("RELATIONSHIP KEY constraint"),
"{many_absent}"
);
let mistyped = ConstraintViolation::preexisting_type_mismatch(
"KNOWS", "since", "INTEGER", "STRING", 3,
)
.on_entity(EntityKind::Relationship)
.to_string();
assert_reads_as_a_relationship(&mistyped);
assert!(
mistyped.contains("3 existing relationships of type 'KNOWS'"),
"{mistyped}"
);
assert!(
mistyped.contains("Convert or delete those relationships"),
"{mistyped}"
);
}
#[test]
fn a_violation_is_a_node_one_until_told_otherwise() {
let violation = ConstraintViolation::missing(ConstraintKind::NotNull, "Person", "email");
assert_eq!(violation.entity, EntityKind::Node);
assert_eq!(
violation.on_entity(EntityKind::Relationship).entity,
EntityKind::Relationship
);
}
#[test]
fn normalize_properties_makes_order_and_repeats_irrelevant() {
let a = normalize_properties(&["b".to_string(), "a".to_string()]);
let b = normalize_properties(&["a".to_string(), "b".to_string(), "a".to_string()]);
assert_eq!(a, b);
}
}