1#![allow(dead_code)]
25
26use std::collections::HashSet;
27use std::fmt::Write as _;
28
29use thiserror::Error;
30
31use super::constraint_catalog::{
32 ConstraintCatalog, ConstraintDefinition, StoredConstraintKind, StoredPropertyType,
33 StoredPropertyTypeTerm, StoredScalarType, StoredVectorCoordType,
34};
35use super::index_catalog::StoredIndexEntity;
36use super::InMemoryGraph;
37use crate::types::{NodeId, Properties, PropertyValue, RelationshipId};
38
39#[derive(Debug, Clone, Error)]
50pub enum ConstraintViolation {
51 #[error("[22N77] property presence verification failed. {kind} must have the property `{property}`",
52 kind = entity_kind_label(*entity, label))]
53 MissingProperty {
54 constraint: String,
55 entity: StoredIndexEntity,
56 label: String,
57 property: String,
58 },
59 #[error("[22N77] property presence verification failed. {kind} must have the properties: {properties}",
60 kind = entity_kind_label(*entity, label),
61 properties = properties.join(", "))]
62 MissingPropertiesForKey {
63 constraint: String,
64 entity: StoredIndexEntity,
65 label: String,
66 properties: Vec<String>,
67 },
68 #[error("[22N78] property type verification failed. {kind} must have property `{property}` with value type {expected}",
69 kind = entity_kind_label(*entity, label))]
70 WrongPropertyType {
71 constraint: String,
72 entity: StoredIndexEntity,
73 label: String,
74 property: String,
75 expected: String,
76 },
77 #[error("[22N79] property uniqueness constraint violated. {kind} already has property `{property}` with the supplied value",
78 kind = entity_kind_label(*entity, label))]
79 UniquenessViolated {
80 constraint: String,
81 entity: StoredIndexEntity,
82 label: String,
83 property: String,
84 },
85}
86
87impl ConstraintViolation {
88 pub const fn gql_status(&self) -> &'static str {
89 match self {
90 ConstraintViolation::MissingProperty { .. }
91 | ConstraintViolation::MissingPropertiesForKey { .. } => "22N77",
92 ConstraintViolation::WrongPropertyType { .. } => "22N78",
93 ConstraintViolation::UniquenessViolated { .. } => "22N79",
94 }
95 }
96
97 pub fn constraint_name(&self) -> &str {
98 match self {
99 ConstraintViolation::MissingProperty { constraint, .. }
100 | ConstraintViolation::MissingPropertiesForKey { constraint, .. }
101 | ConstraintViolation::WrongPropertyType { constraint, .. }
102 | ConstraintViolation::UniquenessViolated { constraint, .. } => constraint,
103 }
104 }
105}
106
107fn entity_kind_label(entity: StoredIndexEntity, label: &str) -> String {
108 match entity {
109 StoredIndexEntity::Node => format!("NODE with label `{label}`"),
110 StoredIndexEntity::Relationship => format!("RELATIONSHIP with type `{label}`"),
111 }
112}
113
114impl InMemoryGraph {
115 pub(super) fn validate_existing_data_for_constraint(
123 &self,
124 def: &ConstraintDefinition,
125 ) -> Result<(), ConstraintViolation> {
126 match def.entity {
127 StoredIndexEntity::Node => self.validate_existing_nodes_for_constraint(def),
128 StoredIndexEntity::Relationship => self.validate_existing_rels_for_constraint(def),
129 }
130 }
131
132 fn validate_existing_nodes_for_constraint(
133 &self,
134 def: &ConstraintDefinition,
135 ) -> Result<(), ConstraintViolation> {
136 let label = def.label.as_str();
137 let mut seen: HashSet<String> = HashSet::new();
138 for (_, node) in self.iter_nodes() {
139 if !node.labels.iter().any(|l| l == label) {
140 continue;
141 }
142 validate_record_against_constraint(def, &node.properties, &mut seen, true)?;
143 }
144 Ok(())
145 }
146
147 fn validate_existing_rels_for_constraint(
148 &self,
149 def: &ConstraintDefinition,
150 ) -> Result<(), ConstraintViolation> {
151 let rel_type = def.label.as_str();
152 let mut seen: HashSet<String> = HashSet::new();
153 for (_, rel) in self.iter_rels() {
154 if rel.rel_type != rel_type {
155 continue;
156 }
157 validate_record_against_constraint(def, &rel.properties, &mut seen, true)?;
158 }
159 Ok(())
160 }
161}
162
163fn validate_record_against_constraint(
164 def: &ConstraintDefinition,
165 properties: &Properties,
166 seen: &mut HashSet<String>,
167 check_existence: bool,
168) -> Result<(), ConstraintViolation> {
169 if check_existence && def.kind.requires_existence() {
171 let missing: Vec<String> = def
172 .properties
173 .iter()
174 .filter(|p| !properties.contains_key(p.as_str()))
175 .cloned()
176 .collect();
177 if !missing.is_empty() {
178 return Err(if def.properties.len() == 1 {
179 ConstraintViolation::MissingProperty {
180 constraint: def.name.clone(),
181 entity: def.entity,
182 label: def.label.clone(),
183 property: def.properties[0].clone(),
184 }
185 } else {
186 ConstraintViolation::MissingPropertiesForKey {
187 constraint: def.name.clone(),
188 entity: def.entity,
189 label: def.label.clone(),
190 properties: def.properties.clone(),
191 }
192 });
193 }
194 }
195
196 if let StoredConstraintKind::PropertyType(target) = &def.kind {
198 let key = &def.properties[0];
199 if let Some(value) = properties.get(key.as_str()) {
200 if !value_matches_property_type(value, target) {
201 return Err(ConstraintViolation::WrongPropertyType {
202 constraint: def.name.clone(),
203 entity: def.entity,
204 label: def.label.clone(),
205 property: key.clone(),
206 expected: target.to_string(),
207 });
208 }
209 }
210 }
211
212 if def.kind.requires_uniqueness() {
219 let tuple_present = def
220 .properties
221 .iter()
222 .all(|p| properties.contains_key(p.as_str()));
223 if tuple_present {
224 let key = property_tuple_key(def, properties);
225 if !seen.insert(key) {
226 let property_label = if def.properties.len() == 1 {
227 def.properties[0].clone()
228 } else {
229 def.properties.join(", ")
230 };
231 return Err(ConstraintViolation::UniquenessViolated {
232 constraint: def.name.clone(),
233 entity: def.entity,
234 label: def.label.clone(),
235 property: property_label,
236 });
237 }
238 }
239 }
240
241 Ok(())
242}
243
244fn property_tuple_key(def: &ConstraintDefinition, properties: &Properties) -> String {
248 let mut out = String::with_capacity(64);
249 for (i, key) in def.properties.iter().enumerate() {
250 if i > 0 {
251 out.push('\u{1f}');
252 }
253 if let Some(value) = properties.get(key.as_str()) {
254 append_property_value_key(&mut out, value);
255 }
256 }
257 out
258}
259
260fn append_property_value_key(out: &mut String, value: &PropertyValue) {
261 match value {
262 PropertyValue::Null => out.push('N'),
263 PropertyValue::Bool(b) => {
264 out.push('B');
265 out.push(if *b { 'T' } else { 'F' });
266 }
267 PropertyValue::Int(i) => {
268 out.push('I');
269 out.push_str(&i.to_string());
270 }
271 PropertyValue::Float(f) => {
272 out.push('F');
273 out.push_str(&format!("{f:?}"));
274 }
275 PropertyValue::String(s) => {
276 out.push('S');
277 append_len_prefixed_str(out, s);
278 }
279 PropertyValue::Date(d) => {
280 out.push_str("D:");
281 append_len_prefixed_str(out, &format!("{d:?}"));
282 }
283 PropertyValue::Time(t) => {
284 out.push_str("T:");
285 append_len_prefixed_str(out, &format!("{t:?}"));
286 }
287 PropertyValue::LocalTime(t) => {
288 out.push_str("LT:");
289 append_len_prefixed_str(out, &format!("{t:?}"));
290 }
291 PropertyValue::DateTime(dt) => {
292 out.push_str("DT:");
293 append_len_prefixed_str(out, &format!("{dt:?}"));
294 }
295 PropertyValue::LocalDateTime(dt) => {
296 out.push_str("LDT:");
297 append_len_prefixed_str(out, &format!("{dt:?}"));
298 }
299 PropertyValue::Duration(d) => {
300 out.push_str("DUR:");
301 append_len_prefixed_str(out, &format!("{d:?}"));
302 }
303 PropertyValue::Point(p) => {
304 out.push_str("P:");
305 append_len_prefixed_str(out, &format!("{p:?}"));
306 }
307 PropertyValue::Vector(v) => {
308 out.push_str("V:");
309 append_len_prefixed_str(out, &v.to_key_string());
310 }
311 PropertyValue::List(items) => {
312 out.push('L');
313 append_len(out, items.len());
314 for item in items {
315 append_property_value_key(out, item);
316 }
317 }
318 PropertyValue::Map(entries) => {
319 out.push('M');
320 append_len(out, entries.len());
321 for (k, v) in entries {
322 append_len_prefixed_str(out, k);
323 append_property_value_key(out, v);
324 }
325 }
326 PropertyValue::Binary(b) => {
327 out.push_str("BIN:");
328 append_len(out, b.len());
329 for segment in b.chunks() {
330 for byte in segment {
331 let _ = write!(out, "{byte:02x}");
332 }
333 }
334 }
335 }
336}
337
338fn append_len(out: &mut String, len: usize) {
339 out.push_str(&len.to_string());
340 out.push(':');
341}
342
343fn append_len_prefixed_str(out: &mut String, value: &str) {
344 append_len(out, value.len());
345 out.push_str(value);
346}
347
348pub fn value_matches_property_type(value: &PropertyValue, target: &StoredPropertyType) -> bool {
351 target
352 .alternatives
353 .iter()
354 .any(|term| value_matches_term(value, term))
355}
356
357fn value_matches_term(value: &PropertyValue, term: &StoredPropertyTypeTerm) -> bool {
358 match term {
359 StoredPropertyTypeTerm::Scalar(scalar) => value_matches_scalar(value, *scalar),
360 StoredPropertyTypeTerm::List { inner, not_null } => match value {
361 PropertyValue::List(items) => items.iter().all(|item| {
362 if matches!(item, PropertyValue::Null) {
363 !*not_null
364 } else {
365 value_matches_term(item, inner)
366 }
367 }),
368 _ => false,
369 },
370 StoredPropertyTypeTerm::Vector { coord, dimension } => match value {
371 PropertyValue::Vector(v) => vector_matches(v, *coord, *dimension),
372 _ => false,
373 },
374 }
375}
376
377fn value_matches_scalar(value: &PropertyValue, scalar: StoredScalarType) -> bool {
378 match (value, scalar) {
379 (PropertyValue::Bool(_), StoredScalarType::Boolean) => true,
380 (PropertyValue::String(_), StoredScalarType::String) => true,
381 (PropertyValue::Int(_), StoredScalarType::Integer) => true,
382 (PropertyValue::Float(_), StoredScalarType::Float) => true,
383 (PropertyValue::Date(_), StoredScalarType::Date) => true,
384 (PropertyValue::Time(_), StoredScalarType::ZonedTime) => true,
385 (PropertyValue::LocalTime(_), StoredScalarType::LocalTime) => true,
386 (PropertyValue::DateTime(_), StoredScalarType::ZonedDateTime) => true,
387 (PropertyValue::LocalDateTime(_), StoredScalarType::LocalDateTime) => true,
388 (PropertyValue::Duration(_), StoredScalarType::Duration) => true,
389 (PropertyValue::Point(_), StoredScalarType::Point) => true,
390 _ => false,
394 }
395}
396
397fn vector_matches(
398 vector: &crate::types::LoraVector,
399 coord: StoredVectorCoordType,
400 dimension: u32,
401) -> bool {
402 if vector.dimension != dimension as usize {
403 return false;
404 }
405 use crate::types::VectorValues;
406 use StoredVectorCoordType::*;
407 matches!(
408 (&vector.values, coord),
409 (VectorValues::Float64(_), Float64)
410 | (VectorValues::Float32(_), Float32)
411 | (VectorValues::Integer64(_), Int64)
412 | (VectorValues::Integer32(_), Int32)
413 | (VectorValues::Integer16(_), Int16)
414 | (VectorValues::Integer8(_), Int8)
415 )
416}
417
418#[derive(Clone, Copy)]
419enum NodeLabelMatcher<'a> {
420 AnyOf(&'a [String]),
421 One(&'a str),
422}
423
424impl NodeLabelMatcher<'_> {
425 fn contains(self, label: &str) -> bool {
426 match self {
427 NodeLabelMatcher::AnyOf(labels) => labels.iter().any(|l| l == label),
428 NodeLabelMatcher::One(candidate) => candidate == label,
429 }
430 }
431}
432
433#[derive(Clone, Copy)]
434enum ConstraintRecord<'a> {
435 Node {
436 labels: NodeLabelMatcher<'a>,
437 properties: &'a Properties,
438 skip: Option<NodeId>,
439 },
440 Relationship {
441 rel_type: &'a str,
442 properties: &'a Properties,
443 skip: Option<RelationshipId>,
444 },
445}
446
447impl<'a> ConstraintRecord<'a> {
448 fn applies_to(self, def: &ConstraintDefinition) -> bool {
449 match self {
450 ConstraintRecord::Node { labels, .. } => {
451 def.entity == StoredIndexEntity::Node && labels.contains(&def.label)
452 }
453 ConstraintRecord::Relationship { rel_type, .. } => {
454 def.entity == StoredIndexEntity::Relationship && def.label == rel_type
455 }
456 }
457 }
458
459 fn properties(self) -> &'a Properties {
460 match self {
461 ConstraintRecord::Node { properties, .. }
462 | ConstraintRecord::Relationship { properties, .. } => properties,
463 }
464 }
465
466 fn has_uniqueness_conflict(
467 self,
468 graph: &InMemoryGraph,
469 def: &ConstraintDefinition,
470 tuple: &[PropertyValue],
471 ) -> bool {
472 match self {
473 ConstraintRecord::Node { skip, .. } => {
474 any_other_node_with_tuple(graph, &def.label, &def.properties, tuple, skip)
475 }
476 ConstraintRecord::Relationship { skip, .. } => {
477 any_other_rel_with_tuple(graph, &def.label, &def.properties, tuple, skip)
478 }
479 }
480 }
481}
482
483fn check_record_constraints(
484 catalog: &ConstraintCatalog,
485 graph: &InMemoryGraph,
486 record: ConstraintRecord<'_>,
487) -> Result<(), ConstraintViolation> {
488 check_record_constraints_with(catalog, graph, record, true)
489}
490
491fn check_record_constraints_with(
492 catalog: &ConstraintCatalog,
493 graph: &InMemoryGraph,
494 record: ConstraintRecord<'_>,
495 check_existence: bool,
496) -> Result<(), ConstraintViolation> {
497 for def in catalog.iter() {
498 if !record.applies_to(def) {
499 continue;
500 }
501
502 let properties = record.properties();
503 let mut probe: HashSet<String> = HashSet::new();
504 validate_record_against_constraint(def, properties, &mut probe, check_existence)?;
505
506 if def.kind.requires_uniqueness() {
507 if let Some(tuple) = constrained_tuple(def, properties) {
508 if record.has_uniqueness_conflict(graph, def, &tuple) {
509 return Err(uniqueness_violation(def));
510 }
511 }
512 }
513 }
514 Ok(())
515}
516
517pub(crate) fn check_node_create(
522 catalog: &ConstraintCatalog,
523 graph: &InMemoryGraph,
524 labels: &[String],
525 properties: &Properties,
526) -> Result<(), ConstraintViolation> {
527 check_record_constraints(
528 catalog,
529 graph,
530 ConstraintRecord::Node {
531 labels: NodeLabelMatcher::AnyOf(labels),
532 properties,
533 skip: None,
534 },
535 )
536}
537
538pub(crate) fn check_relationship_create(
539 catalog: &ConstraintCatalog,
540 graph: &InMemoryGraph,
541 rel_type: &str,
542 properties: &Properties,
543) -> Result<(), ConstraintViolation> {
544 check_record_constraints(
545 catalog,
546 graph,
547 ConstraintRecord::Relationship {
548 rel_type,
549 properties,
550 skip: None,
551 },
552 )
553}
554
555pub(crate) fn check_node_create_deferred(
560 catalog: &ConstraintCatalog,
561 graph: &InMemoryGraph,
562 labels: &[String],
563 properties: &Properties,
564) -> Result<(), ConstraintViolation> {
565 check_record_constraints_with(
566 catalog,
567 graph,
568 ConstraintRecord::Node {
569 labels: NodeLabelMatcher::AnyOf(labels),
570 properties,
571 skip: None,
572 },
573 false,
574 )
575}
576
577pub(crate) fn check_relationship_create_deferred(
579 catalog: &ConstraintCatalog,
580 graph: &InMemoryGraph,
581 rel_type: &str,
582 properties: &Properties,
583) -> Result<(), ConstraintViolation> {
584 check_record_constraints_with(
585 catalog,
586 graph,
587 ConstraintRecord::Relationship {
588 rel_type,
589 properties,
590 skip: None,
591 },
592 false,
593 )
594}
595
596pub(crate) fn check_node_existence(
599 catalog: &ConstraintCatalog,
600 graph: &InMemoryGraph,
601 node_id: NodeId,
602) -> Result<(), ConstraintViolation> {
603 let Some(node) = graph.node_at(node_id) else {
604 return Ok(());
605 };
606 for def in catalog.iter() {
607 if def.entity != StoredIndexEntity::Node
608 || !def.kind.requires_existence()
609 || !node.labels.iter().any(|l| l == &def.label)
610 {
611 continue;
612 }
613 if let Some(missing) = def
614 .properties
615 .iter()
616 .find(|p| !node.properties.contains_key(p.as_str()))
617 {
618 return Err(missing_property_violation(def, missing));
619 }
620 }
621 Ok(())
622}
623
624pub(crate) fn check_relationship_existence(
626 catalog: &ConstraintCatalog,
627 graph: &InMemoryGraph,
628 rel_id: RelationshipId,
629) -> Result<(), ConstraintViolation> {
630 let Some(rel) = graph.rel_at(rel_id) else {
631 return Ok(());
632 };
633 for def in catalog.iter() {
634 if def.entity != StoredIndexEntity::Relationship
635 || !def.kind.requires_existence()
636 || rel.rel_type != def.label
637 {
638 continue;
639 }
640 if let Some(missing) = def
641 .properties
642 .iter()
643 .find(|p| !rel.properties.contains_key(p.as_str()))
644 {
645 return Err(missing_property_violation(def, missing));
646 }
647 }
648 Ok(())
649}
650
651fn any_other_node_with_tuple(
660 graph: &InMemoryGraph,
661 label: &str,
662 keys: &[String],
663 target: &[PropertyValue],
664 skip: Option<NodeId>,
665) -> bool {
666 let tuple_matches = |node: &crate::NodeRecord| {
667 node.labels.iter().any(|l| l == label)
668 && keys.iter().enumerate().all(|(idx, key)| {
669 node.properties
670 .get(key.as_str())
671 .map(|v| v == &target[idx])
672 .unwrap_or(false)
673 })
674 };
675 let candidates = match (keys.first(), target.first()) {
676 (Some(key), Some(value)) => graph.indexed_node_ids(label, key, value),
677 _ => None,
678 };
679 let candidates =
680 candidates.unwrap_or_else(|| crate::GraphStorage::node_ids_by_label(graph, label));
681 candidates
682 .into_iter()
683 .filter(|id| Some(*id) != skip)
684 .any(|id| graph.node_at(id).is_some_and(tuple_matches))
685}
686
687fn any_other_rel_with_tuple(
689 graph: &InMemoryGraph,
690 rel_type: &str,
691 keys: &[String],
692 target: &[PropertyValue],
693 skip: Option<RelationshipId>,
694) -> bool {
695 let tuple_matches = |rel: &crate::RelationshipRecord| {
696 rel.rel_type == rel_type
697 && keys.iter().enumerate().all(|(idx, key)| {
698 rel.properties
699 .get(key.as_str())
700 .map(|v| v == &target[idx])
701 .unwrap_or(false)
702 })
703 };
704 let candidates = match (keys.first(), target.first()) {
705 (Some(key), Some(value)) => graph.indexed_rel_ids(rel_type, key, value),
706 _ => None,
707 };
708 let candidates =
709 candidates.unwrap_or_else(|| crate::GraphStorage::rel_ids_by_type(graph, rel_type));
710 candidates
711 .into_iter()
712 .filter(|id| Some(*id) != skip)
713 .any(|id| graph.rel_at(id).is_some_and(tuple_matches))
714}
715
716fn render_constraint_property_label(def: &ConstraintDefinition) -> String {
717 if def.properties.len() == 1 {
718 def.properties[0].clone()
719 } else {
720 def.properties.join(", ")
721 }
722}
723
724fn uniqueness_violation(def: &ConstraintDefinition) -> ConstraintViolation {
725 ConstraintViolation::UniquenessViolated {
726 constraint: def.name.clone(),
727 entity: def.entity,
728 label: def.label.clone(),
729 property: render_constraint_property_label(def),
730 }
731}
732
733fn missing_property_violation(def: &ConstraintDefinition, property: &str) -> ConstraintViolation {
734 if def.properties.len() == 1 {
735 ConstraintViolation::MissingProperty {
736 constraint: def.name.clone(),
737 entity: def.entity,
738 label: def.label.clone(),
739 property: property.to_string(),
740 }
741 } else {
742 ConstraintViolation::MissingPropertiesForKey {
743 constraint: def.name.clone(),
744 entity: def.entity,
745 label: def.label.clone(),
746 properties: def.properties.clone(),
747 }
748 }
749}
750
751fn constrained_tuple(
752 def: &ConstraintDefinition,
753 properties: &Properties,
754) -> Option<Vec<PropertyValue>> {
755 def.properties
756 .iter()
757 .map(|p| properties.get(p.as_str()).cloned())
758 .collect()
759}
760
761fn constrained_tuple_after_set(
762 def: &ConstraintDefinition,
763 properties: &Properties,
764 key: &str,
765 value: &PropertyValue,
766) -> Option<Vec<PropertyValue>> {
767 def.properties
768 .iter()
769 .map(|prop| {
770 if prop == key {
771 Some(value.clone())
772 } else {
773 properties.get(prop.as_str()).cloned()
774 }
775 })
776 .collect()
777}
778
779pub(crate) fn check_node_set_property(
783 catalog: &ConstraintCatalog,
784 graph: &InMemoryGraph,
785 node_id: NodeId,
786 key: &str,
787 value: &PropertyValue,
788) -> Result<(), ConstraintViolation> {
789 let node = match graph.node_at(node_id) {
790 Some(n) => n,
791 None => return Ok(()), };
793 for def in catalog.iter() {
794 if def.entity != StoredIndexEntity::Node {
795 continue;
796 }
797 if !node.labels.iter().any(|l| l == &def.label) {
798 continue;
799 }
800 if !def.properties.iter().any(|p| p == key) {
801 continue;
802 }
803 if let StoredConstraintKind::PropertyType(target) = &def.kind {
805 if !value_matches_property_type(value, target) {
806 return Err(ConstraintViolation::WrongPropertyType {
807 constraint: def.name.clone(),
808 entity: def.entity,
809 label: def.label.clone(),
810 property: key.to_string(),
811 expected: target.to_string(),
812 });
813 }
814 }
815 if def.kind.requires_uniqueness() {
818 if let Some(tuple) = constrained_tuple_after_set(def, &node.properties, key, value) {
819 if any_other_node_with_tuple(
820 graph,
821 &def.label,
822 &def.properties,
823 &tuple,
824 Some(node_id),
825 ) {
826 return Err(uniqueness_violation(def));
827 }
828 }
829 }
830 }
831 Ok(())
832}
833
834pub(crate) fn check_node_remove_property(
837 catalog: &ConstraintCatalog,
838 graph: &InMemoryGraph,
839 node_id: NodeId,
840 key: &str,
841) -> Result<(), ConstraintViolation> {
842 let node = match graph.node_at(node_id) {
843 Some(n) => n,
844 None => return Ok(()),
845 };
846 for def in catalog.iter() {
847 if def.entity != StoredIndexEntity::Node {
848 continue;
849 }
850 if !node.labels.iter().any(|l| l == &def.label) {
851 continue;
852 }
853 if !def.kind.requires_existence() {
854 continue;
855 }
856 if !def.properties.iter().any(|p| p == key) {
857 continue;
858 }
859 return Err(missing_property_violation(def, key));
860 }
861 Ok(())
862}
863
864pub(crate) fn check_node_replace_properties(
868 catalog: &ConstraintCatalog,
869 graph: &InMemoryGraph,
870 node_id: NodeId,
871 properties: &Properties,
872) -> Result<(), ConstraintViolation> {
873 let node = match graph.node_at(node_id) {
874 Some(n) => n,
875 None => return Ok(()),
876 };
877 check_record_constraints(
878 catalog,
879 graph,
880 ConstraintRecord::Node {
881 labels: NodeLabelMatcher::AnyOf(&node.labels),
882 properties,
883 skip: Some(node_id),
884 },
885 )
886}
887
888pub(crate) fn check_relationship_set_property(
889 catalog: &ConstraintCatalog,
890 graph: &InMemoryGraph,
891 rel_id: RelationshipId,
892 key: &str,
893 value: &PropertyValue,
894) -> Result<(), ConstraintViolation> {
895 let rel = match graph.rel_at(rel_id) {
896 Some(r) => r,
897 None => return Ok(()),
898 };
899 for def in catalog.iter() {
900 if def.entity != StoredIndexEntity::Relationship {
901 continue;
902 }
903 if def.label != rel.rel_type {
904 continue;
905 }
906 if !def.properties.iter().any(|p| p == key) {
907 continue;
908 }
909 if let StoredConstraintKind::PropertyType(target) = &def.kind {
910 if !value_matches_property_type(value, target) {
911 return Err(ConstraintViolation::WrongPropertyType {
912 constraint: def.name.clone(),
913 entity: def.entity,
914 label: def.label.clone(),
915 property: key.to_string(),
916 expected: target.to_string(),
917 });
918 }
919 }
920 if def.kind.requires_uniqueness() {
921 if let Some(tuple) = constrained_tuple_after_set(def, &rel.properties, key, value) {
922 if any_other_rel_with_tuple(
923 graph,
924 &def.label,
925 &def.properties,
926 &tuple,
927 Some(rel_id),
928 ) {
929 return Err(uniqueness_violation(def));
930 }
931 }
932 }
933 }
934 Ok(())
935}
936
937pub(crate) fn check_relationship_remove_property(
938 catalog: &ConstraintCatalog,
939 graph: &InMemoryGraph,
940 rel_id: RelationshipId,
941 key: &str,
942) -> Result<(), ConstraintViolation> {
943 let rel = match graph.rel_at(rel_id) {
944 Some(r) => r,
945 None => return Ok(()),
946 };
947 for def in catalog.iter() {
948 if def.entity != StoredIndexEntity::Relationship {
949 continue;
950 }
951 if def.label != rel.rel_type {
952 continue;
953 }
954 if !def.kind.requires_existence() {
955 continue;
956 }
957 if !def.properties.iter().any(|p| p == key) {
958 continue;
959 }
960 return Err(missing_property_violation(def, key));
961 }
962 Ok(())
963}
964
965pub(crate) fn check_relationship_replace_properties(
969 catalog: &ConstraintCatalog,
970 graph: &InMemoryGraph,
971 rel_id: RelationshipId,
972 properties: &Properties,
973) -> Result<(), ConstraintViolation> {
974 let rel = match graph.rel_at(rel_id) {
975 Some(r) => r,
976 None => return Ok(()),
977 };
978 check_record_constraints(
979 catalog,
980 graph,
981 ConstraintRecord::Relationship {
982 rel_type: &rel.rel_type,
983 properties,
984 skip: Some(rel_id),
985 },
986 )
987}
988
989pub(crate) fn check_node_add_label(
994 catalog: &ConstraintCatalog,
995 graph: &InMemoryGraph,
996 node_id: NodeId,
997 label: &str,
998) -> Result<(), ConstraintViolation> {
999 let node = match graph.node_at(node_id) {
1000 Some(n) => n,
1001 None => return Ok(()),
1002 };
1003 check_record_constraints(
1004 catalog,
1005 graph,
1006 ConstraintRecord::Node {
1007 labels: NodeLabelMatcher::One(label),
1008 properties: &node.properties,
1009 skip: Some(node_id),
1010 },
1011 )
1012}