Skip to main content

lora_store/memory/
constraint_enforce.rs

1//! Constraint enforcement primitives.
2//!
3//! The [`ConstraintCatalog`](super::ConstraintCatalog) records *what*
4//! constraints exist; this module knows *how* to check whether a piece
5//! of data — either an existing record or a proposed mutation —
6//! complies.
7//!
8//! Why it lives here: both the DDL pre-create scan ("does the current
9//! graph already violate the constraint we're about to register?") and
10//! the runtime mutation pre-check ("would this write violate any
11//! installed constraint?") need the same value-shape inspection. The
12//! executor calls the runtime hooks from `lora-executor`; the DDL path
13//! calls the scan from [`super::graph::InMemoryGraph::register_constraint`].
14//!
15//! Performance: the storage impl checks an atomic active-constraint
16//! counter before calling into this module, so workloads that never call
17//! `CREATE CONSTRAINT` skip the catalog lock entirely. Once constraints
18//! exist, the catalog read is held across validation of a single mutation;
19//! the writer mutex on the database serialises it against concurrent DDL.
20
21// Several helpers below are pub-within-store and used by the runtime
22// pre-check the executor will install in the next iteration; suppress
23// dead-code while that wiring is still pending.
24#![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/// Why a mutation was rejected by a constraint check. The codes match
44/// the GQLSTATUS-style shapes:
45///
46/// * 22N77 — property presence verification failed.
47/// * 22N78 — property type verification failed.
48/// * 22N79 — property uniqueness constraint violated.
49/// * 22N80 — index entry conflict (duplicate row found by the backing
50///   index during a CREATE CONSTRAINT scan).
51/// * 50N11 — generic "constraint creation failed" wrapper used at DDL
52///   time to surface the underlying 22N7x error.
53#[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    /// Check whether the *current* graph contains data that would
120    /// violate `def` if it were registered. Called from
121    /// `register_constraint` just before the catalog write commits.
122    ///
123    /// Returns the first violation we find; we don't enumerate all
124    /// failures because a single rejection is enough to refuse the
125    /// CREATE CONSTRAINT.
126    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    /// Scans the label's nodes only, and decodes only the constrained
137    /// properties of each: copying every property of every node out of
138    /// its stored form was most of the time a restart spent on a
139    /// constraint. A constraint on one property, other than a type
140    /// constraint, reads the value in place without copying it.
141    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
224/// The one property a constraint other than a type constraint covers,
225/// when it covers one: its records are checked without copying values.
226fn 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
234/// What a scan learns from one record: `Err` if it violates the
235/// existence or type checks, else its tuple hash when uniqueness applies
236/// to it (built in `buf`).
237type Checked = Result<Option<u64>, ConstraintViolation>;
238
239/// [`Checked`] for a record's constrained properties (`None`: no record).
240fn 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
252/// [`check_record`] for a [`single_key`] constraint, from the stored value
253/// read in place: the same verdict and the same tuple key.
254fn check_single(
255    def: &ConstraintDefinition,
256    value: Option<ValueRef<'_>>,
257    buf: &mut String,
258) -> Checked {
259    let Some(value) = value else {
260        // The existence check's violation, as for a record without it.
261        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
274/// [`append_property_value_key`] for a value read in place: a scalar is
275/// written without being copied out first.
276fn 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
299/// Ids a scan of [`validate_scope`] takes per task.
300const SCAN_IDS: u64 = 1 << 16;
301
302/// What one task of [`validate_scope`] found in its ids.
303struct ScannedRange {
304    /// The first record the existence or type checks reject.
305    rejected: Option<(u64, ConstraintViolation)>,
306    /// `(tuple hash, id)` of each record the uniqueness check applies to.
307    tuples: Vec<(u64, u64)>,
308}
309
310/// Check the records `ids` lists (all below `slots`) against `def`, on
311/// every core when the build allows, and report the violation a scan in
312/// id order would meet first: per record, the existence and type checks,
313/// then uniqueness against the records before it. `check` checks one
314/// record ([`Checked`]); `properties_of` reads a record's constrained
315/// properties, to confirm records whose tuples hash alike.
316fn 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                    // Nothing after it in this range can be met first.
333                    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        // At one record the existence and type checks come first.
366        (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
373/// The hash of `properties`' tuple key (built in `key`), or `None` when
374/// uniqueness does not apply to the record: plain uniqueness only applies
375/// when every constrained property is present, and a key constraint's
376/// existence check has already failed a record without them.
377fn 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
395/// The smallest id whose tuple an earlier record already has, given each
396/// record's `(tuple hash, id)`. Holds hashes, not keys: a 1M-node scan
397/// that kept the keys held about three times the backing index it was
398/// about to build. Records with one hash are confirmed by rebuilding
399/// their keys from `properties_of`, so a collision never reads as a
400/// duplicate.
401fn 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        // Ids ascend within a run: the first key equal to an earlier one
427        // is the run's earliest repeat.
428        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
441/// The constraint's property keys, interned once for a scan.
442fn constrained_keys(def: &ConstraintDefinition) -> Vec<Arc<str>> {
443    def.properties
444        .iter()
445        .map(|key| crate::intern(key))
446        .collect()
447}
448
449/// The constrained properties a record holds: all a constraint check
450/// reads.
451fn 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    // 1) Existence checks.
463    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    // 2) Property type checks (single-property only by grammar).
490    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
508/// Cheap stable string-encoding for a property tuple, appended to `out`:
509/// two tuples are equal for the pre-create uniqueness scan exactly when
510/// their keys are. Not exposed to callers because the shape isn't durable.
511fn 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
610/// True when `value` satisfies any branch of the target type. Used by
611/// both DDL-time scans and runtime mutation checks.
612pub 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        // Map / Any are rejected at DDL time, so they should never appear
653        // here; reaching this arm with one of them indicates an upstream
654        // bug — fail closed.
655        _ => 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
780/// Public read-side check used by mutation paths: given a proposed
781/// node create (labels + properties), is it accepted by every
782/// installed constraint? Cheap when no constraints are registered
783/// (single `is_empty()` on the catalog).
784pub(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
818/// [`check_node_create`] without the existence checks, for a statement
819/// that checks existence once it has finished (a later `SET` in the same
820/// statement may still supply the property). Pair with
821/// [`check_node_existence`].
822pub(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
840/// Relationship counterpart of [`check_node_create_deferred`].
841pub(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
859/// Existence (and key) constraints on a node as it stands now. A node
860/// that no longer exists passes.
861pub(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
887/// Relationship counterpart of [`check_node_existence`].
888pub(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
914/// Whether another node with `label` already holds `target` for `keys`.
915///
916/// Uniqueness constraints own a backing range index, which keeps the
917/// label-scoped hash index for the first key active. Looking the value up
918/// there makes each check O(matches) instead of a scan of every node in
919/// the graph, which made loading N constrained nodes O(N^2). When the
920/// value has no index image (temporal, spatial, vector) or the index is
921/// not active, fall back to scanning the label's nodes only.
922fn 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
949/// Relationship counterpart of [`any_other_node_with_tuple`].
950fn 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    // Reads only the constrained properties of the stored record.
1029    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
1041/// Mutation pre-check: about to `SET node.key = value`. Validates
1042/// every node-level constraint whose schema covers any of the node's
1043/// labels and any of its constrained properties touched by this write.
1044pub(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(()), // mutation will fail downstream
1054    };
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        // Type check on the new value.
1066        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        // Uniqueness: build the post-set tuple and search the rest of
1078        // the graph for an identical one.
1079        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
1096/// Mutation pre-check: about to `REMOVE node.key`. Rejects when an
1097/// existence / key constraint requires the property to remain present.
1098pub(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
1126/// Mutation pre-check: about to replace the full property map on a
1127/// node. Validate the final record shape, but skip the node itself
1128/// when checking uniqueness.
1129pub(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
1138/// [`check_node_replace_properties`] without the existence checks, for a
1139/// statement that checks existence once it has finished (`SET n = {…},
1140/// n.required = …`). Pair with [`check_node_existence`].
1141pub(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
1250/// Mutation pre-check: about to replace the full property map on a
1251/// relationship. Validate the final record shape, but skip the
1252/// relationship itself when checking uniqueness.
1253pub(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
1262/// Relationship counterpart of [`check_node_replace_properties_deferred`].
1263pub(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
1295/// Mutation pre-check: about to `SET n:Label` (add label). All
1296/// existence / type / uniqueness constraints attached to `Label`
1297/// suddenly start applying to this node; if any of them is violated
1298/// the mutation is rejected.
1299pub(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
1320/// [`check_node_add_label`] without the existence checks, for a statement
1321/// that checks existence once it has finished. Pair with
1322/// [`check_node_existence`].
1323pub(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    /// Records 0..n carry `values[id]` under `k`, all under one forced
1368    /// hash, so every record after the first takes the collision path.
1369    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        // Int 1 and Float 1.0 stay distinct, as they did as key strings.
1379        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}