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().strs().any(|l| l == label) {
140 continue;
141 }
142 validate_record_against_constraint(
143 def,
144 &node.properties().to_owned(),
145 &mut seen,
146 true,
147 )?;
148 }
149 Ok(())
150 }
151
152 fn validate_existing_rels_for_constraint(
153 &self,
154 def: &ConstraintDefinition,
155 ) -> Result<(), ConstraintViolation> {
156 let rel_type = def.label.as_str();
157 let mut seen: HashSet<String> = HashSet::new();
158 for (_, rel) in self.iter_rels() {
159 if rel.rel_type() != rel_type {
160 continue;
161 }
162 validate_record_against_constraint(def, &rel.properties().to_owned(), &mut seen, true)?;
163 }
164 Ok(())
165 }
166}
167
168fn validate_record_against_constraint(
169 def: &ConstraintDefinition,
170 properties: &Properties,
171 seen: &mut HashSet<String>,
172 check_existence: bool,
173) -> Result<(), ConstraintViolation> {
174 if check_existence && def.kind.requires_existence() {
176 let missing: Vec<String> = def
177 .properties
178 .iter()
179 .filter(|p| !properties.contains_key(p.as_str()))
180 .cloned()
181 .collect();
182 if !missing.is_empty() {
183 return Err(if def.properties.len() == 1 {
184 ConstraintViolation::MissingProperty {
185 constraint: def.name.clone(),
186 entity: def.entity,
187 label: def.label.clone(),
188 property: def.properties[0].clone(),
189 }
190 } else {
191 ConstraintViolation::MissingPropertiesForKey {
192 constraint: def.name.clone(),
193 entity: def.entity,
194 label: def.label.clone(),
195 properties: def.properties.clone(),
196 }
197 });
198 }
199 }
200
201 if let StoredConstraintKind::PropertyType(target) = &def.kind {
203 let key = &def.properties[0];
204 if let Some(value) = properties.get(key.as_str()) {
205 if !value_matches_property_type(value, target) {
206 return Err(ConstraintViolation::WrongPropertyType {
207 constraint: def.name.clone(),
208 entity: def.entity,
209 label: def.label.clone(),
210 property: key.clone(),
211 expected: target.to_string(),
212 });
213 }
214 }
215 }
216
217 if def.kind.requires_uniqueness() {
224 let tuple_present = def
225 .properties
226 .iter()
227 .all(|p| properties.contains_key(p.as_str()));
228 if tuple_present {
229 let key = property_tuple_key(def, properties);
230 if !seen.insert(key) {
231 let property_label = if def.properties.len() == 1 {
232 def.properties[0].clone()
233 } else {
234 def.properties.join(", ")
235 };
236 return Err(ConstraintViolation::UniquenessViolated {
237 constraint: def.name.clone(),
238 entity: def.entity,
239 label: def.label.clone(),
240 property: property_label,
241 });
242 }
243 }
244 }
245
246 Ok(())
247}
248
249fn property_tuple_key(def: &ConstraintDefinition, properties: &Properties) -> String {
253 let mut out = String::with_capacity(64);
254 for (i, key) in def.properties.iter().enumerate() {
255 if i > 0 {
256 out.push('\u{1f}');
257 }
258 if let Some(value) = properties.get(key.as_str()) {
259 append_property_value_key(&mut out, value);
260 }
261 }
262 out
263}
264
265fn append_property_value_key(out: &mut String, value: &PropertyValue) {
266 match value {
267 PropertyValue::Null => out.push('N'),
268 PropertyValue::Bool(b) => {
269 out.push('B');
270 out.push(if *b { 'T' } else { 'F' });
271 }
272 PropertyValue::Int(i) => {
273 out.push('I');
274 out.push_str(&i.to_string());
275 }
276 PropertyValue::Float(f) => {
277 out.push('F');
278 out.push_str(&format!("{f:?}"));
279 }
280 PropertyValue::String(s) => {
281 out.push('S');
282 append_len_prefixed_str(out, s);
283 }
284 PropertyValue::Date(d) => {
285 out.push_str("D:");
286 append_len_prefixed_str(out, &format!("{d:?}"));
287 }
288 PropertyValue::Time(t) => {
289 out.push_str("T:");
290 append_len_prefixed_str(out, &format!("{t:?}"));
291 }
292 PropertyValue::LocalTime(t) => {
293 out.push_str("LT:");
294 append_len_prefixed_str(out, &format!("{t:?}"));
295 }
296 PropertyValue::DateTime(dt) => {
297 out.push_str("DT:");
298 append_len_prefixed_str(out, &format!("{dt:?}"));
299 }
300 PropertyValue::LocalDateTime(dt) => {
301 out.push_str("LDT:");
302 append_len_prefixed_str(out, &format!("{dt:?}"));
303 }
304 PropertyValue::Duration(d) => {
305 out.push_str("DUR:");
306 append_len_prefixed_str(out, &format!("{d:?}"));
307 }
308 PropertyValue::Point(p) => {
309 out.push_str("P:");
310 append_len_prefixed_str(out, &format!("{p:?}"));
311 }
312 PropertyValue::Vector(v) => {
313 out.push_str("V:");
314 append_len_prefixed_str(out, &v.to_key_string());
315 }
316 PropertyValue::List(items) => {
317 out.push('L');
318 append_len(out, items.len());
319 for item in items {
320 append_property_value_key(out, item);
321 }
322 }
323 PropertyValue::Map(entries) => {
324 out.push('M');
325 append_len(out, entries.len());
326 for (k, v) in entries {
327 append_len_prefixed_str(out, k);
328 append_property_value_key(out, v);
329 }
330 }
331 PropertyValue::Binary(b) => {
332 out.push_str("BIN:");
333 append_len(out, b.len());
334 for segment in b.chunks() {
335 for byte in segment {
336 let _ = write!(out, "{byte:02x}");
337 }
338 }
339 }
340 }
341}
342
343fn append_len(out: &mut String, len: usize) {
344 out.push_str(&len.to_string());
345 out.push(':');
346}
347
348fn append_len_prefixed_str(out: &mut String, value: &str) {
349 append_len(out, value.len());
350 out.push_str(value);
351}
352
353pub fn value_matches_property_type(value: &PropertyValue, target: &StoredPropertyType) -> bool {
356 target
357 .alternatives
358 .iter()
359 .any(|term| value_matches_term(value, term))
360}
361
362fn value_matches_term(value: &PropertyValue, term: &StoredPropertyTypeTerm) -> bool {
363 match term {
364 StoredPropertyTypeTerm::Scalar(scalar) => value_matches_scalar(value, *scalar),
365 StoredPropertyTypeTerm::List { inner, not_null } => match value {
366 PropertyValue::List(items) => items.iter().all(|item| {
367 if matches!(item, PropertyValue::Null) {
368 !*not_null
369 } else {
370 value_matches_term(item, inner)
371 }
372 }),
373 _ => false,
374 },
375 StoredPropertyTypeTerm::Vector { coord, dimension } => match value {
376 PropertyValue::Vector(v) => vector_matches(v, *coord, *dimension),
377 _ => false,
378 },
379 }
380}
381
382fn value_matches_scalar(value: &PropertyValue, scalar: StoredScalarType) -> bool {
383 match (value, scalar) {
384 (PropertyValue::Bool(_), StoredScalarType::Boolean) => true,
385 (PropertyValue::String(_), StoredScalarType::String) => true,
386 (PropertyValue::Int(_), StoredScalarType::Integer) => true,
387 (PropertyValue::Float(_), StoredScalarType::Float) => true,
388 (PropertyValue::Date(_), StoredScalarType::Date) => true,
389 (PropertyValue::Time(_), StoredScalarType::ZonedTime) => true,
390 (PropertyValue::LocalTime(_), StoredScalarType::LocalTime) => true,
391 (PropertyValue::DateTime(_), StoredScalarType::ZonedDateTime) => true,
392 (PropertyValue::LocalDateTime(_), StoredScalarType::LocalDateTime) => true,
393 (PropertyValue::Duration(_), StoredScalarType::Duration) => true,
394 (PropertyValue::Point(_), StoredScalarType::Point) => true,
395 _ => false,
399 }
400}
401
402fn vector_matches(
403 vector: &crate::types::LoraVector,
404 coord: StoredVectorCoordType,
405 dimension: u32,
406) -> bool {
407 if vector.dimension != dimension as usize {
408 return false;
409 }
410 use crate::types::VectorValues;
411 use StoredVectorCoordType::*;
412 matches!(
413 (&vector.values, coord),
414 (VectorValues::Float64(_), Float64)
415 | (VectorValues::Float32(_), Float32)
416 | (VectorValues::Integer64(_), Int64)
417 | (VectorValues::Integer32(_), Int32)
418 | (VectorValues::Integer16(_), Int16)
419 | (VectorValues::Integer8(_), Int8)
420 )
421}
422
423#[derive(Clone, Copy)]
424enum NodeLabelMatcher<'a> {
425 AnyOf(&'a [String]),
426 Names(&'a [crate::Name]),
427 One(&'a str),
428}
429
430impl NodeLabelMatcher<'_> {
431 fn contains(self, label: &str) -> bool {
432 match self {
433 NodeLabelMatcher::AnyOf(labels) => labels.iter().any(|l| l == label),
434 NodeLabelMatcher::Names(labels) => labels.iter().any(|l| l == label),
435 NodeLabelMatcher::One(candidate) => candidate == label,
436 }
437 }
438}
439
440#[derive(Clone, Copy)]
441enum ConstraintRecord<'a> {
442 Node {
443 labels: NodeLabelMatcher<'a>,
444 properties: &'a Properties,
445 skip: Option<NodeId>,
446 },
447 Relationship {
448 rel_type: &'a str,
449 properties: &'a Properties,
450 skip: Option<RelationshipId>,
451 },
452}
453
454impl<'a> ConstraintRecord<'a> {
455 fn applies_to(self, def: &ConstraintDefinition) -> bool {
456 match self {
457 ConstraintRecord::Node { labels, .. } => {
458 def.entity == StoredIndexEntity::Node && labels.contains(&def.label)
459 }
460 ConstraintRecord::Relationship { rel_type, .. } => {
461 def.entity == StoredIndexEntity::Relationship && def.label == rel_type
462 }
463 }
464 }
465
466 fn properties(self) -> &'a Properties {
467 match self {
468 ConstraintRecord::Node { properties, .. }
469 | ConstraintRecord::Relationship { properties, .. } => properties,
470 }
471 }
472
473 fn has_uniqueness_conflict(
474 self,
475 graph: &InMemoryGraph,
476 def: &ConstraintDefinition,
477 tuple: &[PropertyValue],
478 ) -> bool {
479 match self {
480 ConstraintRecord::Node { skip, .. } => {
481 any_other_node_with_tuple(graph, &def.label, &def.properties, tuple, skip)
482 }
483 ConstraintRecord::Relationship { skip, .. } => {
484 any_other_rel_with_tuple(graph, &def.label, &def.properties, tuple, skip)
485 }
486 }
487 }
488}
489
490fn check_record_constraints(
491 catalog: &ConstraintCatalog,
492 graph: &InMemoryGraph,
493 record: ConstraintRecord<'_>,
494) -> Result<(), ConstraintViolation> {
495 check_record_constraints_with(catalog, graph, record, true)
496}
497
498fn check_record_constraints_with(
499 catalog: &ConstraintCatalog,
500 graph: &InMemoryGraph,
501 record: ConstraintRecord<'_>,
502 check_existence: bool,
503) -> Result<(), ConstraintViolation> {
504 for def in catalog.iter() {
505 if !record.applies_to(def) {
506 continue;
507 }
508
509 let properties = record.properties();
510 let mut probe: HashSet<String> = HashSet::new();
511 validate_record_against_constraint(def, properties, &mut probe, check_existence)?;
512
513 if def.kind.requires_uniqueness() {
514 if let Some(tuple) = constrained_tuple(def, properties) {
515 if record.has_uniqueness_conflict(graph, def, &tuple) {
516 return Err(uniqueness_violation(def));
517 }
518 }
519 }
520 }
521 Ok(())
522}
523
524pub(crate) fn check_node_create(
529 catalog: &ConstraintCatalog,
530 graph: &InMemoryGraph,
531 labels: &[String],
532 properties: &Properties,
533) -> Result<(), ConstraintViolation> {
534 check_record_constraints(
535 catalog,
536 graph,
537 ConstraintRecord::Node {
538 labels: NodeLabelMatcher::AnyOf(labels),
539 properties,
540 skip: None,
541 },
542 )
543}
544
545pub(crate) fn check_relationship_create(
546 catalog: &ConstraintCatalog,
547 graph: &InMemoryGraph,
548 rel_type: &str,
549 properties: &Properties,
550) -> Result<(), ConstraintViolation> {
551 check_record_constraints(
552 catalog,
553 graph,
554 ConstraintRecord::Relationship {
555 rel_type,
556 properties,
557 skip: None,
558 },
559 )
560}
561
562pub(crate) fn check_node_create_deferred(
567 catalog: &ConstraintCatalog,
568 graph: &InMemoryGraph,
569 labels: &[String],
570 properties: &Properties,
571) -> Result<(), ConstraintViolation> {
572 check_record_constraints_with(
573 catalog,
574 graph,
575 ConstraintRecord::Node {
576 labels: NodeLabelMatcher::AnyOf(labels),
577 properties,
578 skip: None,
579 },
580 false,
581 )
582}
583
584pub(crate) fn check_relationship_create_deferred(
586 catalog: &ConstraintCatalog,
587 graph: &InMemoryGraph,
588 rel_type: &str,
589 properties: &Properties,
590) -> Result<(), ConstraintViolation> {
591 check_record_constraints_with(
592 catalog,
593 graph,
594 ConstraintRecord::Relationship {
595 rel_type,
596 properties,
597 skip: None,
598 },
599 false,
600 )
601}
602
603pub(crate) fn check_node_existence(
606 catalog: &ConstraintCatalog,
607 graph: &InMemoryGraph,
608 node_id: NodeId,
609) -> Result<(), ConstraintViolation> {
610 let Some(node) = graph.node_at(node_id) else {
611 return Ok(());
612 };
613 for def in catalog.iter() {
614 if def.entity != StoredIndexEntity::Node
615 || !def.kind.requires_existence()
616 || !node.labels().strs().any(|l| l == def.label)
617 {
618 continue;
619 }
620 if let Some(missing) = def
621 .properties
622 .iter()
623 .find(|p| !node.properties().contains_key(p.as_str()))
624 {
625 return Err(missing_property_violation(def, missing));
626 }
627 }
628 Ok(())
629}
630
631pub(crate) fn check_relationship_existence(
633 catalog: &ConstraintCatalog,
634 graph: &InMemoryGraph,
635 rel_id: RelationshipId,
636) -> Result<(), ConstraintViolation> {
637 let Some(rel) = graph.rel_at(rel_id) else {
638 return Ok(());
639 };
640 for def in catalog.iter() {
641 if def.entity != StoredIndexEntity::Relationship
642 || !def.kind.requires_existence()
643 || rel.rel_type() != def.label
644 {
645 continue;
646 }
647 if let Some(missing) = def
648 .properties
649 .iter()
650 .find(|p| !rel.properties().contains_key(p.as_str()))
651 {
652 return Err(missing_property_violation(def, missing));
653 }
654 }
655 Ok(())
656}
657
658fn any_other_node_with_tuple(
667 graph: &InMemoryGraph,
668 label: &str,
669 keys: &[String],
670 target: &[PropertyValue],
671 skip: Option<NodeId>,
672) -> bool {
673 let tuple_matches = |node: crate::NodeRef<'_>| {
674 node.has_label(label)
675 && keys.iter().enumerate().all(|(idx, key)| {
676 node.properties()
677 .get(key.as_str())
678 .is_some_and(|v| v == target[idx])
679 })
680 };
681 let candidates = match (keys.first(), target.first()) {
682 (Some(key), Some(value)) => graph.indexed_node_ids(label, key, value),
683 _ => None,
684 };
685 let candidates =
686 candidates.unwrap_or_else(|| crate::GraphStorage::node_ids_by_label(graph, label));
687 candidates
688 .into_iter()
689 .filter(|id| Some(*id) != skip)
690 .any(|id| graph.node_at(id).is_some_and(tuple_matches))
691}
692
693fn any_other_rel_with_tuple(
695 graph: &InMemoryGraph,
696 rel_type: &str,
697 keys: &[String],
698 target: &[PropertyValue],
699 skip: Option<RelationshipId>,
700) -> bool {
701 let tuple_matches = |rel: crate::RelRef<'_>| {
702 rel.rel_type() == rel_type
703 && keys.iter().enumerate().all(|(idx, key)| {
704 rel.properties()
705 .get(key.as_str())
706 .is_some_and(|v| v == target[idx])
707 })
708 };
709 let candidates = match (keys.first(), target.first()) {
710 (Some(key), Some(value)) => graph.indexed_rel_ids(rel_type, key, value),
711 _ => None,
712 };
713 let candidates =
714 candidates.unwrap_or_else(|| crate::GraphStorage::rel_ids_by_type(graph, rel_type));
715 candidates
716 .into_iter()
717 .filter(|id| Some(*id) != skip)
718 .any(|id| graph.rel_at(id).is_some_and(tuple_matches))
719}
720
721fn render_constraint_property_label(def: &ConstraintDefinition) -> String {
722 if def.properties.len() == 1 {
723 def.properties[0].clone()
724 } else {
725 def.properties.join(", ")
726 }
727}
728
729fn uniqueness_violation(def: &ConstraintDefinition) -> ConstraintViolation {
730 ConstraintViolation::UniquenessViolated {
731 constraint: def.name.clone(),
732 entity: def.entity,
733 label: def.label.clone(),
734 property: render_constraint_property_label(def),
735 }
736}
737
738fn missing_property_violation(def: &ConstraintDefinition, property: &str) -> ConstraintViolation {
739 if def.properties.len() == 1 {
740 ConstraintViolation::MissingProperty {
741 constraint: def.name.clone(),
742 entity: def.entity,
743 label: def.label.clone(),
744 property: property.to_string(),
745 }
746 } else {
747 ConstraintViolation::MissingPropertiesForKey {
748 constraint: def.name.clone(),
749 entity: def.entity,
750 label: def.label.clone(),
751 properties: def.properties.clone(),
752 }
753 }
754}
755
756fn constrained_tuple(
757 def: &ConstraintDefinition,
758 properties: &Properties,
759) -> Option<Vec<PropertyValue>> {
760 def.properties
761 .iter()
762 .map(|p| properties.get(p.as_str()).cloned())
763 .collect()
764}
765
766fn constrained_tuple_after_set(
767 def: &ConstraintDefinition,
768 properties: crate::PropsRef<'_>,
769 key: &str,
770 value: &PropertyValue,
771) -> Option<Vec<PropertyValue>> {
772 def.properties
774 .iter()
775 .map(|prop| {
776 if prop == key {
777 Some(value.clone())
778 } else {
779 properties.get(prop.as_str()).map(|v| v.to_owned())
780 }
781 })
782 .collect()
783}
784
785pub(crate) fn check_node_set_property(
789 catalog: &ConstraintCatalog,
790 graph: &InMemoryGraph,
791 node_id: NodeId,
792 key: &str,
793 value: &PropertyValue,
794) -> Result<(), ConstraintViolation> {
795 let node = match graph.node_at(node_id) {
796 Some(n) => n,
797 None => return Ok(()), };
799 for def in catalog.iter() {
800 if def.entity != StoredIndexEntity::Node {
801 continue;
802 }
803 if !node.labels().strs().any(|l| l == def.label) {
804 continue;
805 }
806 if !def.properties.iter().any(|p| p == key) {
807 continue;
808 }
809 if let StoredConstraintKind::PropertyType(target) = &def.kind {
811 if !value_matches_property_type(value, target) {
812 return Err(ConstraintViolation::WrongPropertyType {
813 constraint: def.name.clone(),
814 entity: def.entity,
815 label: def.label.clone(),
816 property: key.to_string(),
817 expected: target.to_string(),
818 });
819 }
820 }
821 if def.kind.requires_uniqueness() {
824 if let Some(tuple) = constrained_tuple_after_set(def, node.properties(), key, value) {
825 if any_other_node_with_tuple(
826 graph,
827 &def.label,
828 &def.properties,
829 &tuple,
830 Some(node_id),
831 ) {
832 return Err(uniqueness_violation(def));
833 }
834 }
835 }
836 }
837 Ok(())
838}
839
840pub(crate) fn check_node_remove_property(
843 catalog: &ConstraintCatalog,
844 graph: &InMemoryGraph,
845 node_id: NodeId,
846 key: &str,
847) -> Result<(), ConstraintViolation> {
848 let node = match graph.node_at(node_id) {
849 Some(n) => n,
850 None => return Ok(()),
851 };
852 for def in catalog.iter() {
853 if def.entity != StoredIndexEntity::Node {
854 continue;
855 }
856 if !node.labels().strs().any(|l| l == def.label) {
857 continue;
858 }
859 if !def.kind.requires_existence() {
860 continue;
861 }
862 if !def.properties.iter().any(|p| p == key) {
863 continue;
864 }
865 return Err(missing_property_violation(def, key));
866 }
867 Ok(())
868}
869
870pub(crate) fn check_node_replace_properties(
874 catalog: &ConstraintCatalog,
875 graph: &InMemoryGraph,
876 node_id: NodeId,
877 properties: &Properties,
878) -> Result<(), ConstraintViolation> {
879 check_node_replace_properties_with(catalog, graph, node_id, properties, true)
880}
881
882pub(crate) fn check_node_replace_properties_deferred(
886 catalog: &ConstraintCatalog,
887 graph: &InMemoryGraph,
888 node_id: NodeId,
889 properties: &Properties,
890) -> Result<(), ConstraintViolation> {
891 check_node_replace_properties_with(catalog, graph, node_id, properties, false)
892}
893
894fn check_node_replace_properties_with(
895 catalog: &ConstraintCatalog,
896 graph: &InMemoryGraph,
897 node_id: NodeId,
898 properties: &Properties,
899 check_existence: bool,
900) -> Result<(), ConstraintViolation> {
901 let labels = match graph.node_at(node_id) {
902 Some(node) => node.labels().to_owned(),
903 None => return Ok(()),
904 };
905 check_record_constraints_with(
906 catalog,
907 graph,
908 ConstraintRecord::Node {
909 labels: NodeLabelMatcher::Names(&labels),
910 properties,
911 skip: Some(node_id),
912 },
913 check_existence,
914 )
915}
916
917pub(crate) fn check_relationship_set_property(
918 catalog: &ConstraintCatalog,
919 graph: &InMemoryGraph,
920 rel_id: RelationshipId,
921 key: &str,
922 value: &PropertyValue,
923) -> Result<(), ConstraintViolation> {
924 let rel = match graph.rel_at(rel_id) {
925 Some(r) => r,
926 None => return Ok(()),
927 };
928 for def in catalog.iter() {
929 if def.entity != StoredIndexEntity::Relationship {
930 continue;
931 }
932 if def.label != rel.rel_type() {
933 continue;
934 }
935 if !def.properties.iter().any(|p| p == key) {
936 continue;
937 }
938 if let StoredConstraintKind::PropertyType(target) = &def.kind {
939 if !value_matches_property_type(value, target) {
940 return Err(ConstraintViolation::WrongPropertyType {
941 constraint: def.name.clone(),
942 entity: def.entity,
943 label: def.label.clone(),
944 property: key.to_string(),
945 expected: target.to_string(),
946 });
947 }
948 }
949 if def.kind.requires_uniqueness() {
950 if let Some(tuple) = constrained_tuple_after_set(def, rel.properties(), key, value) {
951 if any_other_rel_with_tuple(
952 graph,
953 &def.label,
954 &def.properties,
955 &tuple,
956 Some(rel_id),
957 ) {
958 return Err(uniqueness_violation(def));
959 }
960 }
961 }
962 }
963 Ok(())
964}
965
966pub(crate) fn check_relationship_remove_property(
967 catalog: &ConstraintCatalog,
968 graph: &InMemoryGraph,
969 rel_id: RelationshipId,
970 key: &str,
971) -> Result<(), ConstraintViolation> {
972 let rel = match graph.rel_at(rel_id) {
973 Some(r) => r,
974 None => return Ok(()),
975 };
976 for def in catalog.iter() {
977 if def.entity != StoredIndexEntity::Relationship {
978 continue;
979 }
980 if def.label != rel.rel_type() {
981 continue;
982 }
983 if !def.kind.requires_existence() {
984 continue;
985 }
986 if !def.properties.iter().any(|p| p == key) {
987 continue;
988 }
989 return Err(missing_property_violation(def, key));
990 }
991 Ok(())
992}
993
994pub(crate) fn check_relationship_replace_properties(
998 catalog: &ConstraintCatalog,
999 graph: &InMemoryGraph,
1000 rel_id: RelationshipId,
1001 properties: &Properties,
1002) -> Result<(), ConstraintViolation> {
1003 check_relationship_replace_properties_with(catalog, graph, rel_id, properties, true)
1004}
1005
1006pub(crate) fn check_relationship_replace_properties_deferred(
1008 catalog: &ConstraintCatalog,
1009 graph: &InMemoryGraph,
1010 rel_id: RelationshipId,
1011 properties: &Properties,
1012) -> Result<(), ConstraintViolation> {
1013 check_relationship_replace_properties_with(catalog, graph, rel_id, properties, false)
1014}
1015
1016fn check_relationship_replace_properties_with(
1017 catalog: &ConstraintCatalog,
1018 graph: &InMemoryGraph,
1019 rel_id: RelationshipId,
1020 properties: &Properties,
1021 check_existence: bool,
1022) -> Result<(), ConstraintViolation> {
1023 let rel = match graph.rel_at(rel_id) {
1024 Some(r) => r,
1025 None => return Ok(()),
1026 };
1027 check_record_constraints_with(
1028 catalog,
1029 graph,
1030 ConstraintRecord::Relationship {
1031 rel_type: rel.rel_type(),
1032 properties,
1033 skip: Some(rel_id),
1034 },
1035 check_existence,
1036 )
1037}
1038
1039pub(crate) fn check_node_add_label(
1044 catalog: &ConstraintCatalog,
1045 graph: &InMemoryGraph,
1046 node_id: NodeId,
1047 label: &str,
1048) -> Result<(), ConstraintViolation> {
1049 let node = match graph.node_at(node_id) {
1050 Some(n) => n,
1051 None => return Ok(()),
1052 };
1053 check_record_constraints(
1054 catalog,
1055 graph,
1056 ConstraintRecord::Node {
1057 labels: NodeLabelMatcher::One(label),
1058 properties: &node.properties().to_owned(),
1059 skip: Some(node_id),
1060 },
1061 )
1062}
1063
1064pub(crate) fn check_node_add_label_deferred(
1068 catalog: &ConstraintCatalog,
1069 graph: &InMemoryGraph,
1070 node_id: NodeId,
1071 label: &str,
1072) -> Result<(), ConstraintViolation> {
1073 let Some(node) = graph.node_at(node_id) else {
1074 return Ok(());
1075 };
1076 check_record_constraints_with(
1077 catalog,
1078 graph,
1079 ConstraintRecord::Node {
1080 labels: NodeLabelMatcher::One(label),
1081 properties: &node.properties().to_owned(),
1082 skip: Some(node_id),
1083 },
1084 false,
1085 )
1086}