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
695        // A value that cannot be index-encoded (currently only a string longer
696        // than `MAX_INDEXED_STRING_LEN`) is absent from `node_prop_idx`, and its
697        // property may have no `prop_key` registered at all, so the index path
698        // below would wrongly report no matches. Fall back to a label scan that
699        // compares the stored value directly. This must precede the `label_id`
700        // and `prop_key_id` lookups, which short-circuit to an empty result.
701        let encoded = match encode_property_value(&val) {
702            Some(e) => e,
703            None => return self.scan_label_for_property_eq(rtxn, label, property, &val),
704        };
705
706        let label_key = format!("label:{label}");
707        let label_id = match self.storage.meta.get(rtxn, &label_key)? {
708            Some(b) => {
709                let arr: [u8; 4] = b
710                    .try_into()
711                    .map_err(|_| Error::Corrupt("label id must be 4 bytes"))?;
712                u32::from_be_bytes(arr)
713            }
714            None => return Ok(Vec::new()),
715        };
716
717        let prop_key = format!("prop_key:{property}");
718        let prop_key_id = match self.storage.meta.get(rtxn, &prop_key)? {
719            Some(b) => {
720                let arr: [u8; 4] = b
721                    .try_into()
722                    .map_err(|_| Error::Corrupt("prop key id must be 4 bytes"))?;
723                u32::from_be_bytes(arr)
724            }
725            None => return Ok(Vec::new()),
726        };
727
728        let mut prefix = Vec::with_capacity(4 + 4 + encoded.len());
729        prefix.extend_from_slice(&label_id.to_be_bytes());
730        prefix.extend_from_slice(&prop_key_id.to_be_bytes());
731        prefix.extend_from_slice(&encoded);
732
733        let mut result = Vec::new();
734        for entry in self.storage.node_prop_idx.prefix_iter(rtxn, &prefix)? {
735            let (key, _) = entry?;
736            if key.len() >= 8 {
737                let mut node_id_bytes = [0u8; 8];
738                node_id_bytes.copy_from_slice(&key[key.len() - 8..]);
739                result.push(u64::from_be_bytes(node_id_bytes));
740            }
741        }
742        Ok(result)
743    }
744
745    /// Equality lookup fallback for a node property whose value is not present
746    /// in `node_prop_idx` (an over-long string). Scans the label and compares
747    /// the stored property value directly, preserving ascending ID order.
748    fn scan_label_for_property_eq(
749        &self,
750        rtxn: &heed::RoTxn,
751        label: &str,
752        property: &str,
753        val: &serde_json::Value,
754    ) -> Result<Vec<NodeId>, Error> {
755        let mut result = Vec::new();
756        for id in self.nodes_by_label_impl(rtxn, label)? {
757            if let Some(record) = self.get_node_impl(rtxn, id)? {
758                let props: serde_json::Value = props::decode(&record.props)?;
759                if props.get(property) == Some(val) {
760                    result.push(id);
761                }
762            }
763        }
764        Ok(result)
765    }
766
767    pub fn nodes_by_property_range(
768        &self,
769        label: &str,
770        property: &str,
771        min_val: Option<PropValue>,
772        min_inclusive: bool,
773        max_val: Option<PropValue>,
774        max_inclusive: bool,
775    ) -> Result<Vec<NodeId>, Error> {
776        let rtxn = self.storage.env.read_txn()?;
777        self.nodes_by_property_range_impl(
778            &rtxn,
779            label,
780            property,
781            min_val,
782            min_inclusive,
783            max_val,
784            max_inclusive,
785        )
786    }
787
788    #[allow(clippy::too_many_arguments)]
789    pub(super) fn nodes_by_property_range_impl(
790        &self,
791        rtxn: &heed::RoTxn,
792        label: &str,
793        property: &str,
794        min_val: Option<PropValue>,
795        min_inclusive: bool,
796        max_val: Option<PropValue>,
797        max_inclusive: bool,
798    ) -> Result<Vec<NodeId>, Error> {
799        let label_key = format!("label:{label}");
800        let label_id = match self.storage.meta.get(rtxn, &label_key)? {
801            Some(b) => {
802                let arr: [u8; 4] = b
803                    .try_into()
804                    .map_err(|_| Error::Corrupt("label id must be 4 bytes"))?;
805                u32::from_be_bytes(arr)
806            }
807            None => return Ok(Vec::new()),
808        };
809
810        let prop_key = format!("prop_key:{property}");
811        let prop_key_id = match self.storage.meta.get(rtxn, &prop_key)? {
812            Some(b) => {
813                let arr: [u8; 4] = b
814                    .try_into()
815                    .map_err(|_| Error::Corrupt("prop key id must be 4 bytes"))?;
816                u32::from_be_bytes(arr)
817            }
818            None => return Ok(Vec::new()),
819        };
820
821        let mut prefix = Vec::with_capacity(8);
822        prefix.extend_from_slice(&label_id.to_be_bytes());
823        prefix.extend_from_slice(&prop_key_id.to_be_bytes());
824
825        let min_encoded = min_val
826            .map(|v| v.into_json())
827            .as_ref()
828            .and_then(encode_property_value);
829        let max_encoded = max_val
830            .map(|v| v.into_json())
831            .as_ref()
832            .and_then(encode_property_value);
833
834        let mut result = Vec::new();
835        for entry in self.storage.node_prop_idx.prefix_iter(rtxn, &prefix)? {
836            let (key, _) = entry?;
837            if key.len() >= prefix.len() + 8 {
838                let val_bytes = &key[prefix.len()..key.len() - 8];
839
840                if let Some(ref min_enc) = min_encoded {
841                    if min_inclusive {
842                        if val_bytes < min_enc.as_slice() {
843                            continue;
844                        }
845                    } else if val_bytes <= min_enc.as_slice() {
846                        continue;
847                    }
848                }
849                if let Some(ref max_enc) = max_encoded {
850                    if max_inclusive {
851                        if val_bytes > max_enc.as_slice() {
852                            continue;
853                        }
854                    } else if val_bytes >= max_enc.as_slice() {
855                        continue;
856                    }
857                }
858
859                let mut node_id_bytes = [0u8; 8];
860                node_id_bytes.copy_from_slice(&key[key.len() - 8..]);
861                result.push(u64::from_be_bytes(node_id_bytes));
862            }
863        }
864        Ok(result)
865    }
866
867    pub fn has_node_property_index(&self, label: &str, property: &str) -> Result<bool, Error> {
868        let rtxn = self.storage.env.read_txn()?;
869        self.has_node_property_index_impl(&rtxn, label, property)
870    }
871
872    pub(super) fn has_node_property_index_impl(
873        &self,
874        rtxn: &heed::RoTxn,
875        label: &str,
876        property: &str,
877    ) -> Result<bool, Error> {
878        let label_key = format!("label:{label}");
879        let label_id = match self.storage.meta.get(rtxn, &label_key)? {
880            Some(b) => {
881                let arr: [u8; 4] = b
882                    .try_into()
883                    .map_err(|_| Error::Corrupt("label id must be 4 bytes"))?;
884                u32::from_be_bytes(arr)
885            }
886            None => return Ok(false),
887        };
888
889        let prop_key = format!("prop_key:{property}");
890        let prop_key_id = match self.storage.meta.get(rtxn, &prop_key)? {
891            Some(b) => {
892                let arr: [u8; 4] = b
893                    .try_into()
894                    .map_err(|_| Error::Corrupt("prop key id must be 4 bytes"))?;
895                u32::from_be_bytes(arr)
896            }
897            None => return Ok(false),
898        };
899
900        // Use a prefix seek on node_prop_idx: if any entry exists for this
901        // label+property combination the auto-index (or a user-created index)
902        // has data, so the optimizer may use NodeIndexScan.
903        let mut prefix = Vec::with_capacity(8);
904        prefix.extend_from_slice(&label_id.to_be_bytes());
905        prefix.extend_from_slice(&prop_key_id.to_be_bytes());
906        let mut iter = self.storage.node_prop_idx.prefix_iter(rtxn, &prefix)?;
907        Ok(iter.next().is_some())
908    }
909
910    pub fn edges_by_property(
911        &self,
912        etype: &str,
913        property: &str,
914        val: PropValue,
915    ) -> Result<Vec<EdgeId>, Error> {
916        let rtxn = self.storage.env.read_txn()?;
917        self.edges_by_property_impl(&rtxn, etype, property, val)
918    }
919
920    pub(super) fn edges_by_property_impl(
921        &self,
922        rtxn: &heed::RoTxn,
923        etype: &str,
924        property: &str,
925        val: PropValue,
926    ) -> Result<Vec<EdgeId>, Error> {
927        let val = val.into_json();
928
929        // See `nodes_by_property_impl`: an unindexable value (long string) is
930        // absent from `edge_prop_idx` and may have no registered `prop_key`, so
931        // fall back to a type scan before the short-circuiting meta lookups.
932        let encoded = match encode_property_value(&val) {
933            Some(e) => e,
934            None => return self.scan_type_for_property_eq(rtxn, etype, property, &val),
935        };
936
937        let type_key = format!("type:{etype}");
938        let type_id = match self.storage.meta.get(rtxn, &type_key)? {
939            Some(b) => {
940                let arr: [u8; 4] = b
941                    .try_into()
942                    .map_err(|_| Error::Corrupt("type id must be 4 bytes"))?;
943                u32::from_be_bytes(arr)
944            }
945            None => return Ok(Vec::new()),
946        };
947
948        let prop_key = format!("prop_key:{property}");
949        let prop_key_id = match self.storage.meta.get(rtxn, &prop_key)? {
950            Some(b) => {
951                let arr: [u8; 4] = b
952                    .try_into()
953                    .map_err(|_| Error::Corrupt("prop key id must be 4 bytes"))?;
954                u32::from_be_bytes(arr)
955            }
956            None => return Ok(Vec::new()),
957        };
958
959        let mut prefix = Vec::with_capacity(4 + 4 + encoded.len());
960        prefix.extend_from_slice(&type_id.to_be_bytes());
961        prefix.extend_from_slice(&prop_key_id.to_be_bytes());
962        prefix.extend_from_slice(&encoded);
963
964        let mut result = Vec::new();
965        for entry in self.storage.edge_prop_idx.prefix_iter(rtxn, &prefix)? {
966            let (key, _) = entry?;
967            if key.len() >= 8 {
968                let mut edge_id_bytes = [0u8; 8];
969                edge_id_bytes.copy_from_slice(&key[key.len() - 8..]);
970                result.push(u64::from_be_bytes(edge_id_bytes));
971            }
972        }
973        Ok(result)
974    }
975
976    /// Equality lookup fallback for an edge property whose value is not present
977    /// in `edge_prop_idx` (an over-long string). Scans the type and compares the
978    /// stored property value directly, preserving ascending ID order.
979    fn scan_type_for_property_eq(
980        &self,
981        rtxn: &heed::RoTxn,
982        etype: &str,
983        property: &str,
984        val: &serde_json::Value,
985    ) -> Result<Vec<EdgeId>, Error> {
986        let mut result = Vec::new();
987        for id in self.edges_by_type_impl(rtxn, etype)? {
988            if let Some(record) = self.get_edge_impl(rtxn, id)? {
989                let props: serde_json::Value = props::decode(&record.props)?;
990                if props.get(property) == Some(val) {
991                    result.push(id);
992                }
993            }
994        }
995        Ok(result)
996    }
997
998    pub fn edges_by_property_range(
999        &self,
1000        etype: &str,
1001        property: &str,
1002        min_val: Option<PropValue>,
1003        max_val: Option<PropValue>,
1004    ) -> Result<Vec<EdgeId>, Error> {
1005        let rtxn = self.storage.env.read_txn()?;
1006        self.edges_by_property_range_impl(&rtxn, etype, property, min_val, max_val)
1007    }
1008
1009    pub(super) fn edges_by_property_range_impl(
1010        &self,
1011        rtxn: &heed::RoTxn,
1012        etype: &str,
1013        property: &str,
1014        min_val: Option<PropValue>,
1015        max_val: Option<PropValue>,
1016    ) -> Result<Vec<EdgeId>, Error> {
1017        let type_key = format!("type:{etype}");
1018        let type_id = match self.storage.meta.get(rtxn, &type_key)? {
1019            Some(b) => {
1020                let arr: [u8; 4] = b
1021                    .try_into()
1022                    .map_err(|_| Error::Corrupt("type id must be 4 bytes"))?;
1023                u32::from_be_bytes(arr)
1024            }
1025            None => return Ok(Vec::new()),
1026        };
1027
1028        let prop_key = format!("prop_key:{property}");
1029        let prop_key_id = match self.storage.meta.get(rtxn, &prop_key)? {
1030            Some(b) => {
1031                let arr: [u8; 4] = b
1032                    .try_into()
1033                    .map_err(|_| Error::Corrupt("prop key id must be 4 bytes"))?;
1034                u32::from_be_bytes(arr)
1035            }
1036            None => return Ok(Vec::new()),
1037        };
1038
1039        let mut prefix = Vec::with_capacity(8);
1040        prefix.extend_from_slice(&type_id.to_be_bytes());
1041        prefix.extend_from_slice(&prop_key_id.to_be_bytes());
1042
1043        let min_encoded = min_val
1044            .map(|v| v.into_json())
1045            .as_ref()
1046            .and_then(encode_property_value);
1047        let max_encoded = max_val
1048            .map(|v| v.into_json())
1049            .as_ref()
1050            .and_then(encode_property_value);
1051
1052        let mut result = Vec::new();
1053        for entry in self.storage.edge_prop_idx.prefix_iter(rtxn, &prefix)? {
1054            let (key, _) = entry?;
1055            if key.len() >= prefix.len() + 8 {
1056                let val_bytes = &key[prefix.len()..key.len() - 8];
1057
1058                if let Some(ref min_enc) = min_encoded {
1059                    if val_bytes < min_enc.as_slice() {
1060                        continue;
1061                    }
1062                }
1063                if let Some(ref max_enc) = max_encoded {
1064                    if val_bytes > max_enc.as_slice() {
1065                        continue;
1066                    }
1067                }
1068
1069                let mut edge_id_bytes = [0u8; 8];
1070                edge_id_bytes.copy_from_slice(&key[key.len() - 8..]);
1071                result.push(u64::from_be_bytes(edge_id_bytes));
1072            }
1073        }
1074        Ok(result)
1075    }
1076
1077    pub fn list_node_indexes_and_constraints(&self) -> Result<Vec<(String, String, u8)>, Error> {
1078        let rtxn = self.storage.env.read_txn()?;
1079        let mut result = Vec::new();
1080        for entry in self.storage.meta.iter(&rtxn)? {
1081            let (key, val) = entry?;
1082            if let Some(rest) = key.strip_prefix("idx_meta:node:l:") {
1083                let parts: Vec<&str> = rest.split(":p:").collect();
1084                if parts.len() == 2 {
1085                    if let (Ok(label_id), Ok(prop_key_id)) =
1086                        (parts[0].parse::<u32>(), parts[1].parse::<u32>())
1087                    {
1088                        if let (Some(label_name), Some(prop_name)) = (
1089                            self.label_name_impl(&rtxn, label_id)?,
1090                            crate::storage::ids::get_prop_key_name(
1091                                &self.storage,
1092                                &rtxn,
1093                                prop_key_id,
1094                            )?,
1095                        ) {
1096                            let flags = val.first().copied().unwrap_or(0x00);
1097                            result.push((label_name, prop_name, flags));
1098                        }
1099                    }
1100                }
1101            }
1102        }
1103        Ok(result)
1104    }
1105
1106    pub fn list_edge_indexes_and_constraints(&self) -> Result<Vec<(String, String, u8)>, Error> {
1107        let rtxn = self.storage.env.read_txn()?;
1108        let mut result = Vec::new();
1109        for entry in self.storage.meta.iter(&rtxn)? {
1110            let (key, val) = entry?;
1111            if let Some(rest) = key.strip_prefix("idx_meta:edge:t:") {
1112                let parts: Vec<&str> = rest.split(":p:").collect();
1113                if parts.len() == 2 {
1114                    if let (Ok(type_id), Ok(prop_key_id)) =
1115                        (parts[0].parse::<u32>(), parts[1].parse::<u32>())
1116                    {
1117                        if let (Some(type_name), Some(prop_name)) = (
1118                            self.type_name_impl(&rtxn, type_id)?,
1119                            crate::storage::ids::get_prop_key_name(
1120                                &self.storage,
1121                                &rtxn,
1122                                prop_key_id,
1123                            )?,
1124                        ) {
1125                            let flags = val.first().copied().unwrap_or(0x00);
1126                            result.push((type_name, prop_name, flags));
1127                        }
1128                    }
1129                }
1130            }
1131        }
1132        Ok(result)
1133    }
1134}
1135
1136#[cfg(test)]
1137mod tests {
1138    use serde_json::json;
1139    use tempfile::TempDir;
1140
1141    use super::*;
1142
1143    fn open_tmp() -> (TempDir, Graph) {
1144        let dir = TempDir::new().unwrap();
1145        let g = Graph::open(dir.path(), 1).unwrap();
1146        (dir, g)
1147    }
1148
1149    /// Dropping an explicit property index must leave the always-on auto-index
1150    /// intact so `nodes_by_property` still finds existing nodes.
1151    #[test]
1152    fn drop_index_preserves_auto_index() {
1153        let (_dir, g) = open_tmp();
1154        let id = g.add_node("Person", &json!({"age": 30})).unwrap();
1155
1156        g.create_node_property_index("Person", "age").unwrap();
1157        g.drop_node_property_index("Person", "age").unwrap();
1158
1159        assert_eq!(
1160            g.nodes_by_property("Person", "age", PropValue::Int(30))
1161                .unwrap(),
1162            vec![id],
1163            "auto-index entries must survive dropping the explicit index"
1164        );
1165    }
1166
1167    /// A string property too long to fit an LMDB index key is left unindexed,
1168    /// so an equality lookup must fall back to a label scan rather than wrongly
1169    /// reporting no matches.
1170    #[test]
1171    fn nodes_by_property_finds_unindexed_long_string() {
1172        let (_dir, g) = open_tmp();
1173        let long = "word ".repeat(4000); // ~20 KB, well over the index key bound
1174        let id = g
1175            .add_node("Post", &json!({ "body": long.clone() }))
1176            .unwrap();
1177        // A different long body must not match.
1178        g.add_node("Post", &json!({ "body": "other ".repeat(4000) }))
1179            .unwrap();
1180
1181        assert_eq!(
1182            g.nodes_by_property("Post", "body", PropValue::Str(long))
1183                .unwrap(),
1184            vec![id],
1185            "equality lookup on an unindexed long string must scan and match"
1186        );
1187    }
1188
1189    /// Dropping a unique constraint must keep property lookups working and stop
1190    /// enforcing uniqueness.
1191    #[test]
1192    fn drop_unique_constraint_preserves_lookups() {
1193        let (_dir, g) = open_tmp();
1194        let id = g.add_node("User", &json!({"email": "a@b.c"})).unwrap();
1195
1196        g.create_node_unique_constraint("User", "email").unwrap();
1197        g.drop_node_unique_constraint("User", "email").unwrap();
1198
1199        assert_eq!(
1200            g.nodes_by_property("User", "email", PropValue::Str("a@b.c".into()))
1201                .unwrap(),
1202            vec![id]
1203        );
1204
1205        // Uniqueness is no longer enforced; a duplicate value is accepted and
1206        // both nodes are findable.
1207        let id2 = g.add_node("User", &json!({"email": "a@b.c"})).unwrap();
1208        let mut hits = g
1209            .nodes_by_property("User", "email", PropValue::Str("a@b.c".into()))
1210            .unwrap();
1211        hits.sort();
1212        let mut expected = vec![id, id2];
1213        expected.sort();
1214        assert_eq!(hits, expected);
1215    }
1216
1217    /// Two nodes with integer properties beyond 2^53 must be distinguishable by
1218    /// `nodes_by_property`; the values previously collapsed through `f64`.
1219    #[test]
1220    fn large_integer_property_no_false_match() {
1221        let (_dir, g) = open_tmp();
1222        let a = g
1223            .add_node("Item", &json!({"sid": 9_007_199_254_740_992_i64}))
1224            .unwrap();
1225        let b = g
1226            .add_node("Item", &json!({"sid": 9_007_199_254_740_993_i64}))
1227            .unwrap();
1228
1229        assert_eq!(
1230            g.nodes_by_property("Item", "sid", PropValue::Int(9_007_199_254_740_992))
1231                .unwrap(),
1232            vec![a]
1233        );
1234        assert_eq!(
1235            g.nodes_by_property("Item", "sid", PropValue::Int(9_007_199_254_740_993))
1236                .unwrap(),
1237            vec![b]
1238        );
1239    }
1240
1241    /// An integer-valued property must still be findable when queried with the
1242    /// equal float, matching Cypher's `30 = 30.0` semantics.
1243    #[test]
1244    fn integer_property_matches_float_query() {
1245        let (_dir, g) = open_tmp();
1246        let id = g.add_node("Person", &json!({"age": 30})).unwrap();
1247        assert_eq!(
1248            g.nodes_by_property("Person", "age", PropValue::Float(30.0))
1249                .unwrap(),
1250            vec![id]
1251        );
1252    }
1253
1254    /// `node_count_hint` is the node-id high-water mark: it tracks allocations
1255    /// and must not decrease when a node is deleted.
1256    #[test]
1257    fn node_count_hint_is_high_water_mark() {
1258        let (_dir, g) = open_tmp();
1259        assert_eq!(g.node_count_hint().unwrap(), 0);
1260
1261        let a = g.add_node("N", &()).unwrap();
1262        g.add_node("N", &()).unwrap();
1263        assert_eq!(g.node_count_hint().unwrap(), 2);
1264
1265        g.delete_node(a).unwrap();
1266        assert_eq!(g.node_count_hint().unwrap(), 2);
1267    }
1268
1269    /// An edge property index created before any edges exist must be populated
1270    /// by `add_edge`, and one created afterwards must backfill existing edges.
1271    #[test]
1272    fn edge_property_index_lookup() {
1273        let (_dir, g) = open_tmp();
1274        let a = g.add_node("N", &()).unwrap();
1275        let b = g.add_node("N", &()).unwrap();
1276
1277        // Backfill path: the edge exists before the index.
1278        let e1 = g.add_edge(a, b, "ROAD", &json!({"cost": 5})).unwrap();
1279        g.create_edge_property_index("ROAD", "cost").unwrap();
1280
1281        // Insert path: the edge arrives after the index.
1282        let e2 = g.add_edge(b, a, "ROAD", &json!({"cost": 7})).unwrap();
1283
1284        assert_eq!(
1285            g.edges_by_property("ROAD", "cost", PropValue::Int(5))
1286                .unwrap(),
1287            vec![e1]
1288        );
1289        assert_eq!(
1290            g.edges_by_property("ROAD", "cost", PropValue::Int(7))
1291                .unwrap(),
1292            vec![e2]
1293        );
1294        assert_eq!(
1295            g.edges_by_property_range(
1296                "ROAD",
1297                "cost",
1298                Some(PropValue::Int(5)),
1299                Some(PropValue::Int(7)),
1300            )
1301            .unwrap(),
1302            vec![e1, e2]
1303        );
1304    }
1305
1306    #[test]
1307    fn drop_edge_property_index_removes_entries() {
1308        let (_dir, g) = open_tmp();
1309        let a = g.add_node("N", &()).unwrap();
1310        let b = g.add_node("N", &()).unwrap();
1311        g.create_edge_property_index("ROAD", "cost").unwrap();
1312        g.add_edge(a, b, "ROAD", &json!({"cost": 5})).unwrap();
1313
1314        g.drop_edge_property_index("ROAD", "cost").unwrap();
1315        assert_eq!(
1316            g.edges_by_property("ROAD", "cost", PropValue::Int(5))
1317                .unwrap(),
1318            Vec::<EdgeId>::new()
1319        );
1320    }
1321
1322    #[test]
1323    fn edge_unique_constraint_rejects_duplicate_insert() {
1324        let (_dir, g) = open_tmp();
1325        let a = g.add_node("N", &()).unwrap();
1326        let b = g.add_node("N", &()).unwrap();
1327        g.create_edge_unique_constraint("ROAD", "toll_id").unwrap();
1328
1329        g.add_edge(a, b, "ROAD", &json!({"toll_id": 1})).unwrap();
1330        let err = g
1331            .add_edge(b, a, "ROAD", &json!({"toll_id": 1}))
1332            .unwrap_err();
1333        assert!(matches!(err, Error::UniqueConstraintViolation(..)));
1334    }
1335
1336    #[test]
1337    fn edge_unique_constraint_rejects_existing_duplicates() {
1338        let (_dir, g) = open_tmp();
1339        let a = g.add_node("N", &()).unwrap();
1340        let b = g.add_node("N", &()).unwrap();
1341        g.add_edge(a, b, "ROAD", &json!({"toll_id": 1})).unwrap();
1342        g.add_edge(b, a, "ROAD", &json!({"toll_id": 1})).unwrap();
1343
1344        let err = g
1345            .create_edge_unique_constraint("ROAD", "toll_id")
1346            .unwrap_err();
1347        assert!(matches!(err, Error::UniqueConstraintViolation(..)));
1348    }
1349
1350    #[test]
1351    fn edge_required_constraint_rejects_missing_property() {
1352        let (_dir, g) = open_tmp();
1353        let a = g.add_node("N", &()).unwrap();
1354        let b = g.add_node("N", &()).unwrap();
1355        g.create_edge_required_constraint("ROAD", "cost").unwrap();
1356
1357        let err = g.add_edge(a, b, "ROAD", &json!({})).unwrap_err();
1358        assert!(matches!(err, Error::RequiredConstraintViolation(..)));
1359
1360        // Creating the constraint must also reject pre-existing violations.
1361        g.add_edge(a, b, "RAIL", &json!({})).unwrap();
1362        let err = g
1363            .create_edge_required_constraint("RAIL", "cost")
1364            .unwrap_err();
1365        assert!(matches!(err, Error::RequiredConstraintViolation(..)));
1366    }
1367
1368    /// `update_edge` must re-index the edge under its new property values:
1369    /// the old index entry disappears and the new one is findable.
1370    #[test]
1371    fn update_edge_reindexes_edge_properties() {
1372        let (_dir, g) = open_tmp();
1373        let a = g.add_node("N", &()).unwrap();
1374        let b = g.add_node("N", &()).unwrap();
1375        g.create_edge_property_index("ROAD", "cost").unwrap();
1376        let eid = g.add_edge(a, b, "ROAD", &json!({"cost": 5})).unwrap();
1377
1378        g.update_edge(eid, &json!({"cost": 7})).unwrap();
1379
1380        assert_eq!(
1381            g.edges_by_property("ROAD", "cost", PropValue::Int(5))
1382                .unwrap(),
1383            Vec::<EdgeId>::new(),
1384            "stale index entry must be removed"
1385        );
1386        assert_eq!(
1387            g.edges_by_property("ROAD", "cost", PropValue::Int(7))
1388                .unwrap(),
1389            vec![eid],
1390            "new value must be indexed"
1391        );
1392    }
1393
1394    #[test]
1395    fn update_edge_enforces_unique_constraint() {
1396        let (_dir, g) = open_tmp();
1397        let a = g.add_node("N", &()).unwrap();
1398        let b = g.add_node("N", &()).unwrap();
1399        g.create_edge_unique_constraint("ROAD", "toll_id").unwrap();
1400        g.add_edge(a, b, "ROAD", &json!({"toll_id": 1})).unwrap();
1401        let e2 = g.add_edge(b, a, "ROAD", &json!({"toll_id": 2})).unwrap();
1402
1403        let err = g.update_edge(e2, &json!({"toll_id": 1})).unwrap_err();
1404        assert!(matches!(err, Error::UniqueConstraintViolation(..)));
1405
1406        // Updating an edge to keep its own value must not self-conflict.
1407        g.update_edge(e2, &json!({"toll_id": 2})).unwrap();
1408    }
1409
1410    #[test]
1411    fn update_edge_enforces_required_constraint() {
1412        let (_dir, g) = open_tmp();
1413        let a = g.add_node("N", &()).unwrap();
1414        let b = g.add_node("N", &()).unwrap();
1415        g.create_edge_required_constraint("ROAD", "cost").unwrap();
1416        let eid = g.add_edge(a, b, "ROAD", &json!({"cost": 5})).unwrap();
1417
1418        let err = g.update_edge(eid, &json!({})).unwrap_err();
1419        assert!(matches!(err, Error::RequiredConstraintViolation(..)));
1420    }
1421}
1422
1423#[cfg(test)]
1424mod label_filter_tests {
1425    use serde_json::json;
1426    use tempfile::TempDir;
1427
1428    use crate::Graph;
1429
1430    fn open_tmp() -> (TempDir, Graph) {
1431        let dir = TempDir::new().unwrap();
1432        let g = Graph::open(dir.path(), 1).unwrap();
1433        (dir, g)
1434    }
1435
1436    #[test]
1437    fn label_filter_keeps_only_labeled_nodes() {
1438        let (_dir, g) = open_tmp();
1439        let a = g.add_node("Person", &json!({})).unwrap();
1440        let b = g.add_node("City", &json!({})).unwrap();
1441        let c = g.add_node_multi(&["City", "Person"], &json!({})).unwrap();
1442
1443        let filtered = g.label_filter(&[a, b, c], "Person").unwrap();
1444        assert_eq!(filtered.len(), 2);
1445        assert!(filtered.contains(&a));
1446        assert!(filtered.contains(&c));
1447    }
1448
1449    #[test]
1450    fn label_filter_unknown_label_is_empty() {
1451        let (_dir, g) = open_tmp();
1452        let a = g.add_node("Person", &json!({})).unwrap();
1453        assert!(g.label_filter(&[a], "Ghost").unwrap().is_empty());
1454    }
1455
1456    #[test]
1457    fn label_filter_sees_committed_writes_immediately() {
1458        let (_dir, g) = open_tmp();
1459        let a = g.add_node("Person", &json!({})).unwrap();
1460        g.add_label(a, "Admin").unwrap();
1461        assert_eq!(g.label_filter(&[a], "Admin").unwrap(), vec![a]);
1462        g.remove_label(a, "Admin").unwrap();
1463        assert!(g.label_filter(&[a], "Admin").unwrap().is_empty());
1464    }
1465}