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