1#![allow(dead_code)]
25
26use std::fmt::Write as _;
27use std::hash::{BuildHasher, BuildHasherDefault};
28
29use thiserror::Error;
30
31use super::constraint_catalog::{
32 ConstraintCatalog, ConstraintDefinition, StoredConstraintKind, StoredPropertyType,
33 StoredPropertyTypeTerm, StoredScalarType, StoredVectorCoordType,
34};
35use super::cow::{FastHashMap, KeyHasher};
36use super::index_catalog::StoredIndexEntity;
37use super::InMemoryGraph;
38use crate::types::{NodeId, Properties, PropertyValue, RelationshipId};
39
40#[derive(Debug, Clone, Error)]
51pub enum ConstraintViolation {
52 #[error("[22N77] property presence verification failed. {kind} must have the property `{property}`",
53 kind = entity_kind_label(*entity, label))]
54 MissingProperty {
55 constraint: String,
56 entity: StoredIndexEntity,
57 label: String,
58 property: String,
59 },
60 #[error("[22N77] property presence verification failed. {kind} must have the properties: {properties}",
61 kind = entity_kind_label(*entity, label),
62 properties = properties.join(", "))]
63 MissingPropertiesForKey {
64 constraint: String,
65 entity: StoredIndexEntity,
66 label: String,
67 properties: Vec<String>,
68 },
69 #[error("[22N78] property type verification failed. {kind} must have property `{property}` with value type {expected}",
70 kind = entity_kind_label(*entity, label))]
71 WrongPropertyType {
72 constraint: String,
73 entity: StoredIndexEntity,
74 label: String,
75 property: String,
76 expected: String,
77 },
78 #[error("[22N79] property uniqueness constraint violated. {kind} already has property `{property}` with the supplied value",
79 kind = entity_kind_label(*entity, label))]
80 UniquenessViolated {
81 constraint: String,
82 entity: StoredIndexEntity,
83 label: String,
84 property: String,
85 },
86}
87
88impl ConstraintViolation {
89 pub const fn gql_status(&self) -> &'static str {
90 match self {
91 ConstraintViolation::MissingProperty { .. }
92 | ConstraintViolation::MissingPropertiesForKey { .. } => "22N77",
93 ConstraintViolation::WrongPropertyType { .. } => "22N78",
94 ConstraintViolation::UniquenessViolated { .. } => "22N79",
95 }
96 }
97
98 pub fn constraint_name(&self) -> &str {
99 match self {
100 ConstraintViolation::MissingProperty { constraint, .. }
101 | ConstraintViolation::MissingPropertiesForKey { constraint, .. }
102 | ConstraintViolation::WrongPropertyType { constraint, .. }
103 | ConstraintViolation::UniquenessViolated { constraint, .. } => constraint,
104 }
105 }
106}
107
108fn entity_kind_label(entity: StoredIndexEntity, label: &str) -> String {
109 match entity {
110 StoredIndexEntity::Node => format!("NODE with label `{label}`"),
111 StoredIndexEntity::Relationship => format!("RELATIONSHIP with type `{label}`"),
112 }
113}
114
115impl InMemoryGraph {
116 pub(super) fn validate_existing_data_for_constraint(
124 &self,
125 def: &ConstraintDefinition,
126 ) -> Result<(), ConstraintViolation> {
127 match def.entity {
128 StoredIndexEntity::Node => self.validate_existing_nodes_for_constraint(def),
129 StoredIndexEntity::Relationship => self.validate_existing_rels_for_constraint(def),
130 }
131 }
132
133 fn validate_existing_nodes_for_constraint(
134 &self,
135 def: &ConstraintDefinition,
136 ) -> Result<(), ConstraintViolation> {
137 let label = def.label.as_str();
138 let mut seen = SeenTuples::default();
139 for (id, node) in self.iter_nodes() {
140 if !node.labels().strs().any(|l| l == label) {
141 continue;
142 }
143 let properties = node.properties().to_owned();
144 validate_record_against_constraint(def, &properties, true)?;
145 seen.insert(def, id, &properties, |other| {
146 self.node_at(other).map(|n| n.properties().to_owned())
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 = SeenTuples::default();
158 for (id, rel) in self.iter_rels() {
159 if rel.rel_type() != rel_type {
160 continue;
161 }
162 let properties = rel.properties().to_owned();
163 validate_record_against_constraint(def, &properties, true)?;
164 seen.insert(def, id, &properties, |other| {
165 self.rel_at(other).map(|r| r.properties().to_owned())
166 })?;
167 }
168 Ok(())
169 }
170}
171
172#[derive(Default)]
179struct SeenTuples {
180 first: FastHashMap<u64, u64>,
182 collided: Vec<(u64, u64)>,
184 key: String,
186}
187
188impl SeenTuples {
189 fn insert(
195 &mut self,
196 def: &ConstraintDefinition,
197 id: u64,
198 properties: &Properties,
199 properties_of: impl Fn(u64) -> Option<Properties>,
200 ) -> Result<(), ConstraintViolation> {
201 if !def.kind.requires_uniqueness()
202 || !def
203 .properties
204 .iter()
205 .all(|p| properties.contains_key(p.as_str()))
206 {
207 return Ok(());
208 }
209 self.key.clear();
210 write_property_tuple_key(&mut self.key, def, properties);
211 let hash = BuildHasherDefault::<KeyHasher>::default().hash_one(self.key.as_str());
212 self.insert_hashed(def, hash, id, properties_of)
213 }
214
215 fn insert_hashed(
217 &mut self,
218 def: &ConstraintDefinition,
219 hash: u64,
220 id: u64,
221 properties_of: impl Fn(u64) -> Option<Properties>,
222 ) -> Result<(), ConstraintViolation> {
223 let Some(&first) = self.first.get(&hash) else {
224 self.first.insert(hash, id);
225 return Ok(());
226 };
227 let earlier = std::iter::once(first).chain(
228 self.collided
229 .iter()
230 .filter(|(h, _)| *h == hash)
231 .map(|&(_, other)| other),
232 );
233 let mut other_key = String::new();
234 for other in earlier {
235 other_key.clear();
236 if let Some(other_properties) = properties_of(other) {
237 write_property_tuple_key(&mut other_key, def, &other_properties);
238 }
239 if other_key == self.key {
240 return Err(uniqueness_violation(def));
241 }
242 }
243 self.collided.push((hash, id));
244 Ok(())
245 }
246}
247
248fn validate_record_against_constraint(
249 def: &ConstraintDefinition,
250 properties: &Properties,
251 check_existence: bool,
252) -> Result<(), ConstraintViolation> {
253 if check_existence && def.kind.requires_existence() {
255 let missing: Vec<String> = def
256 .properties
257 .iter()
258 .filter(|p| !properties.contains_key(p.as_str()))
259 .cloned()
260 .collect();
261 if !missing.is_empty() {
262 return Err(if def.properties.len() == 1 {
263 ConstraintViolation::MissingProperty {
264 constraint: def.name.clone(),
265 entity: def.entity,
266 label: def.label.clone(),
267 property: def.properties[0].clone(),
268 }
269 } else {
270 ConstraintViolation::MissingPropertiesForKey {
271 constraint: def.name.clone(),
272 entity: def.entity,
273 label: def.label.clone(),
274 properties: def.properties.clone(),
275 }
276 });
277 }
278 }
279
280 if let StoredConstraintKind::PropertyType(target) = &def.kind {
282 let key = &def.properties[0];
283 if let Some(value) = properties.get(key.as_str()) {
284 if !value_matches_property_type(value, target) {
285 return Err(ConstraintViolation::WrongPropertyType {
286 constraint: def.name.clone(),
287 entity: def.entity,
288 label: def.label.clone(),
289 property: key.clone(),
290 expected: target.to_string(),
291 });
292 }
293 }
294 }
295
296 Ok(())
297}
298
299fn write_property_tuple_key(out: &mut String, def: &ConstraintDefinition, properties: &Properties) {
303 for (i, key) in def.properties.iter().enumerate() {
304 if i > 0 {
305 out.push('\u{1f}');
306 }
307 if let Some(value) = properties.get(key.as_str()) {
308 append_property_value_key(out, value);
309 }
310 }
311}
312
313fn append_property_value_key(out: &mut String, value: &PropertyValue) {
314 match value {
315 PropertyValue::Null => out.push('N'),
316 PropertyValue::Bool(b) => {
317 out.push('B');
318 out.push(if *b { 'T' } else { 'F' });
319 }
320 PropertyValue::Int(i) => {
321 out.push('I');
322 out.push_str(&i.to_string());
323 }
324 PropertyValue::Float(f) => {
325 out.push('F');
326 out.push_str(&format!("{f:?}"));
327 }
328 PropertyValue::String(s) => {
329 out.push('S');
330 append_len_prefixed_str(out, s);
331 }
332 PropertyValue::Date(d) => {
333 out.push_str("D:");
334 append_len_prefixed_str(out, &format!("{d:?}"));
335 }
336 PropertyValue::Time(t) => {
337 out.push_str("T:");
338 append_len_prefixed_str(out, &format!("{t:?}"));
339 }
340 PropertyValue::LocalTime(t) => {
341 out.push_str("LT:");
342 append_len_prefixed_str(out, &format!("{t:?}"));
343 }
344 PropertyValue::DateTime(dt) => {
345 out.push_str("DT:");
346 append_len_prefixed_str(out, &format!("{dt:?}"));
347 }
348 PropertyValue::LocalDateTime(dt) => {
349 out.push_str("LDT:");
350 append_len_prefixed_str(out, &format!("{dt:?}"));
351 }
352 PropertyValue::Duration(d) => {
353 out.push_str("DUR:");
354 append_len_prefixed_str(out, &format!("{d:?}"));
355 }
356 PropertyValue::Point(p) => {
357 out.push_str("P:");
358 append_len_prefixed_str(out, &format!("{p:?}"));
359 }
360 PropertyValue::Vector(v) => {
361 out.push_str("V:");
362 append_len_prefixed_str(out, &v.to_key_string());
363 }
364 PropertyValue::List(items) => {
365 out.push('L');
366 append_len(out, items.len());
367 for item in items {
368 append_property_value_key(out, item);
369 }
370 }
371 PropertyValue::Map(entries) => {
372 out.push('M');
373 append_len(out, entries.len());
374 for (k, v) in entries {
375 append_len_prefixed_str(out, k);
376 append_property_value_key(out, v);
377 }
378 }
379 PropertyValue::Binary(b) => {
380 out.push_str("BIN:");
381 append_len(out, b.len());
382 for segment in b.chunks() {
383 for byte in segment {
384 let _ = write!(out, "{byte:02x}");
385 }
386 }
387 }
388 }
389}
390
391fn append_len(out: &mut String, len: usize) {
392 out.push_str(&len.to_string());
393 out.push(':');
394}
395
396fn append_len_prefixed_str(out: &mut String, value: &str) {
397 append_len(out, value.len());
398 out.push_str(value);
399}
400
401pub fn value_matches_property_type(value: &PropertyValue, target: &StoredPropertyType) -> bool {
404 target
405 .alternatives
406 .iter()
407 .any(|term| value_matches_term(value, term))
408}
409
410fn value_matches_term(value: &PropertyValue, term: &StoredPropertyTypeTerm) -> bool {
411 match term {
412 StoredPropertyTypeTerm::Scalar(scalar) => value_matches_scalar(value, *scalar),
413 StoredPropertyTypeTerm::List { inner, not_null } => match value {
414 PropertyValue::List(items) => items.iter().all(|item| {
415 if matches!(item, PropertyValue::Null) {
416 !*not_null
417 } else {
418 value_matches_term(item, inner)
419 }
420 }),
421 _ => false,
422 },
423 StoredPropertyTypeTerm::Vector { coord, dimension } => match value {
424 PropertyValue::Vector(v) => vector_matches(v, *coord, *dimension),
425 _ => false,
426 },
427 }
428}
429
430fn value_matches_scalar(value: &PropertyValue, scalar: StoredScalarType) -> bool {
431 match (value, scalar) {
432 (PropertyValue::Bool(_), StoredScalarType::Boolean) => true,
433 (PropertyValue::String(_), StoredScalarType::String) => true,
434 (PropertyValue::Int(_), StoredScalarType::Integer) => true,
435 (PropertyValue::Float(_), StoredScalarType::Float) => true,
436 (PropertyValue::Date(_), StoredScalarType::Date) => true,
437 (PropertyValue::Time(_), StoredScalarType::ZonedTime) => true,
438 (PropertyValue::LocalTime(_), StoredScalarType::LocalTime) => true,
439 (PropertyValue::DateTime(_), StoredScalarType::ZonedDateTime) => true,
440 (PropertyValue::LocalDateTime(_), StoredScalarType::LocalDateTime) => true,
441 (PropertyValue::Duration(_), StoredScalarType::Duration) => true,
442 (PropertyValue::Point(_), StoredScalarType::Point) => true,
443 _ => false,
447 }
448}
449
450fn vector_matches(
451 vector: &crate::types::LoraVector,
452 coord: StoredVectorCoordType,
453 dimension: u32,
454) -> bool {
455 if vector.dimension != dimension as usize {
456 return false;
457 }
458 use crate::types::VectorValues;
459 use StoredVectorCoordType::*;
460 matches!(
461 (&vector.values, coord),
462 (VectorValues::Float64(_), Float64)
463 | (VectorValues::Float32(_), Float32)
464 | (VectorValues::Integer64(_), Int64)
465 | (VectorValues::Integer32(_), Int32)
466 | (VectorValues::Integer16(_), Int16)
467 | (VectorValues::Integer8(_), Int8)
468 )
469}
470
471#[derive(Clone, Copy)]
472enum NodeLabelMatcher<'a> {
473 AnyOf(&'a [String]),
474 Names(&'a [crate::Name]),
475 One(&'a str),
476}
477
478impl NodeLabelMatcher<'_> {
479 fn contains(self, label: &str) -> bool {
480 match self {
481 NodeLabelMatcher::AnyOf(labels) => labels.iter().any(|l| l == label),
482 NodeLabelMatcher::Names(labels) => labels.iter().any(|l| l == label),
483 NodeLabelMatcher::One(candidate) => candidate == label,
484 }
485 }
486}
487
488#[derive(Clone, Copy)]
489enum ConstraintRecord<'a> {
490 Node {
491 labels: NodeLabelMatcher<'a>,
492 properties: &'a Properties,
493 skip: Option<NodeId>,
494 },
495 Relationship {
496 rel_type: &'a str,
497 properties: &'a Properties,
498 skip: Option<RelationshipId>,
499 },
500}
501
502impl<'a> ConstraintRecord<'a> {
503 fn applies_to(self, def: &ConstraintDefinition) -> bool {
504 match self {
505 ConstraintRecord::Node { labels, .. } => {
506 def.entity == StoredIndexEntity::Node && labels.contains(&def.label)
507 }
508 ConstraintRecord::Relationship { rel_type, .. } => {
509 def.entity == StoredIndexEntity::Relationship && def.label == rel_type
510 }
511 }
512 }
513
514 fn properties(self) -> &'a Properties {
515 match self {
516 ConstraintRecord::Node { properties, .. }
517 | ConstraintRecord::Relationship { properties, .. } => properties,
518 }
519 }
520
521 fn has_uniqueness_conflict(
522 self,
523 graph: &InMemoryGraph,
524 def: &ConstraintDefinition,
525 tuple: &[PropertyValue],
526 ) -> bool {
527 match self {
528 ConstraintRecord::Node { skip, .. } => {
529 any_other_node_with_tuple(graph, &def.label, &def.properties, tuple, skip)
530 }
531 ConstraintRecord::Relationship { skip, .. } => {
532 any_other_rel_with_tuple(graph, &def.label, &def.properties, tuple, skip)
533 }
534 }
535 }
536}
537
538fn check_record_constraints(
539 catalog: &ConstraintCatalog,
540 graph: &InMemoryGraph,
541 record: ConstraintRecord<'_>,
542) -> Result<(), ConstraintViolation> {
543 check_record_constraints_with(catalog, graph, record, true)
544}
545
546fn check_record_constraints_with(
547 catalog: &ConstraintCatalog,
548 graph: &InMemoryGraph,
549 record: ConstraintRecord<'_>,
550 check_existence: bool,
551) -> Result<(), ConstraintViolation> {
552 for def in catalog.iter() {
553 if !record.applies_to(def) {
554 continue;
555 }
556
557 let properties = record.properties();
558 validate_record_against_constraint(def, properties, check_existence)?;
559
560 if def.kind.requires_uniqueness() {
561 if let Some(tuple) = constrained_tuple(def, properties) {
562 if record.has_uniqueness_conflict(graph, def, &tuple) {
563 return Err(uniqueness_violation(def));
564 }
565 }
566 }
567 }
568 Ok(())
569}
570
571pub(crate) fn check_node_create(
576 catalog: &ConstraintCatalog,
577 graph: &InMemoryGraph,
578 labels: &[String],
579 properties: &Properties,
580) -> Result<(), ConstraintViolation> {
581 check_record_constraints(
582 catalog,
583 graph,
584 ConstraintRecord::Node {
585 labels: NodeLabelMatcher::AnyOf(labels),
586 properties,
587 skip: None,
588 },
589 )
590}
591
592pub(crate) fn check_relationship_create(
593 catalog: &ConstraintCatalog,
594 graph: &InMemoryGraph,
595 rel_type: &str,
596 properties: &Properties,
597) -> Result<(), ConstraintViolation> {
598 check_record_constraints(
599 catalog,
600 graph,
601 ConstraintRecord::Relationship {
602 rel_type,
603 properties,
604 skip: None,
605 },
606 )
607}
608
609pub(crate) fn check_node_create_deferred(
614 catalog: &ConstraintCatalog,
615 graph: &InMemoryGraph,
616 labels: &[String],
617 properties: &Properties,
618) -> Result<(), ConstraintViolation> {
619 check_record_constraints_with(
620 catalog,
621 graph,
622 ConstraintRecord::Node {
623 labels: NodeLabelMatcher::AnyOf(labels),
624 properties,
625 skip: None,
626 },
627 false,
628 )
629}
630
631pub(crate) fn check_relationship_create_deferred(
633 catalog: &ConstraintCatalog,
634 graph: &InMemoryGraph,
635 rel_type: &str,
636 properties: &Properties,
637) -> Result<(), ConstraintViolation> {
638 check_record_constraints_with(
639 catalog,
640 graph,
641 ConstraintRecord::Relationship {
642 rel_type,
643 properties,
644 skip: None,
645 },
646 false,
647 )
648}
649
650pub(crate) fn check_node_existence(
653 catalog: &ConstraintCatalog,
654 graph: &InMemoryGraph,
655 node_id: NodeId,
656) -> Result<(), ConstraintViolation> {
657 let Some(node) = graph.node_at(node_id) else {
658 return Ok(());
659 };
660 for def in catalog.iter() {
661 if def.entity != StoredIndexEntity::Node
662 || !def.kind.requires_existence()
663 || !node.labels().strs().any(|l| l == def.label)
664 {
665 continue;
666 }
667 if let Some(missing) = def
668 .properties
669 .iter()
670 .find(|p| !node.properties().contains_key(p.as_str()))
671 {
672 return Err(missing_property_violation(def, missing));
673 }
674 }
675 Ok(())
676}
677
678pub(crate) fn check_relationship_existence(
680 catalog: &ConstraintCatalog,
681 graph: &InMemoryGraph,
682 rel_id: RelationshipId,
683) -> Result<(), ConstraintViolation> {
684 let Some(rel) = graph.rel_at(rel_id) else {
685 return Ok(());
686 };
687 for def in catalog.iter() {
688 if def.entity != StoredIndexEntity::Relationship
689 || !def.kind.requires_existence()
690 || rel.rel_type() != def.label
691 {
692 continue;
693 }
694 if let Some(missing) = def
695 .properties
696 .iter()
697 .find(|p| !rel.properties().contains_key(p.as_str()))
698 {
699 return Err(missing_property_violation(def, missing));
700 }
701 }
702 Ok(())
703}
704
705fn any_other_node_with_tuple(
714 graph: &InMemoryGraph,
715 label: &str,
716 keys: &[String],
717 target: &[PropertyValue],
718 skip: Option<NodeId>,
719) -> bool {
720 let tuple_matches = |node: crate::NodeRef<'_>| {
721 node.has_label(label)
722 && keys.iter().enumerate().all(|(idx, key)| {
723 node.properties()
724 .get(key.as_str())
725 .is_some_and(|v| v == target[idx])
726 })
727 };
728 let candidates = match (keys.first(), target.first()) {
729 (Some(key), Some(value)) => graph.indexed_node_ids(label, key, value),
730 _ => None,
731 };
732 let candidates =
733 candidates.unwrap_or_else(|| crate::GraphStorage::node_ids_by_label(graph, label));
734 candidates
735 .into_iter()
736 .filter(|id| Some(*id) != skip)
737 .any(|id| graph.node_at(id).is_some_and(tuple_matches))
738}
739
740fn any_other_rel_with_tuple(
742 graph: &InMemoryGraph,
743 rel_type: &str,
744 keys: &[String],
745 target: &[PropertyValue],
746 skip: Option<RelationshipId>,
747) -> bool {
748 let tuple_matches = |rel: crate::RelRef<'_>| {
749 rel.rel_type() == rel_type
750 && keys.iter().enumerate().all(|(idx, key)| {
751 rel.properties()
752 .get(key.as_str())
753 .is_some_and(|v| v == target[idx])
754 })
755 };
756 let candidates = match (keys.first(), target.first()) {
757 (Some(key), Some(value)) => graph.indexed_rel_ids(rel_type, key, value),
758 _ => None,
759 };
760 let candidates =
761 candidates.unwrap_or_else(|| crate::GraphStorage::rel_ids_by_type(graph, rel_type));
762 candidates
763 .into_iter()
764 .filter(|id| Some(*id) != skip)
765 .any(|id| graph.rel_at(id).is_some_and(tuple_matches))
766}
767
768fn render_constraint_property_label(def: &ConstraintDefinition) -> String {
769 if def.properties.len() == 1 {
770 def.properties[0].clone()
771 } else {
772 def.properties.join(", ")
773 }
774}
775
776fn uniqueness_violation(def: &ConstraintDefinition) -> ConstraintViolation {
777 ConstraintViolation::UniquenessViolated {
778 constraint: def.name.clone(),
779 entity: def.entity,
780 label: def.label.clone(),
781 property: render_constraint_property_label(def),
782 }
783}
784
785fn missing_property_violation(def: &ConstraintDefinition, property: &str) -> ConstraintViolation {
786 if def.properties.len() == 1 {
787 ConstraintViolation::MissingProperty {
788 constraint: def.name.clone(),
789 entity: def.entity,
790 label: def.label.clone(),
791 property: property.to_string(),
792 }
793 } else {
794 ConstraintViolation::MissingPropertiesForKey {
795 constraint: def.name.clone(),
796 entity: def.entity,
797 label: def.label.clone(),
798 properties: def.properties.clone(),
799 }
800 }
801}
802
803fn constrained_tuple(
804 def: &ConstraintDefinition,
805 properties: &Properties,
806) -> Option<Vec<PropertyValue>> {
807 def.properties
808 .iter()
809 .map(|p| properties.get(p.as_str()).cloned())
810 .collect()
811}
812
813fn constrained_tuple_after_set(
814 def: &ConstraintDefinition,
815 properties: crate::PropsRef<'_>,
816 key: &str,
817 value: &PropertyValue,
818) -> Option<Vec<PropertyValue>> {
819 def.properties
821 .iter()
822 .map(|prop| {
823 if prop == key {
824 Some(value.clone())
825 } else {
826 properties.get(prop.as_str()).map(|v| v.to_owned())
827 }
828 })
829 .collect()
830}
831
832pub(crate) fn check_node_set_property(
836 catalog: &ConstraintCatalog,
837 graph: &InMemoryGraph,
838 node_id: NodeId,
839 key: &str,
840 value: &PropertyValue,
841) -> Result<(), ConstraintViolation> {
842 let node = match graph.node_at(node_id) {
843 Some(n) => n,
844 None => return Ok(()), };
846 for def in catalog.iter() {
847 if def.entity != StoredIndexEntity::Node {
848 continue;
849 }
850 if !node.labels().strs().any(|l| l == def.label) {
851 continue;
852 }
853 if !def.properties.iter().any(|p| p == key) {
854 continue;
855 }
856 if let StoredConstraintKind::PropertyType(target) = &def.kind {
858 if !value_matches_property_type(value, target) {
859 return Err(ConstraintViolation::WrongPropertyType {
860 constraint: def.name.clone(),
861 entity: def.entity,
862 label: def.label.clone(),
863 property: key.to_string(),
864 expected: target.to_string(),
865 });
866 }
867 }
868 if def.kind.requires_uniqueness() {
871 if let Some(tuple) = constrained_tuple_after_set(def, node.properties(), key, value) {
872 if any_other_node_with_tuple(
873 graph,
874 &def.label,
875 &def.properties,
876 &tuple,
877 Some(node_id),
878 ) {
879 return Err(uniqueness_violation(def));
880 }
881 }
882 }
883 }
884 Ok(())
885}
886
887pub(crate) fn check_node_remove_property(
890 catalog: &ConstraintCatalog,
891 graph: &InMemoryGraph,
892 node_id: NodeId,
893 key: &str,
894) -> Result<(), ConstraintViolation> {
895 let node = match graph.node_at(node_id) {
896 Some(n) => n,
897 None => return Ok(()),
898 };
899 for def in catalog.iter() {
900 if def.entity != StoredIndexEntity::Node {
901 continue;
902 }
903 if !node.labels().strs().any(|l| l == def.label) {
904 continue;
905 }
906 if !def.kind.requires_existence() {
907 continue;
908 }
909 if !def.properties.iter().any(|p| p == key) {
910 continue;
911 }
912 return Err(missing_property_violation(def, key));
913 }
914 Ok(())
915}
916
917pub(crate) fn check_node_replace_properties(
921 catalog: &ConstraintCatalog,
922 graph: &InMemoryGraph,
923 node_id: NodeId,
924 properties: &Properties,
925) -> Result<(), ConstraintViolation> {
926 check_node_replace_properties_with(catalog, graph, node_id, properties, true)
927}
928
929pub(crate) fn check_node_replace_properties_deferred(
933 catalog: &ConstraintCatalog,
934 graph: &InMemoryGraph,
935 node_id: NodeId,
936 properties: &Properties,
937) -> Result<(), ConstraintViolation> {
938 check_node_replace_properties_with(catalog, graph, node_id, properties, false)
939}
940
941fn check_node_replace_properties_with(
942 catalog: &ConstraintCatalog,
943 graph: &InMemoryGraph,
944 node_id: NodeId,
945 properties: &Properties,
946 check_existence: bool,
947) -> Result<(), ConstraintViolation> {
948 let labels = match graph.node_at(node_id) {
949 Some(node) => node.labels().to_owned(),
950 None => return Ok(()),
951 };
952 check_record_constraints_with(
953 catalog,
954 graph,
955 ConstraintRecord::Node {
956 labels: NodeLabelMatcher::Names(&labels),
957 properties,
958 skip: Some(node_id),
959 },
960 check_existence,
961 )
962}
963
964pub(crate) fn check_relationship_set_property(
965 catalog: &ConstraintCatalog,
966 graph: &InMemoryGraph,
967 rel_id: RelationshipId,
968 key: &str,
969 value: &PropertyValue,
970) -> Result<(), ConstraintViolation> {
971 let rel = match graph.rel_at(rel_id) {
972 Some(r) => r,
973 None => return Ok(()),
974 };
975 for def in catalog.iter() {
976 if def.entity != StoredIndexEntity::Relationship {
977 continue;
978 }
979 if def.label != rel.rel_type() {
980 continue;
981 }
982 if !def.properties.iter().any(|p| p == key) {
983 continue;
984 }
985 if let StoredConstraintKind::PropertyType(target) = &def.kind {
986 if !value_matches_property_type(value, target) {
987 return Err(ConstraintViolation::WrongPropertyType {
988 constraint: def.name.clone(),
989 entity: def.entity,
990 label: def.label.clone(),
991 property: key.to_string(),
992 expected: target.to_string(),
993 });
994 }
995 }
996 if def.kind.requires_uniqueness() {
997 if let Some(tuple) = constrained_tuple_after_set(def, rel.properties(), key, value) {
998 if any_other_rel_with_tuple(
999 graph,
1000 &def.label,
1001 &def.properties,
1002 &tuple,
1003 Some(rel_id),
1004 ) {
1005 return Err(uniqueness_violation(def));
1006 }
1007 }
1008 }
1009 }
1010 Ok(())
1011}
1012
1013pub(crate) fn check_relationship_remove_property(
1014 catalog: &ConstraintCatalog,
1015 graph: &InMemoryGraph,
1016 rel_id: RelationshipId,
1017 key: &str,
1018) -> Result<(), ConstraintViolation> {
1019 let rel = match graph.rel_at(rel_id) {
1020 Some(r) => r,
1021 None => return Ok(()),
1022 };
1023 for def in catalog.iter() {
1024 if def.entity != StoredIndexEntity::Relationship {
1025 continue;
1026 }
1027 if def.label != rel.rel_type() {
1028 continue;
1029 }
1030 if !def.kind.requires_existence() {
1031 continue;
1032 }
1033 if !def.properties.iter().any(|p| p == key) {
1034 continue;
1035 }
1036 return Err(missing_property_violation(def, key));
1037 }
1038 Ok(())
1039}
1040
1041pub(crate) fn check_relationship_replace_properties(
1045 catalog: &ConstraintCatalog,
1046 graph: &InMemoryGraph,
1047 rel_id: RelationshipId,
1048 properties: &Properties,
1049) -> Result<(), ConstraintViolation> {
1050 check_relationship_replace_properties_with(catalog, graph, rel_id, properties, true)
1051}
1052
1053pub(crate) fn check_relationship_replace_properties_deferred(
1055 catalog: &ConstraintCatalog,
1056 graph: &InMemoryGraph,
1057 rel_id: RelationshipId,
1058 properties: &Properties,
1059) -> Result<(), ConstraintViolation> {
1060 check_relationship_replace_properties_with(catalog, graph, rel_id, properties, false)
1061}
1062
1063fn check_relationship_replace_properties_with(
1064 catalog: &ConstraintCatalog,
1065 graph: &InMemoryGraph,
1066 rel_id: RelationshipId,
1067 properties: &Properties,
1068 check_existence: bool,
1069) -> Result<(), ConstraintViolation> {
1070 let rel = match graph.rel_at(rel_id) {
1071 Some(r) => r,
1072 None => return Ok(()),
1073 };
1074 check_record_constraints_with(
1075 catalog,
1076 graph,
1077 ConstraintRecord::Relationship {
1078 rel_type: rel.rel_type(),
1079 properties,
1080 skip: Some(rel_id),
1081 },
1082 check_existence,
1083 )
1084}
1085
1086pub(crate) fn check_node_add_label(
1091 catalog: &ConstraintCatalog,
1092 graph: &InMemoryGraph,
1093 node_id: NodeId,
1094 label: &str,
1095) -> Result<(), ConstraintViolation> {
1096 let node = match graph.node_at(node_id) {
1097 Some(n) => n,
1098 None => return Ok(()),
1099 };
1100 check_record_constraints(
1101 catalog,
1102 graph,
1103 ConstraintRecord::Node {
1104 labels: NodeLabelMatcher::One(label),
1105 properties: &node.properties().to_owned(),
1106 skip: Some(node_id),
1107 },
1108 )
1109}
1110
1111pub(crate) fn check_node_add_label_deferred(
1115 catalog: &ConstraintCatalog,
1116 graph: &InMemoryGraph,
1117 node_id: NodeId,
1118 label: &str,
1119) -> Result<(), ConstraintViolation> {
1120 let Some(node) = graph.node_at(node_id) else {
1121 return Ok(());
1122 };
1123 check_record_constraints_with(
1124 catalog,
1125 graph,
1126 ConstraintRecord::Node {
1127 labels: NodeLabelMatcher::One(label),
1128 properties: &node.properties().to_owned(),
1129 skip: Some(node_id),
1130 },
1131 false,
1132 )
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1138 use crate::intern;
1139
1140 fn unique_on(property: &str) -> ConstraintDefinition {
1141 ConstraintDefinition {
1142 backing: Default::default(),
1143 name: "u".into(),
1144 kind: StoredConstraintKind::Unique,
1145 entity: StoredIndexEntity::Node,
1146 label: "L".into(),
1147 properties: vec![property.into()],
1148 owned_index: Some("u".into()),
1149 }
1150 }
1151
1152 fn with(property: &str, value: PropertyValue) -> Properties {
1153 let mut properties = Properties::new();
1154 properties.insert(intern(property), value);
1155 properties
1156 }
1157
1158 fn insert_colliding(values: &[PropertyValue]) -> Result<(), ConstraintViolation> {
1162 let def = unique_on("k");
1163 let records: Vec<Properties> = values.iter().map(|v| with("k", v.clone())).collect();
1164 let mut seen = SeenTuples::default();
1165 for (id, properties) in records.iter().enumerate() {
1166 seen.key.clear();
1167 write_property_tuple_key(&mut seen.key, &def, properties);
1168 seen.insert_hashed(&def, 7, id as u64, |other| {
1169 records.get(other as usize).cloned()
1170 })?;
1171 }
1172 Ok(())
1173 }
1174
1175 #[test]
1176 fn a_shared_hash_is_not_a_duplicate() {
1177 let values = [
1179 PropertyValue::Int(1),
1180 PropertyValue::Float(1.0),
1181 PropertyValue::String("1".into()),
1182 PropertyValue::Int(2),
1183 ];
1184 assert!(insert_colliding(&values).is_ok());
1185 }
1186
1187 #[test]
1188 fn an_equal_tuple_behind_a_collision_is_found() {
1189 let values = [
1190 PropertyValue::Int(1),
1191 PropertyValue::Int(2),
1192 PropertyValue::Int(3),
1193 PropertyValue::Int(2),
1194 ];
1195 let err = insert_colliding(&values).unwrap_err();
1196 assert_eq!(err.gql_status(), "22N79");
1197 }
1198
1199 #[test]
1200 fn records_without_the_tuple_are_skipped() {
1201 let def = unique_on("k");
1202 let mut seen = SeenTuples::default();
1203 for id in 0..3 {
1204 seen.insert(&def, id, &with("other", PropertyValue::Int(1)), |_| None)
1205 .unwrap();
1206 }
1207 assert!(seen.first.is_empty());
1208 }
1209}