Skip to main content

issundb_core/graph/
index.rs

1use super::*;
2
3impl Graph {
4    // ------------------------------------------------------------------
5    // Secondary index queries
6    // ------------------------------------------------------------------
7
8    /// Returns all node IDs with the given label, in ascending ID order.
9    pub fn nodes_by_label(&self, label: &str) -> Result<Vec<NodeId>, Error> {
10        let rtxn = self.storage.env.read_txn()?;
11        self.nodes_by_label_impl(&rtxn, label)
12    }
13
14    pub(super) fn nodes_by_label_impl(
15        &self,
16        rtxn: &heed::RoTxn,
17        label: &str,
18    ) -> Result<Vec<NodeId>, Error> {
19        let label_id = {
20            let key = format!("label:{label}");
21            match self.storage.meta.get(rtxn, &key)? {
22                Some(b) => {
23                    let arr: [u8; 4] = b
24                        .try_into()
25                        .map_err(|_| Error::Corrupt("label id must be 4 bytes"))?;
26                    u32::from_be_bytes(arr)
27                }
28                None => return Ok(vec![]),
29            }
30        };
31        let prefix = label_id.to_be_bytes();
32        let iter = self.storage.label_idx.prefix_iter(rtxn, &prefix)?;
33        let mut ids = Vec::new();
34        for result in iter {
35            let (key, _) = result?;
36            let id_bytes: [u8; 8] = key[4..]
37                .try_into()
38                .map_err(|_| Error::Corrupt("label_idx key has wrong length"))?;
39            ids.push(u64::from_be_bytes(id_bytes));
40        }
41        Ok(ids)
42    }
43
44    /// Returns the subset of `nodes` that carry `label`, preserving input
45    /// order. One `label_idx` point lookup per candidate, so the cost scales
46    /// with the candidate set rather than the label population.
47    #[doc(hidden)]
48    pub fn label_filter(&self, nodes: &[NodeId], label: &str) -> Result<Vec<NodeId>, Error> {
49        let rtxn = self.storage.env.read_txn()?;
50        let label_id = match get_label(&self.storage, &rtxn, label)? {
51            Some(id) => id,
52            None => return Ok(vec![]),
53        };
54        let mut out = Vec::new();
55        for &n in nodes {
56            if self
57                .storage
58                .label_idx
59                .get(&rtxn, &composite_key(label_id, n))?
60                .is_some()
61            {
62                out.push(n);
63            }
64        }
65        Ok(out)
66    }
67
68    /// Returns all edge IDs with the given type, in ascending ID order.
69    pub fn edges_by_type(&self, etype: &str) -> Result<Vec<EdgeId>, Error> {
70        let rtxn = self.storage.env.read_txn()?;
71        self.edges_by_type_impl(&rtxn, etype)
72    }
73
74    pub(super) fn edges_by_type_impl(
75        &self,
76        rtxn: &heed::RoTxn,
77        etype: &str,
78    ) -> Result<Vec<EdgeId>, Error> {
79        let type_id = {
80            let key = format!("type:{etype}");
81            match self.storage.meta.get(rtxn, &key)? {
82                Some(b) => {
83                    let arr: [u8; 4] = b
84                        .try_into()
85                        .map_err(|_| Error::Corrupt("type id must be 4 bytes"))?;
86                    u32::from_be_bytes(arr)
87                }
88                None => return Ok(vec![]),
89            }
90        };
91        let prefix = type_id.to_be_bytes();
92        let iter = self.storage.type_idx.prefix_iter(rtxn, &prefix)?;
93        let mut ids = Vec::new();
94        for result in iter {
95            let (key, _) = result?;
96            let id_bytes: [u8; 8] = key[4..]
97                .try_into()
98                .map_err(|_| Error::Corrupt("type_idx key has wrong length"))?;
99            ids.push(u64::from_be_bytes(id_bytes));
100        }
101        Ok(ids)
102    }
103
104    // ------------------------------------------------------------------
105    // Registry reverse lookups
106    // ------------------------------------------------------------------
107
108    /// Resolves a `LabelId` back to its string name.
109    ///
110    /// Scans the `meta` sub-database for the matching `label:{name}` entry.
111    /// Returns `None` for ids that are not in the registry.
112    pub fn label_name(&self, id: LabelId) -> Result<Option<String>, Error> {
113        let rtxn = self.storage.env.read_txn()?;
114        self.label_name_impl(&rtxn, id)
115    }
116
117    pub(super) fn label_name_impl(
118        &self,
119        rtxn: &heed::RoTxn,
120        id: LabelId,
121    ) -> Result<Option<String>, Error> {
122        self.meta_reverse_lookup_impl(rtxn, "label:", id)
123    }
124
125    /// Resolves a `TypeId` back to its string name.
126    ///
127    /// Scans the `meta` sub-database for the matching `type:{name}` entry.
128    /// Returns `None` for ids that are not in the registry.
129    pub fn type_name(&self, id: TypeId) -> Result<Option<String>, Error> {
130        let rtxn = self.storage.env.read_txn()?;
131        self.type_name_impl(&rtxn, id)
132    }
133
134    pub(super) fn type_name_impl(
135        &self,
136        rtxn: &heed::RoTxn,
137        id: TypeId,
138    ) -> Result<Option<String>, Error> {
139        self.meta_reverse_lookup_impl(rtxn, "type:", id)
140    }
141
142    pub(super) fn prop_key_name_impl(
143        &self,
144        rtxn: &heed::RoTxn,
145        id: PropKeyId,
146    ) -> Result<Option<String>, Error> {
147        self.meta_reverse_lookup_impl(rtxn, "prop_key:", id)
148    }
149
150    /// Validate the active edge constraints for `etype` against the edge's
151    /// encoded properties and write one `edge_prop_idx` entry per indexed
152    /// property. Shared by `add_edge` and `update_edge`; `update_edge` must
153    /// drop the edge's old entries first so the unique check never conflicts
154    /// with the edge itself.
155    pub(super) fn write_edge_index_entries(
156        &self,
157        wtxn: &mut heed::RwTxn,
158        edge_id: EdgeId,
159        type_id: TypeId,
160        etype: &str,
161        encoded_props: &[u8],
162    ) -> Result<(), Error> {
163        let active_indexes = self.get_active_edge_indexes(wtxn, type_id)?;
164        if active_indexes.is_empty() {
165            return Ok(());
166        }
167        let props_json: serde_json::Value = props::decode(encoded_props)?;
168        for (prop_key_id, flags) in active_indexes {
169            if let Some(prop_name) = self.prop_key_name_impl(wtxn, prop_key_id)? {
170                let prop_val = props_json.get(&prop_name);
171
172                // 1. Required constraint check
173                if flags == 0x02
174                    && (prop_val.is_none() || prop_val == Some(&serde_json::Value::Null))
175                {
176                    return Err(Error::RequiredConstraintViolation(
177                        etype.to_string(),
178                        prop_name.to_string(),
179                    ));
180                }
181
182                if let Some(val) = prop_val {
183                    if val != &serde_json::Value::Null {
184                        if let Some(encoded) = encode_property_value(val) {
185                            // 2. Unique constraint check
186                            if flags == 0x01 {
187                                let mut prefix = Vec::with_capacity(4 + 4 + encoded.len());
188                                prefix.extend_from_slice(&type_id.to_be_bytes());
189                                prefix.extend_from_slice(&prop_key_id.to_be_bytes());
190                                prefix.extend_from_slice(&encoded);
191
192                                for entry in
193                                    self.storage.edge_prop_idx.prefix_iter(wtxn, &prefix)?
194                                {
195                                    let (key, _) = entry?;
196                                    if key.len() >= 8 {
197                                        let mut edge_id_bytes = [0u8; 8];
198                                        edge_id_bytes.copy_from_slice(&key[key.len() - 8..]);
199                                        let found_edge_id = u64::from_be_bytes(edge_id_bytes);
200                                        if found_edge_id != edge_id {
201                                            return Err(Error::UniqueConstraintViolation(
202                                                etype.to_string(),
203                                                prop_name.to_string(),
204                                                val.to_string(),
205                                            ));
206                                        }
207                                    }
208                                }
209                            }
210
211                            // 3. Write index entry
212                            let idx_key =
213                                edge_prop_index_key(type_id, prop_key_id, &encoded, edge_id);
214                            self.storage.edge_prop_idx.put(wtxn, &idx_key, &())?;
215                        }
216                    }
217                }
218            }
219        }
220        Ok(())
221    }
222
223    pub(super) fn delete_edge_index_entries(
224        &self,
225        wtxn: &mut heed::RwTxn,
226        edge_id: EdgeId,
227        record: &EdgeRecord,
228    ) -> Result<(), Error> {
229        let active_indexes = self.get_active_edge_indexes(wtxn, record.edge_type)?;
230        if !active_indexes.is_empty() {
231            let props_json: serde_json::Value = props::decode(&record.props)?;
232            for (prop_key_id, _) in active_indexes {
233                if let Some(prop_name) = self.prop_key_name_impl(wtxn, prop_key_id)? {
234                    if let Some(val) = props_json.get(&prop_name) {
235                        if let Some(encoded) = encode_property_value(val) {
236                            let idx_key = edge_prop_index_key(
237                                record.edge_type,
238                                prop_key_id,
239                                &encoded,
240                                edge_id,
241                            );
242                            self.storage.edge_prop_idx.delete(wtxn, &idx_key)?;
243                        }
244                    }
245                }
246            }
247        }
248        Ok(())
249    }
250
251    /// Get the count of nodes matching a string label.
252    pub fn node_count_by_label(&self, label: &str) -> Result<u64, Error> {
253        let rtxn = self.storage.env.read_txn()?;
254        self.node_count_by_label_impl(&rtxn, label)
255    }
256
257    /// Upper-bound estimate of the node count: the node-id high-water mark. This
258    /// does not decrease when a node is deleted, so it is not an exact live
259    /// count; it exists for query-planner cardinality estimates (for example,
260    /// average relationship fan-out). O(1).
261    pub fn node_count_hint(&self) -> Result<u64, Error> {
262        let rtxn = self.storage.env.read_txn()?;
263        crate::storage::ids::node_high_water(&self.storage, &rtxn)
264    }
265
266    pub(super) fn node_count_by_label_impl(
267        &self,
268        rtxn: &heed::RoTxn,
269        label: &str,
270    ) -> Result<u64, Error> {
271        let meta_key = format!("label:{label}");
272        if let Some(b) = self.storage.meta.get(rtxn, &meta_key)? {
273            let arr: [u8; 4] = b
274                .try_into()
275                .map_err(|_| Error::Corrupt("label id must be 4 bytes"))?;
276            let label_id = u32::from_be_bytes(arr);
277            crate::storage::ids::get_label_count(&self.storage, rtxn, label_id)
278        } else {
279            Ok(0)
280        }
281    }
282
283    /// Get the count of edges matching a string type.
284    pub fn edge_count_by_type(&self, etype: &str) -> Result<u64, Error> {
285        let rtxn = self.storage.env.read_txn()?;
286        self.edge_count_by_type_impl(&rtxn, etype)
287    }
288
289    pub(super) fn edge_count_by_type_impl(
290        &self,
291        rtxn: &heed::RoTxn,
292        etype: &str,
293    ) -> Result<u64, Error> {
294        let meta_key = format!("type:{etype}");
295        if let Some(b) = self.storage.meta.get(rtxn, &meta_key)? {
296            let arr: [u8; 4] = b
297                .try_into()
298                .map_err(|_| Error::Corrupt("type id must be 4 bytes"))?;
299            let type_id = u32::from_be_bytes(arr);
300            crate::storage::ids::get_type_count(&self.storage, rtxn, type_id)
301        } else {
302            Ok(0)
303        }
304    }
305
306    pub(super) fn meta_reverse_lookup_impl(
307        &self,
308        rtxn: &heed::RoTxn,
309        prefix: &str,
310        id: u32,
311    ) -> Result<Option<String>, Error> {
312        for entry in self.storage.meta.iter(rtxn)? {
313            let (key, val) = entry?;
314            if let Some(name) = key.strip_prefix(prefix) {
315                if val.len() == 4 {
316                    let stored = u32::from_be_bytes([val[0], val[1], val[2], val[3]]);
317                    if stored == id {
318                        return Ok(Some(name.to_owned()));
319                    }
320                }
321            }
322        }
323        Ok(None)
324    }
325
326    pub(super) fn get_active_node_indexes(
327        &self,
328        rtxn: &heed::RoTxn,
329        label_id: LabelId,
330    ) -> Result<Vec<(PropKeyId, u8)>, Error> {
331        let prefix = format!("idx_meta:node:l:{label_id}:p:");
332        let mut active = Vec::new();
333        for entry in self.storage.meta.prefix_iter(rtxn, &prefix)? {
334            let (key, val) = entry?;
335            if let Some(prop_str) = key.strip_prefix(&prefix) {
336                let prop_key_id: PropKeyId = prop_str
337                    .parse()
338                    .map_err(|_| Error::Corrupt("prop key id in meta must be integer"))?;
339                let flags = val.first().copied().unwrap_or(0x00);
340                active.push((prop_key_id, flags));
341            }
342        }
343        Ok(active)
344    }
345
346    pub(super) fn get_active_edge_indexes(
347        &self,
348        rtxn: &heed::RoTxn,
349        type_id: TypeId,
350    ) -> Result<Vec<(PropKeyId, u8)>, Error> {
351        let prefix = format!("idx_meta:edge:t:{type_id}:p:");
352        let mut active = Vec::new();
353        for entry in self.storage.meta.prefix_iter(rtxn, &prefix)? {
354            let (key, val) = entry?;
355            if let Some(prop_str) = key.strip_prefix(&prefix) {
356                let prop_key_id: PropKeyId = prop_str
357                    .parse()
358                    .map_err(|_| Error::Corrupt("prop key id in meta must be integer"))?;
359                let flags = val.first().copied().unwrap_or(0x00);
360                active.push((prop_key_id, flags));
361            }
362        }
363        Ok(active)
364    }
365
366    pub fn create_node_property_index(&self, label: &str, property: &str) -> Result<(), Error> {
367        let _guard = self._write_lock.lock();
368        let mut wtxn = self.storage.env.write_txn()?;
369        self.create_node_index_impl(&mut wtxn, label, property, 0x00)?;
370        wtxn.commit()?;
371        Ok(())
372    }
373
374    pub fn create_node_unique_constraint(&self, label: &str, property: &str) -> Result<(), Error> {
375        let _guard = self._write_lock.lock();
376        let mut wtxn = self.storage.env.write_txn()?;
377        self.create_node_index_impl(&mut wtxn, label, property, 0x01)?;
378        wtxn.commit()?;
379        Ok(())
380    }
381
382    pub fn create_node_required_constraint(
383        &self,
384        label: &str,
385        property: &str,
386    ) -> Result<(), Error> {
387        let _guard = self._write_lock.lock();
388        let mut wtxn = self.storage.env.write_txn()?;
389        self.create_node_index_impl(&mut wtxn, label, property, 0x02)?;
390        wtxn.commit()?;
391        Ok(())
392    }
393
394    pub(super) fn create_node_index_impl(
395        &self,
396        wtxn: &mut heed::RwTxn,
397        label: &str,
398        property: &str,
399        flags: u8,
400    ) -> Result<(), Error> {
401        let label_id = get_or_create_label(&self.storage, wtxn, label)?;
402        let prop_key_id = get_or_create_prop_key(&self.storage, wtxn, property)?;
403        let meta_key = format!("idx_meta:node:l:{label_id}:p:{prop_key_id}");
404
405        if let Some(existing_val) = self.storage.meta.get(wtxn, &meta_key)? {
406            if !existing_val.is_empty() && existing_val[0] == flags {
407                return Ok(());
408            }
409        }
410
411        let node_ids = self.nodes_by_label_impl(wtxn, label)?;
412        let mut seen_values = ahash::AHashSet::new();
413
414        for node_id in &node_ids {
415            let record = self
416                .get_node_impl(wtxn, *node_id)?
417                .ok_or(Error::NodeNotFound(*node_id))?;
418            let props_json: serde_json::Value = props::decode(&record.props)?;
419            let prop_val = props_json.get(property);
420
421            if flags == 0x02 && (prop_val.is_none() || prop_val == Some(&serde_json::Value::Null)) {
422                return Err(Error::RequiredConstraintViolation(
423                    label.to_string(),
424                    property.to_string(),
425                ));
426            }
427
428            if let Some(val) = prop_val {
429                if flags == 0x01 && !seen_values.insert(val.clone()) {
430                    return Err(Error::UniqueConstraintViolation(
431                        label.to_string(),
432                        property.to_string(),
433                        val.to_string(),
434                    ));
435                }
436            }
437        }
438
439        self.storage.meta.put(wtxn, &meta_key, &[flags])?;
440
441        for node_id in node_ids {
442            let record = self
443                .get_node_impl(wtxn, node_id)?
444                .ok_or(Error::NodeNotFound(node_id))?;
445            let props_json: serde_json::Value = props::decode(&record.props)?;
446            if let Some(val) = props_json.get(property) {
447                if let Some(encoded) = encode_property_value(val) {
448                    let idx_key = node_prop_index_key(label_id, prop_key_id, &encoded, node_id);
449                    self.storage.node_prop_idx.put(wtxn, &idx_key, &())?;
450                }
451            }
452        }
453
454        Ok(())
455    }
456
457    pub fn drop_node_property_index(&self, label: &str, property: &str) -> Result<(), Error> {
458        let _guard = self._write_lock.lock();
459        let mut wtxn = self.storage.env.write_txn()?;
460        self.drop_node_index_impl(&mut wtxn, label, property, 0x00)?;
461        wtxn.commit()?;
462        Ok(())
463    }
464
465    pub fn drop_node_unique_constraint(&self, label: &str, property: &str) -> Result<(), Error> {
466        let _guard = self._write_lock.lock();
467        let mut wtxn = self.storage.env.write_txn()?;
468        self.drop_node_index_impl(&mut wtxn, label, property, 0x01)?;
469        wtxn.commit()?;
470        Ok(())
471    }
472
473    pub fn drop_node_required_constraint(&self, label: &str, property: &str) -> Result<(), Error> {
474        let _guard = self._write_lock.lock();
475        let mut wtxn = self.storage.env.write_txn()?;
476        self.drop_node_index_impl(&mut wtxn, label, property, 0x02)?;
477        wtxn.commit()?;
478        Ok(())
479    }
480
481    pub(super) fn drop_node_index_impl(
482        &self,
483        wtxn: &mut heed::RwTxn,
484        label: &str,
485        property: &str,
486        flags: u8,
487    ) -> Result<(), Error> {
488        let label_id = get_or_create_label(&self.storage, wtxn, label)?;
489        let prop_key_id = get_or_create_prop_key(&self.storage, wtxn, property)?;
490        let meta_key = format!("idx_meta:node:l:{label_id}:p:{prop_key_id}");
491
492        if let Some(existing_val) = self.storage.meta.get(wtxn, &meta_key)? {
493            if !existing_val.is_empty() && existing_val[0] == flags {
494                self.storage.meta.delete(wtxn, &meta_key)?;
495
496                // `node_prop_idx` doubles as the always-on auto-index for scalar
497                // properties (see `index_node_for_label`). Dropping an explicit
498                // index or constraint must not remove those baseline entries, or
499                // `nodes_by_property` and the Cypher NodeIndexScan would return
500                // wrong (empty) results for still-present nodes. Remove only the
501                // entries the auto-index never maintains: null-valued entries
502                // written by `create_node_index_impl`.
503                let mut prefix = Vec::with_capacity(8);
504                prefix.extend_from_slice(&label_id.to_be_bytes());
505                prefix.extend_from_slice(&prop_key_id.to_be_bytes());
506
507                let mut to_delete = Vec::new();
508                for entry in self.storage.node_prop_idx.prefix_iter(wtxn, &prefix)? {
509                    let (key, _) = entry?;
510                    if key.len() >= prefix.len() + 8 {
511                        let encoded_val = &key[prefix.len()..key.len() - 8];
512                        if encoded_val == [crate::graph::ENCODED_NULL].as_slice() {
513                            to_delete.push(key.to_vec());
514                        }
515                    }
516                }
517
518                for key in to_delete {
519                    self.storage.node_prop_idx.delete(wtxn, &key)?;
520                }
521            }
522        }
523
524        Ok(())
525    }
526
527    pub fn create_edge_property_index(&self, etype: &str, property: &str) -> Result<(), Error> {
528        let _guard = self._write_lock.lock();
529        let mut wtxn = self.storage.env.write_txn()?;
530        self.create_edge_index_impl(&mut wtxn, etype, property, 0x00)?;
531        wtxn.commit()?;
532        Ok(())
533    }
534
535    pub fn create_edge_unique_constraint(&self, etype: &str, property: &str) -> Result<(), Error> {
536        let _guard = self._write_lock.lock();
537        let mut wtxn = self.storage.env.write_txn()?;
538        self.create_edge_index_impl(&mut wtxn, etype, property, 0x01)?;
539        wtxn.commit()?;
540        Ok(())
541    }
542
543    pub fn create_edge_required_constraint(
544        &self,
545        etype: &str,
546        property: &str,
547    ) -> Result<(), Error> {
548        let _guard = self._write_lock.lock();
549        let mut wtxn = self.storage.env.write_txn()?;
550        self.create_edge_index_impl(&mut wtxn, etype, property, 0x02)?;
551        wtxn.commit()?;
552        Ok(())
553    }
554
555    pub(super) fn create_edge_index_impl(
556        &self,
557        wtxn: &mut heed::RwTxn,
558        etype: &str,
559        property: &str,
560        flags: u8,
561    ) -> Result<(), Error> {
562        let type_id = get_or_create_type(&self.storage, wtxn, etype)?;
563        let prop_key_id = get_or_create_prop_key(&self.storage, wtxn, property)?;
564        let meta_key = format!("idx_meta:edge:t:{type_id}:p:{prop_key_id}");
565
566        if let Some(existing_val) = self.storage.meta.get(wtxn, &meta_key)? {
567            if !existing_val.is_empty() && existing_val[0] == flags {
568                return Ok(());
569            }
570        }
571
572        let edge_ids = self.edges_by_type_impl(wtxn, etype)?;
573        let mut seen_values = ahash::AHashSet::new();
574
575        for edge_id in &edge_ids {
576            let record = self
577                .get_edge_impl(wtxn, *edge_id)?
578                .ok_or(Error::EdgeNotFound(*edge_id))?;
579            let props_json: serde_json::Value = props::decode(&record.props)?;
580            let prop_val = props_json.get(property);
581
582            if flags == 0x02 && (prop_val.is_none() || prop_val == Some(&serde_json::Value::Null)) {
583                return Err(Error::RequiredConstraintViolation(
584                    etype.to_string(),
585                    property.to_string(),
586                ));
587            }
588
589            if let Some(val) = prop_val {
590                if flags == 0x01 && !seen_values.insert(val.clone()) {
591                    return Err(Error::UniqueConstraintViolation(
592                        etype.to_string(),
593                        property.to_string(),
594                        val.to_string(),
595                    ));
596                }
597            }
598        }
599
600        self.storage.meta.put(wtxn, &meta_key, &[flags])?;
601
602        for edge_id in edge_ids {
603            let record = self
604                .get_edge_impl(wtxn, edge_id)?
605                .ok_or(Error::EdgeNotFound(edge_id))?;
606            let props_json: serde_json::Value = props::decode(&record.props)?;
607            if let Some(val) = props_json.get(property) {
608                if let Some(encoded) = encode_property_value(val) {
609                    let idx_key = edge_prop_index_key(type_id, prop_key_id, &encoded, edge_id);
610                    self.storage.edge_prop_idx.put(wtxn, &idx_key, &())?;
611                }
612            }
613        }
614
615        Ok(())
616    }
617
618    pub fn drop_edge_property_index(&self, etype: &str, property: &str) -> Result<(), Error> {
619        let _guard = self._write_lock.lock();
620        let mut wtxn = self.storage.env.write_txn()?;
621        self.drop_edge_index_impl(&mut wtxn, etype, property, 0x00)?;
622        wtxn.commit()?;
623        Ok(())
624    }
625
626    pub fn drop_edge_unique_constraint(&self, etype: &str, property: &str) -> Result<(), Error> {
627        let _guard = self._write_lock.lock();
628        let mut wtxn = self.storage.env.write_txn()?;
629        self.drop_edge_index_impl(&mut wtxn, etype, property, 0x01)?;
630        wtxn.commit()?;
631        Ok(())
632    }
633
634    pub fn drop_edge_required_constraint(&self, etype: &str, property: &str) -> Result<(), Error> {
635        let _guard = self._write_lock.lock();
636        let mut wtxn = self.storage.env.write_txn()?;
637        self.drop_edge_index_impl(&mut wtxn, etype, property, 0x02)?;
638        wtxn.commit()?;
639        Ok(())
640    }
641
642    pub(super) fn drop_edge_index_impl(
643        &self,
644        wtxn: &mut heed::RwTxn,
645        etype: &str,
646        property: &str,
647        flags: u8,
648    ) -> Result<(), Error> {
649        let type_id = get_or_create_type(&self.storage, wtxn, etype)?;
650        let prop_key_id = get_or_create_prop_key(&self.storage, wtxn, property)?;
651        let meta_key = format!("idx_meta:edge:t:{type_id}:p:{prop_key_id}");
652
653        if let Some(existing_val) = self.storage.meta.get(wtxn, &meta_key)? {
654            if !existing_val.is_empty() && existing_val[0] == flags {
655                self.storage.meta.delete(wtxn, &meta_key)?;
656
657                let mut prefix = Vec::with_capacity(8);
658                prefix.extend_from_slice(&type_id.to_be_bytes());
659                prefix.extend_from_slice(&prop_key_id.to_be_bytes());
660
661                let mut to_delete = Vec::new();
662                for entry in self.storage.edge_prop_idx.prefix_iter(wtxn, &prefix)? {
663                    let (key, _) = entry?;
664                    to_delete.push(key.to_vec());
665                }
666
667                for key in to_delete {
668                    self.storage.edge_prop_idx.delete(wtxn, &key)?;
669                }
670            }
671        }
672
673        Ok(())
674    }
675
676    pub fn nodes_by_property(
677        &self,
678        label: &str,
679        property: &str,
680        val: PropValue,
681    ) -> Result<Vec<NodeId>, Error> {
682        let rtxn = self.storage.env.read_txn()?;
683        self.nodes_by_property_impl(&rtxn, label, property, val)
684    }
685
686    pub(super) fn nodes_by_property_impl(
687        &self,
688        rtxn: &heed::RoTxn,
689        label: &str,
690        property: &str,
691        val: PropValue,
692    ) -> Result<Vec<NodeId>, Error> {
693        let val = val.into_json();
694        let label_key = format!("label:{label}");
695        let label_id = match self.storage.meta.get(rtxn, &label_key)? {
696            Some(b) => {
697                let arr: [u8; 4] = b
698                    .try_into()
699                    .map_err(|_| Error::Corrupt("label id must be 4 bytes"))?;
700                u32::from_be_bytes(arr)
701            }
702            None => return Ok(Vec::new()),
703        };
704
705        let prop_key = format!("prop_key:{property}");
706        let prop_key_id = match self.storage.meta.get(rtxn, &prop_key)? {
707            Some(b) => {
708                let arr: [u8; 4] = b
709                    .try_into()
710                    .map_err(|_| Error::Corrupt("prop key id must be 4 bytes"))?;
711                u32::from_be_bytes(arr)
712            }
713            None => return Ok(Vec::new()),
714        };
715
716        let encoded = match encode_property_value(&val) {
717            Some(e) => e,
718            None => return Ok(Vec::new()),
719        };
720
721        let mut prefix = Vec::with_capacity(4 + 4 + encoded.len());
722        prefix.extend_from_slice(&label_id.to_be_bytes());
723        prefix.extend_from_slice(&prop_key_id.to_be_bytes());
724        prefix.extend_from_slice(&encoded);
725
726        let mut result = Vec::new();
727        for entry in self.storage.node_prop_idx.prefix_iter(rtxn, &prefix)? {
728            let (key, _) = entry?;
729            if key.len() >= 8 {
730                let mut node_id_bytes = [0u8; 8];
731                node_id_bytes.copy_from_slice(&key[key.len() - 8..]);
732                result.push(u64::from_be_bytes(node_id_bytes));
733            }
734        }
735        Ok(result)
736    }
737
738    pub fn nodes_by_property_range(
739        &self,
740        label: &str,
741        property: &str,
742        min_val: Option<PropValue>,
743        min_inclusive: bool,
744        max_val: Option<PropValue>,
745        max_inclusive: bool,
746    ) -> Result<Vec<NodeId>, Error> {
747        let rtxn = self.storage.env.read_txn()?;
748        self.nodes_by_property_range_impl(
749            &rtxn,
750            label,
751            property,
752            min_val,
753            min_inclusive,
754            max_val,
755            max_inclusive,
756        )
757    }
758
759    #[allow(clippy::too_many_arguments)]
760    pub(super) fn nodes_by_property_range_impl(
761        &self,
762        rtxn: &heed::RoTxn,
763        label: &str,
764        property: &str,
765        min_val: Option<PropValue>,
766        min_inclusive: bool,
767        max_val: Option<PropValue>,
768        max_inclusive: bool,
769    ) -> Result<Vec<NodeId>, Error> {
770        let label_key = format!("label:{label}");
771        let label_id = match self.storage.meta.get(rtxn, &label_key)? {
772            Some(b) => {
773                let arr: [u8; 4] = b
774                    .try_into()
775                    .map_err(|_| Error::Corrupt("label id must be 4 bytes"))?;
776                u32::from_be_bytes(arr)
777            }
778            None => return Ok(Vec::new()),
779        };
780
781        let prop_key = format!("prop_key:{property}");
782        let prop_key_id = match self.storage.meta.get(rtxn, &prop_key)? {
783            Some(b) => {
784                let arr: [u8; 4] = b
785                    .try_into()
786                    .map_err(|_| Error::Corrupt("prop key id must be 4 bytes"))?;
787                u32::from_be_bytes(arr)
788            }
789            None => return Ok(Vec::new()),
790        };
791
792        let mut prefix = Vec::with_capacity(8);
793        prefix.extend_from_slice(&label_id.to_be_bytes());
794        prefix.extend_from_slice(&prop_key_id.to_be_bytes());
795
796        let min_encoded = min_val
797            .map(|v| v.into_json())
798            .as_ref()
799            .and_then(encode_property_value);
800        let max_encoded = max_val
801            .map(|v| v.into_json())
802            .as_ref()
803            .and_then(encode_property_value);
804
805        let mut result = Vec::new();
806        for entry in self.storage.node_prop_idx.prefix_iter(rtxn, &prefix)? {
807            let (key, _) = entry?;
808            if key.len() >= prefix.len() + 8 {
809                let val_bytes = &key[prefix.len()..key.len() - 8];
810
811                if let Some(ref min_enc) = min_encoded {
812                    if min_inclusive {
813                        if val_bytes < min_enc.as_slice() {
814                            continue;
815                        }
816                    } else if val_bytes <= min_enc.as_slice() {
817                        continue;
818                    }
819                }
820                if let Some(ref max_enc) = max_encoded {
821                    if max_inclusive {
822                        if val_bytes > max_enc.as_slice() {
823                            continue;
824                        }
825                    } else if val_bytes >= max_enc.as_slice() {
826                        continue;
827                    }
828                }
829
830                let mut node_id_bytes = [0u8; 8];
831                node_id_bytes.copy_from_slice(&key[key.len() - 8..]);
832                result.push(u64::from_be_bytes(node_id_bytes));
833            }
834        }
835        Ok(result)
836    }
837
838    pub fn has_node_property_index(&self, label: &str, property: &str) -> Result<bool, Error> {
839        let rtxn = self.storage.env.read_txn()?;
840        self.has_node_property_index_impl(&rtxn, label, property)
841    }
842
843    pub(super) fn has_node_property_index_impl(
844        &self,
845        rtxn: &heed::RoTxn,
846        label: &str,
847        property: &str,
848    ) -> Result<bool, Error> {
849        let label_key = format!("label:{label}");
850        let label_id = match self.storage.meta.get(rtxn, &label_key)? {
851            Some(b) => {
852                let arr: [u8; 4] = b
853                    .try_into()
854                    .map_err(|_| Error::Corrupt("label id must be 4 bytes"))?;
855                u32::from_be_bytes(arr)
856            }
857            None => return Ok(false),
858        };
859
860        let prop_key = format!("prop_key:{property}");
861        let prop_key_id = match self.storage.meta.get(rtxn, &prop_key)? {
862            Some(b) => {
863                let arr: [u8; 4] = b
864                    .try_into()
865                    .map_err(|_| Error::Corrupt("prop key id must be 4 bytes"))?;
866                u32::from_be_bytes(arr)
867            }
868            None => return Ok(false),
869        };
870
871        // Use a prefix seek on node_prop_idx: if any entry exists for this
872        // label+property combination the auto-index (or a user-created index)
873        // has data, so the optimizer may use NodeIndexScan.
874        let mut prefix = Vec::with_capacity(8);
875        prefix.extend_from_slice(&label_id.to_be_bytes());
876        prefix.extend_from_slice(&prop_key_id.to_be_bytes());
877        let mut iter = self.storage.node_prop_idx.prefix_iter(rtxn, &prefix)?;
878        Ok(iter.next().is_some())
879    }
880
881    pub fn edges_by_property(
882        &self,
883        etype: &str,
884        property: &str,
885        val: PropValue,
886    ) -> Result<Vec<EdgeId>, Error> {
887        let rtxn = self.storage.env.read_txn()?;
888        self.edges_by_property_impl(&rtxn, etype, property, val)
889    }
890
891    pub(super) fn edges_by_property_impl(
892        &self,
893        rtxn: &heed::RoTxn,
894        etype: &str,
895        property: &str,
896        val: PropValue,
897    ) -> Result<Vec<EdgeId>, Error> {
898        let val = val.into_json();
899        let type_key = format!("type:{etype}");
900        let type_id = match self.storage.meta.get(rtxn, &type_key)? {
901            Some(b) => {
902                let arr: [u8; 4] = b
903                    .try_into()
904                    .map_err(|_| Error::Corrupt("type id must be 4 bytes"))?;
905                u32::from_be_bytes(arr)
906            }
907            None => return Ok(Vec::new()),
908        };
909
910        let prop_key = format!("prop_key:{property}");
911        let prop_key_id = match self.storage.meta.get(rtxn, &prop_key)? {
912            Some(b) => {
913                let arr: [u8; 4] = b
914                    .try_into()
915                    .map_err(|_| Error::Corrupt("prop key id must be 4 bytes"))?;
916                u32::from_be_bytes(arr)
917            }
918            None => return Ok(Vec::new()),
919        };
920
921        let encoded = match encode_property_value(&val) {
922            Some(e) => e,
923            None => return Ok(Vec::new()),
924        };
925
926        let mut prefix = Vec::with_capacity(4 + 4 + encoded.len());
927        prefix.extend_from_slice(&type_id.to_be_bytes());
928        prefix.extend_from_slice(&prop_key_id.to_be_bytes());
929        prefix.extend_from_slice(&encoded);
930
931        let mut result = Vec::new();
932        for entry in self.storage.edge_prop_idx.prefix_iter(rtxn, &prefix)? {
933            let (key, _) = entry?;
934            if key.len() >= 8 {
935                let mut edge_id_bytes = [0u8; 8];
936                edge_id_bytes.copy_from_slice(&key[key.len() - 8..]);
937                result.push(u64::from_be_bytes(edge_id_bytes));
938            }
939        }
940        Ok(result)
941    }
942
943    pub fn edges_by_property_range(
944        &self,
945        etype: &str,
946        property: &str,
947        min_val: Option<PropValue>,
948        max_val: Option<PropValue>,
949    ) -> Result<Vec<EdgeId>, Error> {
950        let rtxn = self.storage.env.read_txn()?;
951        self.edges_by_property_range_impl(&rtxn, etype, property, min_val, max_val)
952    }
953
954    pub(super) fn edges_by_property_range_impl(
955        &self,
956        rtxn: &heed::RoTxn,
957        etype: &str,
958        property: &str,
959        min_val: Option<PropValue>,
960        max_val: Option<PropValue>,
961    ) -> Result<Vec<EdgeId>, Error> {
962        let type_key = format!("type:{etype}");
963        let type_id = match self.storage.meta.get(rtxn, &type_key)? {
964            Some(b) => {
965                let arr: [u8; 4] = b
966                    .try_into()
967                    .map_err(|_| Error::Corrupt("type id must be 4 bytes"))?;
968                u32::from_be_bytes(arr)
969            }
970            None => return Ok(Vec::new()),
971        };
972
973        let prop_key = format!("prop_key:{property}");
974        let prop_key_id = match self.storage.meta.get(rtxn, &prop_key)? {
975            Some(b) => {
976                let arr: [u8; 4] = b
977                    .try_into()
978                    .map_err(|_| Error::Corrupt("prop key id must be 4 bytes"))?;
979                u32::from_be_bytes(arr)
980            }
981            None => return Ok(Vec::new()),
982        };
983
984        let mut prefix = Vec::with_capacity(8);
985        prefix.extend_from_slice(&type_id.to_be_bytes());
986        prefix.extend_from_slice(&prop_key_id.to_be_bytes());
987
988        let min_encoded = min_val
989            .map(|v| v.into_json())
990            .as_ref()
991            .and_then(encode_property_value);
992        let max_encoded = max_val
993            .map(|v| v.into_json())
994            .as_ref()
995            .and_then(encode_property_value);
996
997        let mut result = Vec::new();
998        for entry in self.storage.edge_prop_idx.prefix_iter(rtxn, &prefix)? {
999            let (key, _) = entry?;
1000            if key.len() >= prefix.len() + 8 {
1001                let val_bytes = &key[prefix.len()..key.len() - 8];
1002
1003                if let Some(ref min_enc) = min_encoded {
1004                    if val_bytes < min_enc.as_slice() {
1005                        continue;
1006                    }
1007                }
1008                if let Some(ref max_enc) = max_encoded {
1009                    if val_bytes > max_enc.as_slice() {
1010                        continue;
1011                    }
1012                }
1013
1014                let mut edge_id_bytes = [0u8; 8];
1015                edge_id_bytes.copy_from_slice(&key[key.len() - 8..]);
1016                result.push(u64::from_be_bytes(edge_id_bytes));
1017            }
1018        }
1019        Ok(result)
1020    }
1021
1022    pub fn list_node_indexes_and_constraints(&self) -> Result<Vec<(String, String, u8)>, Error> {
1023        let rtxn = self.storage.env.read_txn()?;
1024        let mut result = Vec::new();
1025        for entry in self.storage.meta.iter(&rtxn)? {
1026            let (key, val) = entry?;
1027            if let Some(rest) = key.strip_prefix("idx_meta:node:l:") {
1028                let parts: Vec<&str> = rest.split(":p:").collect();
1029                if parts.len() == 2 {
1030                    if let (Ok(label_id), Ok(prop_key_id)) =
1031                        (parts[0].parse::<u32>(), parts[1].parse::<u32>())
1032                    {
1033                        if let (Some(label_name), Some(prop_name)) = (
1034                            self.label_name_impl(&rtxn, label_id)?,
1035                            crate::storage::ids::get_prop_key_name(
1036                                &self.storage,
1037                                &rtxn,
1038                                prop_key_id,
1039                            )?,
1040                        ) {
1041                            let flags = val.first().copied().unwrap_or(0x00);
1042                            result.push((label_name, prop_name, flags));
1043                        }
1044                    }
1045                }
1046            }
1047        }
1048        Ok(result)
1049    }
1050
1051    pub fn list_edge_indexes_and_constraints(&self) -> Result<Vec<(String, String, u8)>, Error> {
1052        let rtxn = self.storage.env.read_txn()?;
1053        let mut result = Vec::new();
1054        for entry in self.storage.meta.iter(&rtxn)? {
1055            let (key, val) = entry?;
1056            if let Some(rest) = key.strip_prefix("idx_meta:edge:t:") {
1057                let parts: Vec<&str> = rest.split(":p:").collect();
1058                if parts.len() == 2 {
1059                    if let (Ok(type_id), Ok(prop_key_id)) =
1060                        (parts[0].parse::<u32>(), parts[1].parse::<u32>())
1061                    {
1062                        if let (Some(type_name), Some(prop_name)) = (
1063                            self.type_name_impl(&rtxn, type_id)?,
1064                            crate::storage::ids::get_prop_key_name(
1065                                &self.storage,
1066                                &rtxn,
1067                                prop_key_id,
1068                            )?,
1069                        ) {
1070                            let flags = val.first().copied().unwrap_or(0x00);
1071                            result.push((type_name, prop_name, flags));
1072                        }
1073                    }
1074                }
1075            }
1076        }
1077        Ok(result)
1078    }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083    use serde_json::json;
1084    use tempfile::TempDir;
1085
1086    use super::*;
1087
1088    fn open_tmp() -> (TempDir, Graph) {
1089        let dir = TempDir::new().unwrap();
1090        let g = Graph::open(dir.path(), 1).unwrap();
1091        (dir, g)
1092    }
1093
1094    /// Dropping an explicit property index must leave the always-on auto-index
1095    /// intact so `nodes_by_property` still finds existing nodes.
1096    #[test]
1097    fn drop_index_preserves_auto_index() {
1098        let (_dir, g) = open_tmp();
1099        let id = g.add_node("Person", &json!({"age": 30})).unwrap();
1100
1101        g.create_node_property_index("Person", "age").unwrap();
1102        g.drop_node_property_index("Person", "age").unwrap();
1103
1104        assert_eq!(
1105            g.nodes_by_property("Person", "age", PropValue::Int(30))
1106                .unwrap(),
1107            vec![id],
1108            "auto-index entries must survive dropping the explicit index"
1109        );
1110    }
1111
1112    /// Dropping a unique constraint must keep property lookups working and stop
1113    /// enforcing uniqueness.
1114    #[test]
1115    fn drop_unique_constraint_preserves_lookups() {
1116        let (_dir, g) = open_tmp();
1117        let id = g.add_node("User", &json!({"email": "a@b.c"})).unwrap();
1118
1119        g.create_node_unique_constraint("User", "email").unwrap();
1120        g.drop_node_unique_constraint("User", "email").unwrap();
1121
1122        assert_eq!(
1123            g.nodes_by_property("User", "email", PropValue::Str("a@b.c".into()))
1124                .unwrap(),
1125            vec![id]
1126        );
1127
1128        // Uniqueness is no longer enforced; a duplicate value is accepted and
1129        // both nodes are findable.
1130        let id2 = g.add_node("User", &json!({"email": "a@b.c"})).unwrap();
1131        let mut hits = g
1132            .nodes_by_property("User", "email", PropValue::Str("a@b.c".into()))
1133            .unwrap();
1134        hits.sort();
1135        let mut expected = vec![id, id2];
1136        expected.sort();
1137        assert_eq!(hits, expected);
1138    }
1139
1140    /// Two nodes with integer properties beyond 2^53 must be distinguishable by
1141    /// `nodes_by_property`; the values previously collapsed through `f64`.
1142    #[test]
1143    fn large_integer_property_no_false_match() {
1144        let (_dir, g) = open_tmp();
1145        let a = g
1146            .add_node("Item", &json!({"sid": 9_007_199_254_740_992_i64}))
1147            .unwrap();
1148        let b = g
1149            .add_node("Item", &json!({"sid": 9_007_199_254_740_993_i64}))
1150            .unwrap();
1151
1152        assert_eq!(
1153            g.nodes_by_property("Item", "sid", PropValue::Int(9_007_199_254_740_992))
1154                .unwrap(),
1155            vec![a]
1156        );
1157        assert_eq!(
1158            g.nodes_by_property("Item", "sid", PropValue::Int(9_007_199_254_740_993))
1159                .unwrap(),
1160            vec![b]
1161        );
1162    }
1163
1164    /// An integer-valued property must still be findable when queried with the
1165    /// equal float, matching Cypher's `30 = 30.0` semantics.
1166    #[test]
1167    fn integer_property_matches_float_query() {
1168        let (_dir, g) = open_tmp();
1169        let id = g.add_node("Person", &json!({"age": 30})).unwrap();
1170        assert_eq!(
1171            g.nodes_by_property("Person", "age", PropValue::Float(30.0))
1172                .unwrap(),
1173            vec![id]
1174        );
1175    }
1176
1177    /// `node_count_hint` is the node-id high-water mark: it tracks allocations
1178    /// and must not decrease when a node is deleted.
1179    #[test]
1180    fn node_count_hint_is_high_water_mark() {
1181        let (_dir, g) = open_tmp();
1182        assert_eq!(g.node_count_hint().unwrap(), 0);
1183
1184        let a = g.add_node("N", &()).unwrap();
1185        g.add_node("N", &()).unwrap();
1186        assert_eq!(g.node_count_hint().unwrap(), 2);
1187
1188        g.delete_node(a).unwrap();
1189        assert_eq!(g.node_count_hint().unwrap(), 2);
1190    }
1191
1192    /// An edge property index created before any edges exist must be populated
1193    /// by `add_edge`, and one created afterwards must backfill existing edges.
1194    #[test]
1195    fn edge_property_index_lookup() {
1196        let (_dir, g) = open_tmp();
1197        let a = g.add_node("N", &()).unwrap();
1198        let b = g.add_node("N", &()).unwrap();
1199
1200        // Backfill path: the edge exists before the index.
1201        let e1 = g.add_edge(a, b, "ROAD", &json!({"cost": 5})).unwrap();
1202        g.create_edge_property_index("ROAD", "cost").unwrap();
1203
1204        // Insert path: the edge arrives after the index.
1205        let e2 = g.add_edge(b, a, "ROAD", &json!({"cost": 7})).unwrap();
1206
1207        assert_eq!(
1208            g.edges_by_property("ROAD", "cost", PropValue::Int(5))
1209                .unwrap(),
1210            vec![e1]
1211        );
1212        assert_eq!(
1213            g.edges_by_property("ROAD", "cost", PropValue::Int(7))
1214                .unwrap(),
1215            vec![e2]
1216        );
1217        assert_eq!(
1218            g.edges_by_property_range(
1219                "ROAD",
1220                "cost",
1221                Some(PropValue::Int(5)),
1222                Some(PropValue::Int(7)),
1223            )
1224            .unwrap(),
1225            vec![e1, e2]
1226        );
1227    }
1228
1229    #[test]
1230    fn drop_edge_property_index_removes_entries() {
1231        let (_dir, g) = open_tmp();
1232        let a = g.add_node("N", &()).unwrap();
1233        let b = g.add_node("N", &()).unwrap();
1234        g.create_edge_property_index("ROAD", "cost").unwrap();
1235        g.add_edge(a, b, "ROAD", &json!({"cost": 5})).unwrap();
1236
1237        g.drop_edge_property_index("ROAD", "cost").unwrap();
1238        assert_eq!(
1239            g.edges_by_property("ROAD", "cost", PropValue::Int(5))
1240                .unwrap(),
1241            Vec::<EdgeId>::new()
1242        );
1243    }
1244
1245    #[test]
1246    fn edge_unique_constraint_rejects_duplicate_insert() {
1247        let (_dir, g) = open_tmp();
1248        let a = g.add_node("N", &()).unwrap();
1249        let b = g.add_node("N", &()).unwrap();
1250        g.create_edge_unique_constraint("ROAD", "toll_id").unwrap();
1251
1252        g.add_edge(a, b, "ROAD", &json!({"toll_id": 1})).unwrap();
1253        let err = g
1254            .add_edge(b, a, "ROAD", &json!({"toll_id": 1}))
1255            .unwrap_err();
1256        assert!(matches!(err, Error::UniqueConstraintViolation(..)));
1257    }
1258
1259    #[test]
1260    fn edge_unique_constraint_rejects_existing_duplicates() {
1261        let (_dir, g) = open_tmp();
1262        let a = g.add_node("N", &()).unwrap();
1263        let b = g.add_node("N", &()).unwrap();
1264        g.add_edge(a, b, "ROAD", &json!({"toll_id": 1})).unwrap();
1265        g.add_edge(b, a, "ROAD", &json!({"toll_id": 1})).unwrap();
1266
1267        let err = g
1268            .create_edge_unique_constraint("ROAD", "toll_id")
1269            .unwrap_err();
1270        assert!(matches!(err, Error::UniqueConstraintViolation(..)));
1271    }
1272
1273    #[test]
1274    fn edge_required_constraint_rejects_missing_property() {
1275        let (_dir, g) = open_tmp();
1276        let a = g.add_node("N", &()).unwrap();
1277        let b = g.add_node("N", &()).unwrap();
1278        g.create_edge_required_constraint("ROAD", "cost").unwrap();
1279
1280        let err = g.add_edge(a, b, "ROAD", &json!({})).unwrap_err();
1281        assert!(matches!(err, Error::RequiredConstraintViolation(..)));
1282
1283        // Creating the constraint must also reject pre-existing violations.
1284        g.add_edge(a, b, "RAIL", &json!({})).unwrap();
1285        let err = g
1286            .create_edge_required_constraint("RAIL", "cost")
1287            .unwrap_err();
1288        assert!(matches!(err, Error::RequiredConstraintViolation(..)));
1289    }
1290
1291    /// `update_edge` must re-index the edge under its new property values:
1292    /// the old index entry disappears and the new one is findable.
1293    #[test]
1294    fn update_edge_reindexes_edge_properties() {
1295        let (_dir, g) = open_tmp();
1296        let a = g.add_node("N", &()).unwrap();
1297        let b = g.add_node("N", &()).unwrap();
1298        g.create_edge_property_index("ROAD", "cost").unwrap();
1299        let eid = g.add_edge(a, b, "ROAD", &json!({"cost": 5})).unwrap();
1300
1301        g.update_edge(eid, &json!({"cost": 7})).unwrap();
1302
1303        assert_eq!(
1304            g.edges_by_property("ROAD", "cost", PropValue::Int(5))
1305                .unwrap(),
1306            Vec::<EdgeId>::new(),
1307            "stale index entry must be removed"
1308        );
1309        assert_eq!(
1310            g.edges_by_property("ROAD", "cost", PropValue::Int(7))
1311                .unwrap(),
1312            vec![eid],
1313            "new value must be indexed"
1314        );
1315    }
1316
1317    #[test]
1318    fn update_edge_enforces_unique_constraint() {
1319        let (_dir, g) = open_tmp();
1320        let a = g.add_node("N", &()).unwrap();
1321        let b = g.add_node("N", &()).unwrap();
1322        g.create_edge_unique_constraint("ROAD", "toll_id").unwrap();
1323        g.add_edge(a, b, "ROAD", &json!({"toll_id": 1})).unwrap();
1324        let e2 = g.add_edge(b, a, "ROAD", &json!({"toll_id": 2})).unwrap();
1325
1326        let err = g.update_edge(e2, &json!({"toll_id": 1})).unwrap_err();
1327        assert!(matches!(err, Error::UniqueConstraintViolation(..)));
1328
1329        // Updating an edge to keep its own value must not self-conflict.
1330        g.update_edge(e2, &json!({"toll_id": 2})).unwrap();
1331    }
1332
1333    #[test]
1334    fn update_edge_enforces_required_constraint() {
1335        let (_dir, g) = open_tmp();
1336        let a = g.add_node("N", &()).unwrap();
1337        let b = g.add_node("N", &()).unwrap();
1338        g.create_edge_required_constraint("ROAD", "cost").unwrap();
1339        let eid = g.add_edge(a, b, "ROAD", &json!({"cost": 5})).unwrap();
1340
1341        let err = g.update_edge(eid, &json!({})).unwrap_err();
1342        assert!(matches!(err, Error::RequiredConstraintViolation(..)));
1343    }
1344}
1345
1346#[cfg(test)]
1347mod label_filter_tests {
1348    use serde_json::json;
1349    use tempfile::TempDir;
1350
1351    use crate::Graph;
1352
1353    fn open_tmp() -> (TempDir, Graph) {
1354        let dir = TempDir::new().unwrap();
1355        let g = Graph::open(dir.path(), 1).unwrap();
1356        (dir, g)
1357    }
1358
1359    #[test]
1360    fn label_filter_keeps_only_labeled_nodes() {
1361        let (_dir, g) = open_tmp();
1362        let a = g.add_node("Person", &json!({})).unwrap();
1363        let b = g.add_node("City", &json!({})).unwrap();
1364        let c = g.add_node_multi(&["City", "Person"], &json!({})).unwrap();
1365
1366        let filtered = g.label_filter(&[a, b, c], "Person").unwrap();
1367        assert_eq!(filtered.len(), 2);
1368        assert!(filtered.contains(&a));
1369        assert!(filtered.contains(&c));
1370    }
1371
1372    #[test]
1373    fn label_filter_unknown_label_is_empty() {
1374        let (_dir, g) = open_tmp();
1375        let a = g.add_node("Person", &json!({})).unwrap();
1376        assert!(g.label_filter(&[a], "Ghost").unwrap().is_empty());
1377    }
1378
1379    #[test]
1380    fn label_filter_sees_committed_writes_immediately() {
1381        let (_dir, g) = open_tmp();
1382        let a = g.add_node("Person", &json!({})).unwrap();
1383        g.add_label(a, "Admin").unwrap();
1384        assert_eq!(g.label_filter(&[a], "Admin").unwrap(), vec![a]);
1385        g.remove_label(a, "Admin").unwrap();
1386        assert!(g.label_filter(&[a], "Admin").unwrap().is_empty());
1387    }
1388}