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}