Skip to main content

oxgraph_db/database/
reader.rs

1//! Read transactions: a pinned snapshot and the full read/query surface.
2
3use std::{borrow::Cow, collections::BTreeSet, sync::Arc};
4
5use oxgraph_algo::{
6    PageRankConfig, PageRankError, PageRankWorkspace, Uniform, longest_path_dag,
7    pagerank_graph_with_workspace,
8};
9use oxgraph_graph::{CanonicalElementIdentity, DenseElementIndex, LocalElementIdentity};
10
11use super::IndexProbe;
12use crate::{
13    Catalog, CheckpointGeneration, CommitSeq, DbError, Element, ElementId, IncidenceId,
14    IncidenceRecord, IndexId, PreparedQuery, ProjectionDefinition, ProjectionId, Properties,
15    PropertyKeyId, PropertySubject, PropertyValue, QueryResult, Relation, RelationId,
16    RelationTypeId,
17    catalog::IndexDefinition,
18    overlay::{Snapshot, StateView},
19    projection::{self, GraphProjection, HypergraphProjection, ProjectionElementId},
20    traversal::{self, Direction, Subgraph, Walk},
21    typed::{Assignable, EqualityIndex, ValueType},
22};
23
24/// Reader pin identifying the visible database generation.
25///
26/// # Performance
27///
28/// Copying and comparing a pin is `O(1)`.
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub struct ReadPin {
31    /// Pinned visible commit sequence.
32    pub visible_commit_seq: CommitSeq,
33    /// Pinned checkpoint generation.
34    pub generation: CheckpointGeneration,
35}
36
37/// Read transaction over a pinned snapshot.
38///
39/// A read transaction owns its own `Arc<Snapshot>` and never borrows the
40/// [`Db`], so it stays valid across a later `begin_write`/`checkpoint` on
41/// the same handle (it cloned the snapshot before the write borrowed `&mut`). It
42/// is [`Send`] + [`Sync`] (asserted below).
43///
44/// # Performance
45///
46/// Creating and cloning a read transaction is `O(1)`: it shares the pinned
47/// snapshot through an `Arc`, not by copying.
48pub struct Reader {
49    /// The pinned snapshot this reader observes.
50    pub(super) snapshot: Arc<Snapshot>,
51}
52
53/// Returns whether a [`Reader::neighbors`] walk should follow the edge from the
54/// incidence `from` (the queried element's incidence) to the incidence `to`
55/// (a candidate neighbor's incidence) under `direction`.
56///
57/// Endpoint roles are encoded by incidence-creation order: the source endpoint
58/// has the lower incidence id. `Outgoing` follows source→target (the queried
59/// element is the source, so `from < to`), `Incoming` follows target→source, and
60/// `Both` follows either side.
61///
62/// # Performance
63///
64/// This function is `O(1)`.
65const fn follow_direction(direction: Direction, from: IncidenceId, to: IncidenceId) -> bool {
66    match direction {
67        Direction::Outgoing => from.get() < to.get(),
68        Direction::Incoming => from.get() > to.get(),
69        Direction::Both => true,
70    }
71}
72
73/// `Reader` MUST be `Send + Sync`: it pins only an `Arc<Snapshot>`,
74/// which holds `Arc`-shared `Send + Sync` data (no `Rc`/`RefCell` reachable).
75const fn assert_send_sync<T: Send + Sync>() {}
76const _: () = assert_send_sync::<Reader>();
77const _: () = assert_send_sync::<Arc<Snapshot>>();
78
79impl Reader {
80    /// Returns this transaction's reader pin.
81    ///
82    /// # Performance
83    ///
84    /// This method is `O(1)`.
85    #[must_use]
86    pub fn pin(&self) -> ReadPin {
87        ReadPin {
88            visible_commit_seq: self.snapshot.lsn(),
89            generation: self.snapshot.generation(),
90        }
91    }
92
93    /// Returns catalog metadata.
94    ///
95    /// # Performance
96    ///
97    /// This method is `O(1)`.
98    #[must_use]
99    pub fn catalog(&self) -> &Catalog {
100        self.snapshot.view().catalog_ref()
101    }
102
103    /// Returns visible element count.
104    ///
105    /// # Performance
106    ///
107    /// This method is `O(base + overlay change)`.
108    #[must_use]
109    pub fn element_count(&self) -> usize {
110        self.snapshot.view().element_count()
111    }
112
113    /// Returns visible relation count.
114    ///
115    /// # Performance
116    ///
117    /// This method is `O(base + overlay change)`.
118    #[must_use]
119    pub fn relation_count(&self) -> usize {
120        self.snapshot.view().relation_count()
121    }
122
123    /// Returns visible incidence count.
124    ///
125    /// # Performance
126    ///
127    /// This method is `O(base + overlay change)`.
128    #[must_use]
129    pub fn incidence_count(&self) -> usize {
130        self.snapshot.view().incidence_count()
131    }
132
133    /// Returns every visible element id in id order.
134    ///
135    /// # Performance
136    ///
137    /// This method is `O(element count)`.
138    #[must_use]
139    pub fn element_ids(&self) -> Vec<ElementId> {
140        self.snapshot
141            .view()
142            .elements()
143            .map(|record| record.id)
144            .collect()
145    }
146
147    /// Returns every visible relation id in id order.
148    ///
149    /// # Performance
150    ///
151    /// This method is `O(relation count)`.
152    #[must_use]
153    pub fn relation_ids(&self) -> Vec<RelationId> {
154        self.snapshot
155            .view()
156            .relations()
157            .map(|record| record.id)
158            .collect()
159    }
160
161    /// Returns whether an element exists.
162    ///
163    /// # Performance
164    ///
165    /// This method is `O(log change + log n)`.
166    #[must_use]
167    pub fn contains_element(&self, id: ElementId) -> bool {
168        self.snapshot.view().contains_element(id)
169    }
170
171    /// Returns whether a relation exists.
172    ///
173    /// # Performance
174    ///
175    /// This method is `O(log change + log n)`.
176    #[must_use]
177    pub fn contains_relation(&self, id: RelationId) -> bool {
178        self.snapshot.view().contains_relation(id)
179    }
180
181    /// Returns whether an incidence exists.
182    ///
183    /// # Performance
184    ///
185    /// This method is `O(log change + log n)`.
186    #[must_use]
187    pub fn contains_incidence(&self, id: IncidenceId) -> bool {
188        self.snapshot.view().contains_incidence(id)
189    }
190
191    /// Returns an owned element view — id, labels, and all properties read in one
192    /// call.
193    ///
194    /// # Performance
195    ///
196    /// This method is `O(log n + label count + property count)`.
197    #[must_use]
198    pub fn element(&self, id: ElementId) -> Option<Element> {
199        let view = self.snapshot.view();
200        let record = view.element_ref(id)?;
201        let labels = record.labels.iter().collect();
202        let properties =
203            Properties::from_pairs(view.subject_properties(PropertySubject::Element(id)));
204        Some(Element::new(id, labels, properties))
205    }
206
207    /// Returns an owned relation view — id, type, labels, and all properties read
208    /// in one call.
209    ///
210    /// # Performance
211    ///
212    /// This method is `O(log n + label count + property count)`.
213    #[must_use]
214    pub fn relation(&self, id: RelationId) -> Option<Relation> {
215        let view = self.snapshot.view();
216        let record = view.relation_ref(id)?;
217        let labels = record.labels.iter().collect();
218        let properties =
219            Properties::from_pairs(view.subject_properties(PropertySubject::Relation(id)));
220        Some(Relation::new(id, record.relation_type, labels, properties))
221    }
222
223    /// Returns an owned incidence record.
224    ///
225    /// # Performance
226    ///
227    /// This method is `O(log change + log n)`.
228    #[must_use]
229    pub fn incidence(&self, id: IncidenceId) -> Option<IncidenceRecord> {
230        self.snapshot.view().incidence_ref(id).map(Cow::into_owned)
231    }
232
233    /// Returns every visible incidence attached to an element, in ascending
234    /// incidence-id order.
235    ///
236    /// The merged set mixes overlay-owned and base-borrowed records, so this
237    /// returns an owned [`Vec`] ([`IncidenceRecord`] is [`Copy`], so the copy is
238    /// cheap).
239    ///
240    /// # Performance
241    ///
242    /// This method is `O(base incidences + overlay incidence change)`.
243    #[must_use]
244    pub fn element_incidences(&self, id: ElementId) -> Vec<IncidenceRecord> {
245        self.snapshot.view().element_incidences(id)
246    }
247
248    /// Returns a binary relation's two endpoint elements, ordered by ascending
249    /// incidence id.
250    ///
251    /// Reads the relation's incidences from the reverse-adjacency index and
252    /// returns the elements carried by its first two incidences in id order. A
253    /// relation with fewer than two visible incidences returns `None`. This
254    /// reports endpoints structurally, without consulting any projection's
255    /// source/target roles — use [`Self::neighbors`] when role direction matters.
256    ///
257    /// # Performance
258    ///
259    /// This method is `O(degree)` over the relation's incidences.
260    #[must_use]
261    pub fn endpoints(&self, relation: RelationId) -> Option<(ElementId, ElementId)> {
262        let incidences = self.snapshot.view().relation_incidences(relation);
263        match incidences.as_slice() {
264            [first, second, ..] => Some((first.element, second.element)),
265            _too_few => None,
266        }
267    }
268
269    /// Returns the elements reachable from `element` along relations of
270    /// `relation_type`, in ascending element-id order.
271    ///
272    /// Direction selects the role `element` must play on each relation. Endpoint
273    /// roles are encoded by incidence-creation order: a binary relation's source
274    /// is its lower incidence id and its target the higher (see
275    /// [`Self::endpoints`]). `Outgoing` requires `element` to be the source (and
276    /// yields the target), `Incoming` requires it to be the target (and yields
277    /// the source), and `Both` yields the opposite endpoint either way. Resolved
278    /// over the reverse-adjacency index — each incidence of `element` whose
279    /// relation has the requested type contributes that relation's other
280    /// endpoint — so this works for any binary relation without a materialized
281    /// projection.
282    ///
283    /// # Performance
284    ///
285    /// This method is `O(degree of element + sum of touched relation degrees)`.
286    #[must_use]
287    pub fn neighbors(
288        &self,
289        element: ElementId,
290        relation_type: RelationTypeId,
291        direction: Direction,
292    ) -> Vec<ElementId> {
293        let view = self.snapshot.view();
294        let mut neighbors = BTreeSet::new();
295        for incidence in view.element_incidences(element) {
296            let matches_type = view
297                .relation_ref(incidence.relation)
298                .is_some_and(|record| record.relation_type == Some(relation_type));
299            if !matches_type {
300                continue;
301            }
302            // The incidence id encodes the endpoint role: the source endpoint is
303            // created first (lower incidence id), the target second. Compare
304            // `element`'s incidence id against each other endpoint's to decide
305            // which side `element` is on, then follow per the requested direction.
306            neighbors.extend(
307                view.relation_incidences(incidence.relation)
308                    .into_iter()
309                    .filter(|other| other.element != element)
310                    .filter(|other| follow_direction(direction, incidence.id, other.id))
311                    .map(|other| other.element),
312            );
313        }
314        neighbors.into_iter().collect()
315    }
316
317    /// Returns one owned property value.
318    ///
319    /// Prefer [`Self::value`] (borrowed `Cow`) or [`Self::text`] (borrowed
320    /// `&str`) when the value does not need to outlive this reader; owning is
321    /// correct only across commit boundaries.
322    ///
323    /// # Performance
324    ///
325    /// This method is `O(log subjects + log keys)` plus one value clone
326    /// (`O(1)` for every variant — text payloads are `Arc`-shared).
327    #[must_use]
328    pub fn property(&self, subject: PropertySubject, key: PropertyKeyId) -> Option<PropertyValue> {
329        self.snapshot
330            .view()
331            .property_ref(subject, key)
332            .map(Cow::into_owned)
333    }
334
335    /// Returns one property value borrowed from this reader's pinned snapshot:
336    /// `Cow::Borrowed` for any committed value, `Cow::Owned` only when the
337    /// value lives in the published overlay's delta.
338    ///
339    /// # Performance
340    ///
341    /// This method is `O(log subjects + log keys)`; the borrowed arm clones
342    /// nothing.
343    #[must_use]
344    pub fn value(
345        &self,
346        subject: PropertySubject,
347        key: PropertyKeyId,
348    ) -> Option<Cow<'_, PropertyValue>> {
349        self.snapshot.view().property_ref(subject, key)
350    }
351
352    /// Returns one text property's `Arc`-shared payload, or `None` when the
353    /// property is absent or not text.
354    ///
355    /// # Performance
356    ///
357    /// This method is `O(log subjects + log keys)`; the text bytes are never
358    /// copied (the shared payload is reference-counted for both base- and
359    /// overlay-resident values).
360    #[must_use]
361    pub fn text(&self, subject: PropertySubject, key: PropertyKeyId) -> Option<Arc<str>> {
362        match self.snapshot.view().property_ref(subject, key)?.as_ref() {
363            PropertyValue::Text(text) => Some(Arc::clone(text)),
364            _other => None,
365        }
366    }
367
368    /// Returns the owned element whose value in `index` equals `value`, or `None`
369    /// when no element matches.
370    ///
371    /// # Errors
372    ///
373    /// Returns [`DbError`] when the index is unknown or is not an equality index.
374    ///
375    /// # Performance
376    ///
377    /// This method is `O(log n + label count + property count)`.
378    pub fn element_by_key<T: ValueType>(
379        &self,
380        index: EqualityIndex<T>,
381        value: impl Assignable<T>,
382    ) -> Result<Option<Element>, DbError> {
383        let value = value.into_value()?;
384        let matched = self
385            .lookup(index.id(), IndexProbe::Equal(&value))?
386            .into_iter()
387            .find_map(|subject| match subject {
388                PropertySubject::Element(id) => Some(id),
389                PropertySubject::Relation(_) | PropertySubject::Incidence(_) => None,
390            });
391        Ok(matched.and_then(|id| self.element(id)))
392    }
393
394    /// Returns the number of subjects carried by a membership index (a label or
395    /// relation-type index).
396    ///
397    /// # Errors
398    ///
399    /// Returns [`DbError`] when the index is unknown or does not support
400    /// membership enumeration.
401    ///
402    /// # Performance
403    ///
404    /// This method is `O(indexed family size)`.
405    pub fn count(&self, index: IndexId) -> Result<usize, DbError> {
406        self.lookup(index, IndexProbe::All)
407            .map(|subjects| subjects.len())
408    }
409
410    /// Looks up subjects with a property value.
411    ///
412    /// # Errors
413    ///
414    /// Returns [`DbError`] when the property key is unknown or `value` does not
415    /// match the key schema.
416    ///
417    /// # Performance
418    ///
419    /// This method is `O(property subject count)`.
420    pub fn lookup_property_equal(
421        &self,
422        key: PropertyKeyId,
423        value: &PropertyValue,
424    ) -> Result<Vec<PropertySubject>, DbError> {
425        self.snapshot.view().typed_property_equal(key, value)
426    }
427
428    /// Looks up subjects with a property inside an inclusive range.
429    ///
430    /// # Errors
431    ///
432    /// Returns [`DbError`] when the property key is unknown or either bound
433    /// does not match the key schema.
434    ///
435    /// # Performance
436    ///
437    /// This method is `O(property subject count)`.
438    pub fn lookup_property_range(
439        &self,
440        key: PropertyKeyId,
441        min: &PropertyValue,
442        max: &PropertyValue,
443    ) -> Result<Vec<PropertySubject>, DbError> {
444        self.snapshot.view().typed_property_range(key, min, max)
445    }
446
447    /// Executes an index lookup.
448    ///
449    /// # Errors
450    ///
451    /// Returns [`DbError`] when the index is unknown, the lookup shape does not
452    /// match the index kind, or supplied property values do not match catalog
453    /// schemas.
454    ///
455    /// # Performance
456    ///
457    /// This method is `O(indexed family size)`.
458    pub fn lookup(
459        &self,
460        index: IndexId,
461        lookup: IndexProbe<'_>,
462    ) -> Result<Vec<PropertySubject>, DbError> {
463        let view = self.snapshot.view();
464        let entry = view
465            .catalog()
466            .index(index)
467            .ok_or_else(|| DbError::unknown(index))?;
468        match (&entry.definition, lookup) {
469            (IndexDefinition::Label { label }, IndexProbe::All) => Ok(view
470                .elements_with_label(*label)
471                .into_iter()
472                .map(PropertySubject::Element)
473                .collect()),
474            (IndexDefinition::Label { .. }, _lookup) => {
475                Err(DbError::unsupported("label index expects all lookup"))
476            }
477            (IndexDefinition::RelationType { relation_type }, IndexProbe::All) => Ok(view
478                .relations_with_type(*relation_type)
479                .into_iter()
480                .map(PropertySubject::Relation)
481                .collect()),
482            (IndexDefinition::RelationType { .. }, _lookup) => Err(DbError::unsupported(
483                "relation type index expects all lookup",
484            )),
485            (IndexDefinition::PropertyEquality { key }, IndexProbe::Equal(value)) => {
486                view.typed_property_equal(*key, value)
487            }
488            (IndexDefinition::PropertyEquality { .. }, _lookup) => Err(DbError::unsupported(
489                "property equality index expects equality lookup",
490            )),
491            (IndexDefinition::PropertyRange { key }, IndexProbe::Range { min, max }) => {
492                view.typed_property_range(*key, min, max)
493            }
494            (IndexDefinition::PropertyRange { .. }, _lookup) => Err(DbError::unsupported(
495                "property range index expects range lookup",
496            )),
497            (IndexDefinition::CompositeEquality { keys }, IndexProbe::Composite(values)) => {
498                view.typed_property_composite_equal(keys, values)
499            }
500            (IndexDefinition::CompositeEquality { .. }, _lookup) => Err(DbError::unsupported(
501                "composite equality index expects composite equality lookup",
502            )),
503            (IndexDefinition::Projection { projection }, IndexProbe::All) => {
504                self.projection_index_subjects(*projection)
505            }
506            (IndexDefinition::Projection { .. }, _lookup) => {
507                Err(DbError::unsupported("projection index expects all lookup"))
508            }
509        }
510    }
511
512    /// Materializes a graph projection.
513    ///
514    /// # Errors
515    ///
516    /// Returns [`DbError`] when the projection is unknown, is not a graph, or
517    /// fails validation against current topology.
518    ///
519    /// # Performance
520    ///
521    /// This method is `O(relation count * incidence count)`.
522    pub fn graph_projection(&self, id: ProjectionId) -> Result<GraphProjection, DbError> {
523        let view = self.snapshot.view();
524        let entry = view
525            .catalog()
526            .projection(id)
527            .ok_or_else(|| DbError::unknown(id))?;
528        match &entry.definition {
529            ProjectionDefinition::Graph(definition) => {
530                projection::GraphProjection::from_state(&view, definition.clone())
531            }
532            ProjectionDefinition::Hypergraph(_definition) => {
533                Err(DbError::invalid_projection("projection is not a graph"))
534            }
535        }
536    }
537
538    /// Materializes a graph projection by catalog name.
539    ///
540    /// # Errors
541    ///
542    /// Returns [`DbError`] when the projection is unknown, is not a graph, or
543    /// fails validation against current topology.
544    ///
545    /// # Performance
546    ///
547    /// This method is `O(log projection count + relation count * incidence count)`.
548    pub fn graph_projection_by_name(&self, name: &str) -> Result<GraphProjection, DbError> {
549        let id = self
550            .snapshot
551            .view()
552            .catalog()
553            .projection_id(name)
554            .ok_or_else(|| DbError::unsupported(format!("unknown projection {name}")))?;
555        self.graph_projection(id)
556    }
557
558    /// Walks a cataloged graph projection from canonical seed elements,
559    /// returning the discovered nodes AND the projection edges among them.
560    ///
561    /// Nodes are unique canonical elements in BFS first-discovery order; depth is
562    /// the shortest discovered hop count from any seed. Edges connect two
563    /// discovered nodes, ordered deterministically and unique by relation, so the
564    /// [`Subgraph`] never references a node it omitted.
565    ///
566    /// # Errors
567    ///
568    /// Returns [`DbError`] when the projection is unknown, is not a graph,
569    /// cannot be materialized, or a seed element is not part of the projection.
570    ///
571    /// # Performance
572    ///
573    /// This method is `O(relation count * incidence count + visited edges)`.
574    pub fn walk(
575        &self,
576        projection: ProjectionId,
577        seeds: &[ElementId],
578        walk: Walk,
579    ) -> Result<Subgraph, DbError> {
580        if seeds.is_empty() || walk.limit == 0 {
581            return Ok(Subgraph::default());
582        }
583        let graph = self.graph_projection(projection)?;
584        traversal::walk_graph_projection(&graph, seeds, walk)
585    }
586
587    /// Ranks a cataloged graph projection by personalized `PageRank`, returning
588    /// every projection element paired with its rank, ordered highest first.
589    ///
590    /// `seeds` are the restart (teleport) set: rank mass returns to them on each
591    /// damping step, biasing the stationary distribution toward elements
592    /// reachable from the seeds (random walk with restart). The seed weights are
593    /// normalized internally, so passing the seed elements is sufficient. With no
594    /// seeds this is the uniform-teleport `PageRank` over the projection.
595    ///
596    /// # Errors
597    ///
598    /// Returns [`DbError`] when the projection is unknown, is not a graph, cannot
599    /// be materialized, or `PageRank` rejects the configuration (the
600    /// [`PageRankConfig`] was invalid or the power iteration did not converge).
601    /// Seeds absent from the projection are ignored rather than erroring; with no
602    /// resolvable seed this is the uniform-teleport rank.
603    ///
604    /// # Performance
605    ///
606    /// This method is `O(relation count * incidence count + iterations *
607    /// (visible elements + visible edges) + visible elements * log(visible
608    /// elements))` — the trailing term is the final rank sort.
609    pub fn personalized_pagerank(
610        &self,
611        projection: ProjectionId,
612        seeds: &[ElementId],
613        config: PageRankConfig<f64>,
614    ) -> Result<Vec<(ElementId, f64)>, DbError> {
615        let graph = self.graph_projection(projection)?;
616        let bound = graph.element_bound();
617        let element_count = u32::try_from(bound).map_err(|_| {
618            DbError::traversal("projection exceeds the personalized pagerank index bound")
619        })?;
620
621        let mut personalization = vec![0.0_f64; bound];
622        let mut seeded = false;
623        for &seed in seeds {
624            if let Some(local) = graph.local_element_id(seed) {
625                personalization[graph.element_index(local)] = 1.0;
626                seeded = true;
627            }
628        }
629
630        let mut ranks = vec![0.0_f64; bound];
631        let mut workspace = PageRankWorkspace::for_graph(&graph);
632        pagerank_graph_with_workspace(
633            &graph,
634            &Uniform,
635            (0..element_count).map(ProjectionElementId::new),
636            config,
637            seeded.then_some(personalization.as_slice()),
638            &mut ranks,
639            &mut workspace,
640        )
641        .map_err(|error| {
642            DbError::traversal(match error {
643                PageRankError::InvalidDamping { .. }
644                | PageRankError::InvalidTolerance { .. }
645                | PageRankError::InvalidMaxIterations => "invalid pagerank configuration",
646                PageRankError::NonConverged { .. } => "personalized pagerank did not converge",
647                _ => "personalized pagerank failed",
648            })
649        })?;
650
651        let mut ranked: Vec<(ElementId, f64)> = (0..element_count)
652            .map(|index| {
653                let local = ProjectionElementId::new(index);
654                (
655                    graph.canonical_element_id(local),
656                    ranks[graph.element_index(local)],
657                )
658            })
659            .collect();
660        ranked.sort_by(|left, right| right.1.total_cmp(&left.1));
661        Ok(ranked)
662    }
663
664    /// Returns the longest chain of canonical elements along the projection's
665    /// outgoing edges within the subgraph induced by `elements`.
666    ///
667    /// Only edges whose endpoints are both in `elements` participate. The path
668    /// lists each element once from start to end; its length in edges is
669    /// `path.len() - 1`. An empty `elements` slice yields an empty path.
670    ///
671    /// # Errors
672    ///
673    /// Returns [`DbError`] when the projection is unknown, is not a graph, cannot
674    /// be materialized, or the induced subgraph contains a cycle. Elements absent
675    /// from the projection are ignored, so the chain is computed over the present
676    /// subset.
677    ///
678    /// # Performance
679    ///
680    /// This method is `O(relation count * incidence count + visible elements +
681    /// visible edges)`.
682    pub fn longest_path(
683        &self,
684        projection: ProjectionId,
685        elements: &[ElementId],
686    ) -> Result<Vec<ElementId>, DbError> {
687        if elements.is_empty() {
688            return Ok(Vec::new());
689        }
690        let graph = self.graph_projection(projection)?;
691        let locals = elements
692            .iter()
693            .filter_map(|&element| graph.local_element_id(element))
694            .collect::<Vec<ProjectionElementId>>();
695        let path = longest_path_dag(&graph, &locals)
696            .map_err(|_| DbError::traversal("longest path found a cycle"))?;
697        Ok(path
698            .into_iter()
699            .map(|local| graph.canonical_element_id(local))
700            .collect())
701    }
702
703    /// Materializes a hypergraph projection.
704    ///
705    /// # Errors
706    ///
707    /// Returns [`DbError`] when the projection is unknown, is not a hypergraph,
708    /// or fails validation against current topology.
709    ///
710    /// # Performance
711    ///
712    /// This method is `O(relation count * incidence count)`.
713    pub fn hypergraph_projection(&self, id: ProjectionId) -> Result<HypergraphProjection, DbError> {
714        let view = self.snapshot.view();
715        let entry = view
716            .catalog()
717            .projection(id)
718            .ok_or_else(|| DbError::unknown(id))?;
719        match &entry.definition {
720            ProjectionDefinition::Hypergraph(definition) => {
721                projection::HypergraphProjection::from_state(&view, definition.clone())
722            }
723            ProjectionDefinition::Graph(_definition) => Err(DbError::invalid_projection(
724                "projection is not a hypergraph",
725            )),
726        }
727    }
728
729    /// Executes a prepared query.
730    ///
731    /// # Errors
732    ///
733    /// Returns [`DbError`] when execution cannot materialize a referenced
734    /// projection.
735    ///
736    /// # Performance
737    ///
738    /// This method is `O(plan output + projection build cost when used)`.
739    pub fn run(&self, query: &PreparedQuery) -> Result<QueryResult, DbError> {
740        query.execute(&self.snapshot.view())
741    }
742
743    /// Explains a prepared query.
744    ///
745    /// # Performance
746    ///
747    /// This method is `O(plan size)`.
748    #[must_use]
749    pub fn explain(&self, query: &PreparedQuery) -> String {
750        query.explain()
751    }
752
753    /// Materializes subjects represented by a projection index.
754    ///
755    /// # Errors
756    ///
757    /// Returns [`DbError`] when the projection is unknown or cannot be
758    /// materialized.
759    ///
760    /// # Performance
761    ///
762    /// This method is `O(relation count * incidence count)`.
763    fn projection_index_subjects(
764        &self,
765        projection: ProjectionId,
766    ) -> Result<Vec<PropertySubject>, DbError> {
767        let view = self.snapshot.view();
768        let entry = view
769            .catalog()
770            .projection(projection)
771            .ok_or_else(|| DbError::unknown(projection))?;
772        match &entry.definition {
773            ProjectionDefinition::Graph(definition) => {
774                Ok(projection::GraphProjection::from_state(&view, definition.clone())?.subjects())
775            }
776            ProjectionDefinition::Hypergraph(definition) => Ok(
777                projection::HypergraphProjection::from_state(&view, definition.clone())?.subjects(),
778            ),
779        }
780    }
781}