1#![allow(dead_code)]
25
26use std::fmt::Write as _;
27use std::hash::{BuildHasher, BuildHasherDefault};
28use std::sync::Arc;
29
30use thiserror::Error;
31
32use super::constraint_catalog::{
33 ConstraintCatalog, ConstraintDefinition, StoredConstraintKind, StoredPropertyType,
34 StoredPropertyTypeTerm, StoredScalarType, StoredVectorCoordType,
35};
36use super::cow::KeyHasher;
37use super::index_catalog::StoredIndexEntity;
38use super::InMemoryGraph;
39use crate::encoded::{StoredNode, StoredRel};
40use crate::types::{NodeId, Properties, PropertyValue, RelationshipId};
41use crate::{PropsRef, ValueRef};
42
43#[derive(Debug, Clone, Error)]
54pub enum ConstraintViolation {
55 #[error("[22N77] property presence verification failed. {kind} must have the property `{property}`",
56 kind = entity_kind_label(*entity, label))]
57 MissingProperty {
58 constraint: String,
59 entity: StoredIndexEntity,
60 label: String,
61 property: String,
62 },
63 #[error("[22N77] property presence verification failed. {kind} must have the properties: {properties}",
64 kind = entity_kind_label(*entity, label),
65 properties = properties.join(", "))]
66 MissingPropertiesForKey {
67 constraint: String,
68 entity: StoredIndexEntity,
69 label: String,
70 properties: Vec<String>,
71 },
72 #[error("[22N78] property type verification failed. {kind} must have property `{property}` with value type {expected}",
73 kind = entity_kind_label(*entity, label))]
74 WrongPropertyType {
75 constraint: String,
76 entity: StoredIndexEntity,
77 label: String,
78 property: String,
79 expected: String,
80 },
81 #[error("[22N79] property uniqueness constraint violated. {kind} already has property `{property}` with the supplied value",
82 kind = entity_kind_label(*entity, label))]
83 UniquenessViolated {
84 constraint: String,
85 entity: StoredIndexEntity,
86 label: String,
87 property: String,
88 },
89}
90
91impl ConstraintViolation {
92 pub const fn gql_status(&self) -> &'static str {
93 match self {
94 ConstraintViolation::MissingProperty { .. }
95 | ConstraintViolation::MissingPropertiesForKey { .. } => "22N77",
96 ConstraintViolation::WrongPropertyType { .. } => "22N78",
97 ConstraintViolation::UniquenessViolated { .. } => "22N79",
98 }
99 }
100
101 pub fn constraint_name(&self) -> &str {
102 match self {
103 ConstraintViolation::MissingProperty { constraint, .. }
104 | ConstraintViolation::MissingPropertiesForKey { constraint, .. }
105 | ConstraintViolation::WrongPropertyType { constraint, .. }
106 | ConstraintViolation::UniquenessViolated { constraint, .. } => constraint,
107 }
108 }
109}
110
111fn entity_kind_label(entity: StoredIndexEntity, label: &str) -> String {
112 match entity {
113 StoredIndexEntity::Node => format!("NODE with label `{label}`"),
114 StoredIndexEntity::Relationship => format!("RELATIONSHIP with type `{label}`"),
115 }
116}
117
118impl InMemoryGraph {
119 pub(super) fn validate_existing_data_for_constraint(
127 &self,
128 def: &ConstraintDefinition,
129 ) -> Result<(), ConstraintViolation> {
130 match def.entity {
131 StoredIndexEntity::Node => self.validate_existing_nodes_for_constraint(def),
132 StoredIndexEntity::Relationship => self.validate_existing_rels_for_constraint(def),
133 }
134 }
135
136 fn validate_existing_nodes_for_constraint(
142 &self,
143 def: &ConstraintDefinition,
144 ) -> Result<(), ConstraintViolation> {
145 let Some(ids) = self.nodes_by_label.get(def.label.as_str()) else {
146 return Ok(());
147 };
148 let keys = constrained_keys(def);
149 let properties_of = |id| {
150 self.node_at(id)
151 .map(|node| constrained_properties(&keys, node.properties()))
152 };
153 let slots = self.nodes.len() as u64;
154 match single_key(def) {
155 Some(key) => {
156 let key_id = self.dicts.keys.id_of(key);
157 validate_scope(
158 def,
159 ids,
160 slots,
161 |id, buf| {
162 let Some(bytes) = self.nodes.get(id as usize) else {
163 return Ok(None);
164 };
165 let node = StoredNode::new(id, bytes, &self.dicts);
166 let value = key_id.and_then(|key_id| node.properties().get_id(key_id));
167 check_single(def, value, buf)
168 },
169 properties_of,
170 )
171 }
172 None => validate_scope(
173 def,
174 ids,
175 slots,
176 |id, buf| check_record(def, properties_of(id), buf),
177 properties_of,
178 ),
179 }
180 }
181
182 fn validate_existing_rels_for_constraint(
183 &self,
184 def: &ConstraintDefinition,
185 ) -> Result<(), ConstraintViolation> {
186 let Some(ids) = self.relationships_by_type.get(def.label.as_str()) else {
187 return Ok(());
188 };
189 let keys = constrained_keys(def);
190 let properties_of = |id| {
191 self.rel_at(id)
192 .map(|rel| constrained_properties(&keys, rel.properties()))
193 };
194 let slots = self.relationships.len() as u64;
195 match single_key(def) {
196 Some(key) => {
197 let key_id = self.dicts.keys.id_of(key);
198 validate_scope(
199 def,
200 ids,
201 slots,
202 |id, buf| {
203 let Some(bytes) = self.relationships.get(id as usize) else {
204 return Ok(None);
205 };
206 let rel = StoredRel::new(id, bytes, &self.dicts);
207 let value = key_id.and_then(|key_id| rel.properties().get_id(key_id));
208 check_single(def, value, buf)
209 },
210 properties_of,
211 )
212 }
213 None => validate_scope(
214 def,
215 ids,
216 slots,
217 |id, buf| check_record(def, properties_of(id), buf),
218 properties_of,
219 ),
220 }
221 }
222}
223
224fn single_key(def: &ConstraintDefinition) -> Option<&str> {
227 match (&def.kind, def.properties.as_slice()) {
228 (StoredConstraintKind::PropertyType(_), _) => None,
229 (_, [key]) => Some(key.as_str()),
230 _ => None,
231 }
232}
233
234type Checked = Result<Option<u64>, ConstraintViolation>;
238
239fn check_record(
241 def: &ConstraintDefinition,
242 properties: Option<Properties>,
243 buf: &mut String,
244) -> Checked {
245 let Some(properties) = properties else {
246 return Ok(None);
247 };
248 validate_record_against_constraint(def, &properties, true)?;
249 Ok(tuple_hash(def, &properties, buf))
250}
251
252fn check_single(
255 def: &ConstraintDefinition,
256 value: Option<ValueRef<'_>>,
257 buf: &mut String,
258) -> Checked {
259 let Some(value) = value else {
260 validate_record_against_constraint(def, &Properties::new(), true)?;
262 return Ok(None);
263 };
264 if !def.kind.requires_uniqueness() {
265 return Ok(None);
266 }
267 buf.clear();
268 append_value_ref_key(buf, value);
269 Ok(Some(
270 BuildHasherDefault::<KeyHasher>::default().hash_one(buf.as_str()),
271 ))
272}
273
274fn append_value_ref_key(out: &mut String, value: ValueRef<'_>) {
277 match value {
278 ValueRef::Null => out.push('N'),
279 ValueRef::Bool(b) => {
280 out.push('B');
281 out.push(if b { 'T' } else { 'F' });
282 }
283 ValueRef::Int(i) => {
284 out.push('I');
285 let _ = write!(out, "{i}");
286 }
287 ValueRef::Float(f) => {
288 out.push('F');
289 let _ = write!(out, "{f:?}");
290 }
291 ValueRef::String(s) => {
292 out.push('S');
293 append_len_prefixed_str(out, s);
294 }
295 ValueRef::Other(value) => append_property_value_key(out, &value.get()),
296 }
297}
298
299const SCAN_IDS: u64 = 1 << 16;
301
302struct ScannedRange {
304 rejected: Option<(u64, ConstraintViolation)>,
306 tuples: Vec<(u64, u64)>,
308}
309
310fn validate_scope(
317 def: &ConstraintDefinition,
318 ids: &super::id_bitmap::IdBitmap,
319 slots: u64,
320 check: impl Fn(u64, &mut String) -> Checked + Sync,
321 properties_of: impl Fn(u64) -> Option<Properties> + Sync,
322) -> Result<(), ConstraintViolation> {
323 let scan = |start: u64| {
324 let mut range = ScannedRange {
325 rejected: None,
326 tuples: Vec::new(),
327 };
328 let mut buf = String::new();
329 for id in ids.iter_from(start).take_while(|&id| id < start + SCAN_IDS) {
330 match check(id, &mut buf) {
331 Err(violation) => {
332 range.rejected = Some((id, violation));
334 break;
335 }
336 Ok(Some(hash)) => range.tuples.push((hash, id)),
337 Ok(None) => {}
338 }
339 }
340 range
341 };
342 let starts: Vec<u64> = (0..slots.div_ceil(SCAN_IDS))
343 .map(|i| i * SCAN_IDS)
344 .collect();
345 #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
346 let ranges: Vec<ScannedRange> = {
347 use rayon::prelude::*;
348 starts.into_par_iter().map(scan).collect()
349 };
350 #[cfg(not(all(feature = "parallel", not(target_arch = "wasm32"))))]
351 let ranges: Vec<ScannedRange> = starts.into_iter().map(scan).collect();
352
353 let mut rejected: Option<(u64, ConstraintViolation)> = None;
354 let mut tuples = Vec::new();
355 for range in ranges {
356 if let Some((id, violation)) = range.rejected {
357 if rejected.as_ref().is_none_or(|(first, _)| id < *first) {
358 rejected = Some((id, violation));
359 }
360 }
361 tuples.extend(range.tuples);
362 }
363 let repeat = first_repeat(def, tuples, &properties_of);
364 match (repeat, rejected) {
365 (Some(repeat), Some((first, _))) if repeat < first => Err(uniqueness_violation(def)),
367 (_, Some((_, violation))) => Err(violation),
368 (Some(_), None) => Err(uniqueness_violation(def)),
369 (None, None) => Ok(()),
370 }
371}
372
373fn tuple_hash(
378 def: &ConstraintDefinition,
379 properties: &Properties,
380 key: &mut String,
381) -> Option<u64> {
382 if !def.kind.requires_uniqueness()
383 || !def
384 .properties
385 .iter()
386 .all(|p| properties.contains_key(p.as_str()))
387 {
388 return None;
389 }
390 key.clear();
391 write_property_tuple_key(key, def, properties);
392 Some(BuildHasherDefault::<KeyHasher>::default().hash_one(key.as_str()))
393}
394
395fn first_repeat(
402 def: &ConstraintDefinition,
403 mut tuples: Vec<(u64, u64)>,
404 properties_of: impl Fn(u64) -> Option<Properties>,
405) -> Option<u64> {
406 #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
407 {
408 use rayon::slice::ParallelSliceMut;
409 tuples.par_sort_unstable();
410 }
411 #[cfg(not(all(feature = "parallel", not(target_arch = "wasm32"))))]
412 tuples.sort_unstable();
413
414 let key_of = |id: u64| {
415 let mut key = String::new();
416 if let Some(properties) = properties_of(id) {
417 write_property_tuple_key(&mut key, def, &properties);
418 }
419 key
420 };
421 let mut first: Option<u64> = None;
422 for run in tuples
423 .chunk_by(|a, b| a.0 == b.0)
424 .filter(|run| run.len() > 1)
425 {
426 let mut keys: Vec<String> = Vec::with_capacity(run.len());
429 for &(_, id) in run {
430 let key = key_of(id);
431 if keys.contains(&key) {
432 first = Some(first.map_or(id, |first| first.min(id)));
433 break;
434 }
435 keys.push(key);
436 }
437 }
438 first
439}
440
441fn constrained_keys(def: &ConstraintDefinition) -> Vec<Arc<str>> {
443 def.properties
444 .iter()
445 .map(|key| crate::intern(key))
446 .collect()
447}
448
449fn constrained_properties(keys: &[Arc<str>], stored: PropsRef<'_>) -> Properties {
452 keys.iter()
453 .filter_map(|key| Some((Arc::clone(key), stored.get(key)?.to_owned())))
454 .collect()
455}
456
457fn validate_record_against_constraint(
458 def: &ConstraintDefinition,
459 properties: &Properties,
460 check_existence: bool,
461) -> Result<(), ConstraintViolation> {
462 if check_existence && def.kind.requires_existence() {
464 let missing: Vec<String> = def
465 .properties
466 .iter()
467 .filter(|p| !properties.contains_key(p.as_str()))
468 .cloned()
469 .collect();
470 if !missing.is_empty() {
471 return Err(if def.properties.len() == 1 {
472 ConstraintViolation::MissingProperty {
473 constraint: def.name.clone(),
474 entity: def.entity,
475 label: def.label.clone(),
476 property: def.properties[0].clone(),
477 }
478 } else {
479 ConstraintViolation::MissingPropertiesForKey {
480 constraint: def.name.clone(),
481 entity: def.entity,
482 label: def.label.clone(),
483 properties: def.properties.clone(),
484 }
485 });
486 }
487 }
488
489 if let StoredConstraintKind::PropertyType(target) = &def.kind {
491 let key = &def.properties[0];
492 if let Some(value) = properties.get(key.as_str()) {
493 if !value_matches_property_type(value, target) {
494 return Err(ConstraintViolation::WrongPropertyType {
495 constraint: def.name.clone(),
496 entity: def.entity,
497 label: def.label.clone(),
498 property: key.clone(),
499 expected: target.to_string(),
500 });
501 }
502 }
503 }
504
505 Ok(())
506}
507
508fn write_property_tuple_key(out: &mut String, def: &ConstraintDefinition, properties: &Properties) {
512 for (i, key) in def.properties.iter().enumerate() {
513 if i > 0 {
514 out.push('\u{1f}');
515 }
516 if let Some(value) = properties.get(key.as_str()) {
517 append_property_value_key(out, value);
518 }
519 }
520}
521
522fn append_property_value_key(out: &mut String, value: &PropertyValue) {
523 match value {
524 PropertyValue::Null => out.push('N'),
525 PropertyValue::Bool(b) => {
526 out.push('B');
527 out.push(if *b { 'T' } else { 'F' });
528 }
529 PropertyValue::Int(i) => {
530 out.push('I');
531 out.push_str(&i.to_string());
532 }
533 PropertyValue::Float(f) => {
534 out.push('F');
535 out.push_str(&format!("{f:?}"));
536 }
537 PropertyValue::String(s) => {
538 out.push('S');
539 append_len_prefixed_str(out, s);
540 }
541 PropertyValue::Date(d) => {
542 out.push_str("D:");
543 append_len_prefixed_str(out, &format!("{d:?}"));
544 }
545 PropertyValue::Time(t) => {
546 out.push_str("T:");
547 append_len_prefixed_str(out, &format!("{t:?}"));
548 }
549 PropertyValue::LocalTime(t) => {
550 out.push_str("LT:");
551 append_len_prefixed_str(out, &format!("{t:?}"));
552 }
553 PropertyValue::DateTime(dt) => {
554 out.push_str("DT:");
555 append_len_prefixed_str(out, &format!("{dt:?}"));
556 }
557 PropertyValue::LocalDateTime(dt) => {
558 out.push_str("LDT:");
559 append_len_prefixed_str(out, &format!("{dt:?}"));
560 }
561 PropertyValue::Duration(d) => {
562 out.push_str("DUR:");
563 append_len_prefixed_str(out, &format!("{d:?}"));
564 }
565 PropertyValue::Point(p) => {
566 out.push_str("P:");
567 append_len_prefixed_str(out, &format!("{p:?}"));
568 }
569 PropertyValue::Vector(v) => {
570 out.push_str("V:");
571 append_len_prefixed_str(out, &v.to_key_string());
572 }
573 PropertyValue::List(items) => {
574 out.push('L');
575 append_len(out, items.len());
576 for item in items {
577 append_property_value_key(out, item);
578 }
579 }
580 PropertyValue::Map(entries) => {
581 out.push('M');
582 append_len(out, entries.len());
583 for (k, v) in entries {
584 append_len_prefixed_str(out, k);
585 append_property_value_key(out, v);
586 }
587 }
588 PropertyValue::Binary(b) => {
589 out.push_str("BIN:");
590 append_len(out, b.len());
591 for segment in b.chunks() {
592 for byte in segment {
593 let _ = write!(out, "{byte:02x}");
594 }
595 }
596 }
597 }
598}
599
600fn append_len(out: &mut String, len: usize) {
601 out.push_str(&len.to_string());
602 out.push(':');
603}
604
605fn append_len_prefixed_str(out: &mut String, value: &str) {
606 append_len(out, value.len());
607 out.push_str(value);
608}
609
610pub fn value_matches_property_type(value: &PropertyValue, target: &StoredPropertyType) -> bool {
613 target
614 .alternatives
615 .iter()
616 .any(|term| value_matches_term(value, term))
617}
618
619fn value_matches_term(value: &PropertyValue, term: &StoredPropertyTypeTerm) -> bool {
620 match term {
621 StoredPropertyTypeTerm::Scalar(scalar) => value_matches_scalar(value, *scalar),
622 StoredPropertyTypeTerm::List { inner, not_null } => match value {
623 PropertyValue::List(items) => items.iter().all(|item| {
624 if matches!(item, PropertyValue::Null) {
625 !*not_null
626 } else {
627 value_matches_term(item, inner)
628 }
629 }),
630 _ => false,
631 },
632 StoredPropertyTypeTerm::Vector { coord, dimension } => match value {
633 PropertyValue::Vector(v) => vector_matches(v, *coord, *dimension),
634 _ => false,
635 },
636 }
637}
638
639fn value_matches_scalar(value: &PropertyValue, scalar: StoredScalarType) -> bool {
640 match (value, scalar) {
641 (PropertyValue::Bool(_), StoredScalarType::Boolean) => true,
642 (PropertyValue::String(_), StoredScalarType::String) => true,
643 (PropertyValue::Int(_), StoredScalarType::Integer) => true,
644 (PropertyValue::Float(_), StoredScalarType::Float) => true,
645 (PropertyValue::Date(_), StoredScalarType::Date) => true,
646 (PropertyValue::Time(_), StoredScalarType::ZonedTime) => true,
647 (PropertyValue::LocalTime(_), StoredScalarType::LocalTime) => true,
648 (PropertyValue::DateTime(_), StoredScalarType::ZonedDateTime) => true,
649 (PropertyValue::LocalDateTime(_), StoredScalarType::LocalDateTime) => true,
650 (PropertyValue::Duration(_), StoredScalarType::Duration) => true,
651 (PropertyValue::Point(_), StoredScalarType::Point) => true,
652 _ => false,
656 }
657}
658
659fn vector_matches(
660 vector: &crate::types::LoraVector,
661 coord: StoredVectorCoordType,
662 dimension: u32,
663) -> bool {
664 if vector.dimension != dimension as usize {
665 return false;
666 }
667 use crate::types::VectorValues;
668 use StoredVectorCoordType::*;
669 matches!(
670 (&vector.values, coord),
671 (VectorValues::Float64(_), Float64)
672 | (VectorValues::Float32(_), Float32)
673 | (VectorValues::Integer64(_), Int64)
674 | (VectorValues::Integer32(_), Int32)
675 | (VectorValues::Integer16(_), Int16)
676 | (VectorValues::Integer8(_), Int8)
677 )
678}
679
680#[derive(Clone, Copy)]
681enum NodeLabelMatcher<'a> {
682 AnyOf(&'a [String]),
683 Names(&'a [crate::Name]),
684 One(&'a str),
685}
686
687impl NodeLabelMatcher<'_> {
688 fn contains(self, label: &str) -> bool {
689 match self {
690 NodeLabelMatcher::AnyOf(labels) => labels.iter().any(|l| l == label),
691 NodeLabelMatcher::Names(labels) => labels.iter().any(|l| l == label),
692 NodeLabelMatcher::One(candidate) => candidate == label,
693 }
694 }
695}
696
697#[derive(Clone, Copy)]
698enum ConstraintRecord<'a> {
699 Node {
700 labels: NodeLabelMatcher<'a>,
701 properties: &'a Properties,
702 skip: Option<NodeId>,
703 },
704 Relationship {
705 rel_type: &'a str,
706 properties: &'a Properties,
707 skip: Option<RelationshipId>,
708 },
709}
710
711impl<'a> ConstraintRecord<'a> {
712 fn applies_to(self, def: &ConstraintDefinition) -> bool {
713 match self {
714 ConstraintRecord::Node { labels, .. } => {
715 def.entity == StoredIndexEntity::Node && labels.contains(&def.label)
716 }
717 ConstraintRecord::Relationship { rel_type, .. } => {
718 def.entity == StoredIndexEntity::Relationship && def.label == rel_type
719 }
720 }
721 }
722
723 fn properties(self) -> &'a Properties {
724 match self {
725 ConstraintRecord::Node { properties, .. }
726 | ConstraintRecord::Relationship { properties, .. } => properties,
727 }
728 }
729
730 fn has_uniqueness_conflict(
731 self,
732 graph: &InMemoryGraph,
733 def: &ConstraintDefinition,
734 tuple: &[PropertyValue],
735 ) -> bool {
736 match self {
737 ConstraintRecord::Node { skip, .. } => {
738 any_other_node_with_tuple(graph, &def.label, &def.properties, tuple, skip)
739 }
740 ConstraintRecord::Relationship { skip, .. } => {
741 any_other_rel_with_tuple(graph, &def.label, &def.properties, tuple, skip)
742 }
743 }
744 }
745}
746
747fn check_record_constraints(
748 catalog: &ConstraintCatalog,
749 graph: &InMemoryGraph,
750 record: ConstraintRecord<'_>,
751) -> Result<(), ConstraintViolation> {
752 check_record_constraints_with(catalog, graph, record, true)
753}
754
755fn check_record_constraints_with(
756 catalog: &ConstraintCatalog,
757 graph: &InMemoryGraph,
758 record: ConstraintRecord<'_>,
759 check_existence: bool,
760) -> Result<(), ConstraintViolation> {
761 for def in catalog.iter() {
762 if !record.applies_to(def) {
763 continue;
764 }
765
766 let properties = record.properties();
767 validate_record_against_constraint(def, properties, check_existence)?;
768
769 if def.kind.requires_uniqueness() {
770 if let Some(tuple) = constrained_tuple(def, properties) {
771 if record.has_uniqueness_conflict(graph, def, &tuple) {
772 return Err(uniqueness_violation(def));
773 }
774 }
775 }
776 }
777 Ok(())
778}
779
780pub(crate) fn check_node_create(
785 catalog: &ConstraintCatalog,
786 graph: &InMemoryGraph,
787 labels: &[String],
788 properties: &Properties,
789) -> Result<(), ConstraintViolation> {
790 check_record_constraints(
791 catalog,
792 graph,
793 ConstraintRecord::Node {
794 labels: NodeLabelMatcher::AnyOf(labels),
795 properties,
796 skip: None,
797 },
798 )
799}
800
801pub(crate) fn check_relationship_create(
802 catalog: &ConstraintCatalog,
803 graph: &InMemoryGraph,
804 rel_type: &str,
805 properties: &Properties,
806) -> Result<(), ConstraintViolation> {
807 check_record_constraints(
808 catalog,
809 graph,
810 ConstraintRecord::Relationship {
811 rel_type,
812 properties,
813 skip: None,
814 },
815 )
816}
817
818pub(crate) fn check_node_create_deferred(
823 catalog: &ConstraintCatalog,
824 graph: &InMemoryGraph,
825 labels: &[String],
826 properties: &Properties,
827) -> Result<(), ConstraintViolation> {
828 check_record_constraints_with(
829 catalog,
830 graph,
831 ConstraintRecord::Node {
832 labels: NodeLabelMatcher::AnyOf(labels),
833 properties,
834 skip: None,
835 },
836 false,
837 )
838}
839
840pub(crate) fn check_relationship_create_deferred(
842 catalog: &ConstraintCatalog,
843 graph: &InMemoryGraph,
844 rel_type: &str,
845 properties: &Properties,
846) -> Result<(), ConstraintViolation> {
847 check_record_constraints_with(
848 catalog,
849 graph,
850 ConstraintRecord::Relationship {
851 rel_type,
852 properties,
853 skip: None,
854 },
855 false,
856 )
857}
858
859pub(crate) fn check_node_existence(
862 catalog: &ConstraintCatalog,
863 graph: &InMemoryGraph,
864 node_id: NodeId,
865) -> Result<(), ConstraintViolation> {
866 let Some(node) = graph.node_at(node_id) else {
867 return Ok(());
868 };
869 for def in catalog.iter() {
870 if def.entity != StoredIndexEntity::Node
871 || !def.kind.requires_existence()
872 || !node.labels().strs().any(|l| l == def.label)
873 {
874 continue;
875 }
876 if let Some(missing) = def
877 .properties
878 .iter()
879 .find(|p| !node.properties().contains_key(p.as_str()))
880 {
881 return Err(missing_property_violation(def, missing));
882 }
883 }
884 Ok(())
885}
886
887pub(crate) fn check_relationship_existence(
889 catalog: &ConstraintCatalog,
890 graph: &InMemoryGraph,
891 rel_id: RelationshipId,
892) -> Result<(), ConstraintViolation> {
893 let Some(rel) = graph.rel_at(rel_id) else {
894 return Ok(());
895 };
896 for def in catalog.iter() {
897 if def.entity != StoredIndexEntity::Relationship
898 || !def.kind.requires_existence()
899 || rel.rel_type() != def.label
900 {
901 continue;
902 }
903 if let Some(missing) = def
904 .properties
905 .iter()
906 .find(|p| !rel.properties().contains_key(p.as_str()))
907 {
908 return Err(missing_property_violation(def, missing));
909 }
910 }
911 Ok(())
912}
913
914fn any_other_node_with_tuple(
923 graph: &InMemoryGraph,
924 label: &str,
925 keys: &[String],
926 target: &[PropertyValue],
927 skip: Option<NodeId>,
928) -> bool {
929 let tuple_matches = |node: crate::NodeRef<'_>| {
930 node.has_label(label)
931 && keys.iter().enumerate().all(|(idx, key)| {
932 node.properties()
933 .get(key.as_str())
934 .is_some_and(|v| v == target[idx])
935 })
936 };
937 let candidates = match (keys.first(), target.first()) {
938 (Some(key), Some(value)) => graph.indexed_node_ids(label, key, value),
939 _ => None,
940 };
941 let candidates =
942 candidates.unwrap_or_else(|| crate::GraphStorage::node_ids_by_label(graph, label));
943 candidates
944 .into_iter()
945 .filter(|id| Some(*id) != skip)
946 .any(|id| graph.node_at(id).is_some_and(tuple_matches))
947}
948
949fn any_other_rel_with_tuple(
951 graph: &InMemoryGraph,
952 rel_type: &str,
953 keys: &[String],
954 target: &[PropertyValue],
955 skip: Option<RelationshipId>,
956) -> bool {
957 let tuple_matches = |rel: crate::RelRef<'_>| {
958 rel.rel_type() == rel_type
959 && keys.iter().enumerate().all(|(idx, key)| {
960 rel.properties()
961 .get(key.as_str())
962 .is_some_and(|v| v == target[idx])
963 })
964 };
965 let candidates = match (keys.first(), target.first()) {
966 (Some(key), Some(value)) => graph.indexed_rel_ids(rel_type, key, value),
967 _ => None,
968 };
969 let candidates =
970 candidates.unwrap_or_else(|| crate::GraphStorage::rel_ids_by_type(graph, rel_type));
971 candidates
972 .into_iter()
973 .filter(|id| Some(*id) != skip)
974 .any(|id| graph.rel_at(id).is_some_and(tuple_matches))
975}
976
977fn render_constraint_property_label(def: &ConstraintDefinition) -> String {
978 if def.properties.len() == 1 {
979 def.properties[0].clone()
980 } else {
981 def.properties.join(", ")
982 }
983}
984
985fn uniqueness_violation(def: &ConstraintDefinition) -> ConstraintViolation {
986 ConstraintViolation::UniquenessViolated {
987 constraint: def.name.clone(),
988 entity: def.entity,
989 label: def.label.clone(),
990 property: render_constraint_property_label(def),
991 }
992}
993
994fn missing_property_violation(def: &ConstraintDefinition, property: &str) -> ConstraintViolation {
995 if def.properties.len() == 1 {
996 ConstraintViolation::MissingProperty {
997 constraint: def.name.clone(),
998 entity: def.entity,
999 label: def.label.clone(),
1000 property: property.to_string(),
1001 }
1002 } else {
1003 ConstraintViolation::MissingPropertiesForKey {
1004 constraint: def.name.clone(),
1005 entity: def.entity,
1006 label: def.label.clone(),
1007 properties: def.properties.clone(),
1008 }
1009 }
1010}
1011
1012fn constrained_tuple(
1013 def: &ConstraintDefinition,
1014 properties: &Properties,
1015) -> Option<Vec<PropertyValue>> {
1016 def.properties
1017 .iter()
1018 .map(|p| properties.get(p.as_str()).cloned())
1019 .collect()
1020}
1021
1022fn constrained_tuple_after_set(
1023 def: &ConstraintDefinition,
1024 properties: crate::PropsRef<'_>,
1025 key: &str,
1026 value: &PropertyValue,
1027) -> Option<Vec<PropertyValue>> {
1028 def.properties
1030 .iter()
1031 .map(|prop| {
1032 if prop == key {
1033 Some(value.clone())
1034 } else {
1035 properties.get(prop.as_str()).map(|v| v.to_owned())
1036 }
1037 })
1038 .collect()
1039}
1040
1041pub(crate) fn check_node_set_property(
1045 catalog: &ConstraintCatalog,
1046 graph: &InMemoryGraph,
1047 node_id: NodeId,
1048 key: &str,
1049 value: &PropertyValue,
1050) -> Result<(), ConstraintViolation> {
1051 let node = match graph.node_at(node_id) {
1052 Some(n) => n,
1053 None => return Ok(()), };
1055 for def in catalog.iter() {
1056 if def.entity != StoredIndexEntity::Node {
1057 continue;
1058 }
1059 if !node.labels().strs().any(|l| l == def.label) {
1060 continue;
1061 }
1062 if !def.properties.iter().any(|p| p == key) {
1063 continue;
1064 }
1065 if let StoredConstraintKind::PropertyType(target) = &def.kind {
1067 if !value_matches_property_type(value, target) {
1068 return Err(ConstraintViolation::WrongPropertyType {
1069 constraint: def.name.clone(),
1070 entity: def.entity,
1071 label: def.label.clone(),
1072 property: key.to_string(),
1073 expected: target.to_string(),
1074 });
1075 }
1076 }
1077 if def.kind.requires_uniqueness() {
1080 if let Some(tuple) = constrained_tuple_after_set(def, node.properties(), key, value) {
1081 if any_other_node_with_tuple(
1082 graph,
1083 &def.label,
1084 &def.properties,
1085 &tuple,
1086 Some(node_id),
1087 ) {
1088 return Err(uniqueness_violation(def));
1089 }
1090 }
1091 }
1092 }
1093 Ok(())
1094}
1095
1096pub(crate) fn check_node_remove_property(
1099 catalog: &ConstraintCatalog,
1100 graph: &InMemoryGraph,
1101 node_id: NodeId,
1102 key: &str,
1103) -> Result<(), ConstraintViolation> {
1104 let node = match graph.node_at(node_id) {
1105 Some(n) => n,
1106 None => return Ok(()),
1107 };
1108 for def in catalog.iter() {
1109 if def.entity != StoredIndexEntity::Node {
1110 continue;
1111 }
1112 if !node.labels().strs().any(|l| l == def.label) {
1113 continue;
1114 }
1115 if !def.kind.requires_existence() {
1116 continue;
1117 }
1118 if !def.properties.iter().any(|p| p == key) {
1119 continue;
1120 }
1121 return Err(missing_property_violation(def, key));
1122 }
1123 Ok(())
1124}
1125
1126pub(crate) fn check_node_replace_properties(
1130 catalog: &ConstraintCatalog,
1131 graph: &InMemoryGraph,
1132 node_id: NodeId,
1133 properties: &Properties,
1134) -> Result<(), ConstraintViolation> {
1135 check_node_replace_properties_with(catalog, graph, node_id, properties, true)
1136}
1137
1138pub(crate) fn check_node_replace_properties_deferred(
1142 catalog: &ConstraintCatalog,
1143 graph: &InMemoryGraph,
1144 node_id: NodeId,
1145 properties: &Properties,
1146) -> Result<(), ConstraintViolation> {
1147 check_node_replace_properties_with(catalog, graph, node_id, properties, false)
1148}
1149
1150fn check_node_replace_properties_with(
1151 catalog: &ConstraintCatalog,
1152 graph: &InMemoryGraph,
1153 node_id: NodeId,
1154 properties: &Properties,
1155 check_existence: bool,
1156) -> Result<(), ConstraintViolation> {
1157 let labels = match graph.node_at(node_id) {
1158 Some(node) => node.labels().to_owned(),
1159 None => return Ok(()),
1160 };
1161 check_record_constraints_with(
1162 catalog,
1163 graph,
1164 ConstraintRecord::Node {
1165 labels: NodeLabelMatcher::Names(&labels),
1166 properties,
1167 skip: Some(node_id),
1168 },
1169 check_existence,
1170 )
1171}
1172
1173pub(crate) fn check_relationship_set_property(
1174 catalog: &ConstraintCatalog,
1175 graph: &InMemoryGraph,
1176 rel_id: RelationshipId,
1177 key: &str,
1178 value: &PropertyValue,
1179) -> Result<(), ConstraintViolation> {
1180 let rel = match graph.rel_at(rel_id) {
1181 Some(r) => r,
1182 None => return Ok(()),
1183 };
1184 for def in catalog.iter() {
1185 if def.entity != StoredIndexEntity::Relationship {
1186 continue;
1187 }
1188 if def.label != rel.rel_type() {
1189 continue;
1190 }
1191 if !def.properties.iter().any(|p| p == key) {
1192 continue;
1193 }
1194 if let StoredConstraintKind::PropertyType(target) = &def.kind {
1195 if !value_matches_property_type(value, target) {
1196 return Err(ConstraintViolation::WrongPropertyType {
1197 constraint: def.name.clone(),
1198 entity: def.entity,
1199 label: def.label.clone(),
1200 property: key.to_string(),
1201 expected: target.to_string(),
1202 });
1203 }
1204 }
1205 if def.kind.requires_uniqueness() {
1206 if let Some(tuple) = constrained_tuple_after_set(def, rel.properties(), key, value) {
1207 if any_other_rel_with_tuple(
1208 graph,
1209 &def.label,
1210 &def.properties,
1211 &tuple,
1212 Some(rel_id),
1213 ) {
1214 return Err(uniqueness_violation(def));
1215 }
1216 }
1217 }
1218 }
1219 Ok(())
1220}
1221
1222pub(crate) fn check_relationship_remove_property(
1223 catalog: &ConstraintCatalog,
1224 graph: &InMemoryGraph,
1225 rel_id: RelationshipId,
1226 key: &str,
1227) -> Result<(), ConstraintViolation> {
1228 let rel = match graph.rel_at(rel_id) {
1229 Some(r) => r,
1230 None => return Ok(()),
1231 };
1232 for def in catalog.iter() {
1233 if def.entity != StoredIndexEntity::Relationship {
1234 continue;
1235 }
1236 if def.label != rel.rel_type() {
1237 continue;
1238 }
1239 if !def.kind.requires_existence() {
1240 continue;
1241 }
1242 if !def.properties.iter().any(|p| p == key) {
1243 continue;
1244 }
1245 return Err(missing_property_violation(def, key));
1246 }
1247 Ok(())
1248}
1249
1250pub(crate) fn check_relationship_replace_properties(
1254 catalog: &ConstraintCatalog,
1255 graph: &InMemoryGraph,
1256 rel_id: RelationshipId,
1257 properties: &Properties,
1258) -> Result<(), ConstraintViolation> {
1259 check_relationship_replace_properties_with(catalog, graph, rel_id, properties, true)
1260}
1261
1262pub(crate) fn check_relationship_replace_properties_deferred(
1264 catalog: &ConstraintCatalog,
1265 graph: &InMemoryGraph,
1266 rel_id: RelationshipId,
1267 properties: &Properties,
1268) -> Result<(), ConstraintViolation> {
1269 check_relationship_replace_properties_with(catalog, graph, rel_id, properties, false)
1270}
1271
1272fn check_relationship_replace_properties_with(
1273 catalog: &ConstraintCatalog,
1274 graph: &InMemoryGraph,
1275 rel_id: RelationshipId,
1276 properties: &Properties,
1277 check_existence: bool,
1278) -> Result<(), ConstraintViolation> {
1279 let rel = match graph.rel_at(rel_id) {
1280 Some(r) => r,
1281 None => return Ok(()),
1282 };
1283 check_record_constraints_with(
1284 catalog,
1285 graph,
1286 ConstraintRecord::Relationship {
1287 rel_type: rel.rel_type(),
1288 properties,
1289 skip: Some(rel_id),
1290 },
1291 check_existence,
1292 )
1293}
1294
1295pub(crate) fn check_node_add_label(
1300 catalog: &ConstraintCatalog,
1301 graph: &InMemoryGraph,
1302 node_id: NodeId,
1303 label: &str,
1304) -> Result<(), ConstraintViolation> {
1305 let node = match graph.node_at(node_id) {
1306 Some(n) => n,
1307 None => return Ok(()),
1308 };
1309 check_record_constraints(
1310 catalog,
1311 graph,
1312 ConstraintRecord::Node {
1313 labels: NodeLabelMatcher::One(label),
1314 properties: &node.properties().to_owned(),
1315 skip: Some(node_id),
1316 },
1317 )
1318}
1319
1320pub(crate) fn check_node_add_label_deferred(
1324 catalog: &ConstraintCatalog,
1325 graph: &InMemoryGraph,
1326 node_id: NodeId,
1327 label: &str,
1328) -> Result<(), ConstraintViolation> {
1329 let Some(node) = graph.node_at(node_id) else {
1330 return Ok(());
1331 };
1332 check_record_constraints_with(
1333 catalog,
1334 graph,
1335 ConstraintRecord::Node {
1336 labels: NodeLabelMatcher::One(label),
1337 properties: &node.properties().to_owned(),
1338 skip: Some(node_id),
1339 },
1340 false,
1341 )
1342}
1343
1344#[cfg(test)]
1345mod tests {
1346 use super::*;
1347 use crate::intern;
1348
1349 fn unique_on(property: &str) -> ConstraintDefinition {
1350 ConstraintDefinition {
1351 backing: Default::default(),
1352 name: "u".into(),
1353 kind: StoredConstraintKind::Unique,
1354 entity: StoredIndexEntity::Node,
1355 label: "L".into(),
1356 properties: vec![property.into()],
1357 owned_index: Some("u".into()),
1358 }
1359 }
1360
1361 fn with(property: &str, value: PropertyValue) -> Properties {
1362 let mut properties = Properties::new();
1363 properties.insert(intern(property), value);
1364 properties
1365 }
1366
1367 fn repeat_among_colliding(values: &[PropertyValue]) -> Option<u64> {
1370 let def = unique_on("k");
1371 let records: Vec<Properties> = values.iter().map(|v| with("k", v.clone())).collect();
1372 let tuples = (0..records.len() as u64).map(|id| (7, id)).collect();
1373 first_repeat(&def, tuples, |id| records.get(id as usize).cloned())
1374 }
1375
1376 #[test]
1377 fn a_shared_hash_is_not_a_duplicate() {
1378 let values = [
1380 PropertyValue::Int(1),
1381 PropertyValue::Float(1.0),
1382 PropertyValue::String("1".into()),
1383 PropertyValue::Int(2),
1384 ];
1385 assert_eq!(repeat_among_colliding(&values), None);
1386 }
1387
1388 #[test]
1389 fn an_equal_tuple_behind_a_collision_is_found() {
1390 let values = [
1391 PropertyValue::Int(1),
1392 PropertyValue::Int(2),
1393 PropertyValue::Int(3),
1394 PropertyValue::Int(2),
1395 ];
1396 assert_eq!(repeat_among_colliding(&values), Some(3));
1397 }
1398
1399 #[test]
1400 fn the_earliest_repeat_is_reported() {
1401 let def = unique_on("k");
1402 let records: Vec<Properties> = [5, 6, 6, 5]
1403 .iter()
1404 .map(|v| with("k", PropertyValue::Int(*v)))
1405 .collect();
1406 let mut key = String::new();
1407 let tuples = records
1408 .iter()
1409 .enumerate()
1410 .map(|(id, properties)| (tuple_hash(&def, properties, &mut key).unwrap(), id as u64))
1411 .collect();
1412 let repeat = first_repeat(&def, tuples, |id| records.get(id as usize).cloned());
1413 assert_eq!(repeat, Some(2));
1414 }
1415
1416 #[test]
1417 fn records_without_the_tuple_are_skipped() {
1418 let def = unique_on("k");
1419 let mut key = String::new();
1420 assert_eq!(
1421 tuple_hash(&def, &with("other", PropertyValue::Int(1)), &mut key),
1422 None
1423 );
1424 }
1425
1426 #[test]
1427 fn a_value_checked_in_place_gets_the_copied_values_verdict_and_key() {
1428 let values = [
1429 PropertyValue::Null,
1430 PropertyValue::Bool(true),
1431 PropertyValue::Int(-42),
1432 PropertyValue::Int(i64::MIN),
1433 PropertyValue::Float(1.0),
1434 PropertyValue::Float(-0.0),
1435 PropertyValue::Float(f64::NAN),
1436 PropertyValue::String(String::new()),
1437 PropertyValue::String("héllo".into()),
1438 PropertyValue::List(vec![
1439 PropertyValue::Int(1),
1440 PropertyValue::String("x".into()),
1441 ]),
1442 PropertyValue::Date(crate::LoraDate::new(2024, 2, 29).unwrap()),
1443 ];
1444 for kind in [
1445 StoredConstraintKind::Unique,
1446 StoredConstraintKind::NodeKey,
1447 StoredConstraintKind::Existence,
1448 ] {
1449 let def = ConstraintDefinition {
1450 kind: kind.clone(),
1451 ..unique_on("k")
1452 };
1453 for value in &values {
1454 let mut copied = String::new();
1455 let mut in_place = String::new();
1456 let properties = with("k", value.clone());
1457 let slow = check_record(&def, Some(properties), &mut copied).unwrap();
1458 let fast = check_single(&def, Some(ValueRef::from(value)), &mut in_place).unwrap();
1459 assert_eq!(fast, slow, "{kind:?} {value:?}");
1460 }
1461 let mut buf = String::new();
1462 let slow = check_record(&def, Some(Properties::new()), &mut buf);
1463 let fast = check_single(&def, None, &mut buf);
1464 assert_eq!(
1465 fast.map_err(|e| e.gql_status()),
1466 slow.map_err(|e| e.gql_status()),
1467 "{kind:?} missing"
1468 );
1469 }
1470 }
1471}