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, Serialize, Deserialize)]
pub enum ConstraintKind {
Unique,
NotNull,
NodeKey,
}
impl ConstraintKind {
pub fn keyword(&self) -> &'static str {
match self {
ConstraintKind::Unique => "UNIQUE",
ConstraintKind::NotNull => "NOT NULL",
ConstraintKind::NodeKey => "NODE KEY",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConstraintFailure {
Duplicate { values: Vec<Value> },
Missing { property: String },
Preexisting {
duplicate_tuples: usize,
sample: Vec<Value>,
},
PreexistingMissing { nodes: usize },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NamedConstraint {
pub kind: ConstraintKind,
pub node_type: String,
pub properties: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConstraintViolation {
pub kind: ConstraintKind,
pub node_type: String,
pub properties: Vec<String>,
pub failure: ConstraintFailure,
}
impl ConstraintViolation {
pub fn duplicate(
kind: ConstraintKind,
node_type: impl Into<String>,
properties: Vec<String>,
values: Vec<Value>,
) -> Self {
Self {
kind,
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,
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,
node_type: node_type.into(),
properties,
failure: ConstraintFailure::Preexisting {
duplicate_tuples,
sample,
},
}
}
pub fn is_declaration_failure(&self) -> bool {
matches!(
self.failure,
ConstraintFailure::Preexisting { .. } | ConstraintFailure::PreexistingMissing { .. }
)
}
pub fn preexisting_missing(
kind: ConstraintKind,
node_type: impl Into<String>,
property: impl Into<String>,
nodes: usize,
) -> Self {
Self {
kind,
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 kind = self.kind.keyword();
let descriptor = self.descriptor();
match &self.failure {
ConstraintFailure::Duplicate { values } => {
let plural = if self.properties.len() == 1 {
"property"
} else {
"properties"
};
write!(
f,
"a node with label '{}' and {plural} {} already exists — \
the {kind} constraint on {descriptor} rejects the duplicate. \
Use MERGE to upsert an existing node instead of CREATE.",
self.node_type,
render_pairs(&self.properties, values),
)
}
ConstraintFailure::Missing { property } => write!(
f,
"a node with label '{}' must have the property '{property}' — \
the {kind} constraint on {descriptor} rejects the write. \
Supply a non-null '{property}', or drop the requirement from the \
node type's schema.",
self.node_type,
),
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 node type before declaring the \
constraint.",
render_pairs(&self.properties, sample),
)
}
ConstraintFailure::PreexistingMissing { nodes } => {
let plural = if *nodes == 1 { "node" } else { "nodes" };
write!(
f,
"cannot declare a {kind} constraint on {descriptor}: {nodes} existing \
{plural} of type '{}' have no value for it. Populate or delete those \
nodes before declaring the constraint.",
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 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);
}
}