Skip to main content

oxgraph_db/
projection.rs

1//! Materialized graph and hypergraph projection views.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use oxgraph_csr::build::GraphBuilder;
6use oxgraph_graph::{
7    DenseElementIndex, DenseRelationIndex, EdgeSourceGraph, EdgeTargetGraph, ElementPredecessors,
8    ElementSuccessors, IncomingEdgeCount, IncomingGraph, LocalElementIdentity,
9    LocalRelationIdentity, OutgoingEdgeCount, OutgoingGraph, TopologyBase, TopologyCounts,
10};
11use oxgraph_hyper::{
12    DenseIncidenceIndex, DirectedHyperedgeIncidences, DirectedHyperedgeParticipants,
13    DirectedVertexHyperedges, ElementIncidenceCount, IncidenceBase, IncidenceCounts,
14    IncidenceElement, IncidenceRelation, IncidenceRole, IncidentHyperedges, RelationIncidenceCount,
15    RelationIncidences,
16};
17use oxgraph_hyper_bcsr::build::{HyperVertexId, HypergraphBuilder};
18use serde::{Deserialize, Serialize};
19
20use crate::{
21    DbError, ElementId, IncidenceId, RelationId, RelationTypeId, RoleId,
22    catalog::{GraphProjectionDefinition, HypergraphProjectionDefinition},
23    overlay::StateView,
24    state::{IncidenceRecord, PropertySubject, RelationRecord},
25};
26
27/// Dense projection-local element identifier.
28///
29/// # Performance
30///
31/// Copying, comparing, ordering, and hashing are `O(1)`.
32#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
33pub struct ProjectionElementId(u32);
34
35impl ProjectionElementId {
36    /// Creates a local element ID.
37    ///
38    /// # Performance
39    ///
40    /// This function is `O(1)`.
41    #[must_use]
42    pub const fn new(value: u32) -> Self {
43        Self(value)
44    }
45
46    /// Returns the raw local index.
47    ///
48    /// # Performance
49    ///
50    /// This function is `O(1)`.
51    #[must_use]
52    pub const fn get(self) -> u32 {
53        self.0
54    }
55}
56
57/// Dense projection-local relation identifier.
58///
59/// # Performance
60///
61/// Copying, comparing, ordering, and hashing are `O(1)`.
62#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
63pub struct ProjectionRelationId(u32);
64
65impl ProjectionRelationId {
66    /// Creates a local relation ID.
67    ///
68    /// # Performance
69    ///
70    /// This function is `O(1)`.
71    #[must_use]
72    pub const fn new(value: u32) -> Self {
73        Self(value)
74    }
75
76    /// Returns the raw local index.
77    ///
78    /// # Performance
79    ///
80    /// This function is `O(1)`.
81    #[must_use]
82    pub const fn get(self) -> u32 {
83        self.0
84    }
85}
86
87/// Dense projection-local incidence identifier.
88///
89/// # Performance
90///
91/// Copying, comparing, ordering, and hashing are `O(1)`.
92#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
93pub struct ProjectionIncidenceId(u32);
94
95impl ProjectionIncidenceId {
96    /// Creates a local incidence ID.
97    ///
98    /// # Performance
99    ///
100    /// This function is `O(1)`.
101    #[must_use]
102    pub const fn new(value: u32) -> Self {
103        Self(value)
104    }
105
106    /// Returns the raw local index.
107    ///
108    /// # Performance
109    ///
110    /// This function is `O(1)`.
111    #[must_use]
112    pub const fn get(self) -> u32 {
113        self.0
114    }
115}
116
117/// Materialized binary graph projection.
118///
119/// This view exposes `OxGraph` canonical topology through `oxgraph-graph`
120/// traits. Outgoing arrays are CSR-shaped; incoming arrays are CSC-shaped.
121///
122/// # Performance
123///
124/// Cloning is `O(n + m)` for visible nodes and edges. Traversal is `O(k)` for
125/// each yielded adjacency list.
126#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct GraphProjection {
128    /// Projection definition used to build this view.
129    definition: GraphProjectionDefinition,
130    /// Local-to-canonical elements.
131    elements: Vec<ElementId>,
132    /// Canonical-to-local elements.
133    element_index: BTreeMap<ElementId, ProjectionElementId>,
134    /// Local-to-canonical relations.
135    relations: Vec<RelationId>,
136    /// Canonical-to-local relations.
137    relation_index: BTreeMap<RelationId, ProjectionRelationId>,
138    /// Source and target local IDs by local relation.
139    endpoints: Vec<(ProjectionElementId, ProjectionElementId)>,
140    /// CSR outgoing edge IDs by source local element.
141    out_edges: Vec<Vec<ProjectionRelationId>>,
142    /// CSC incoming edge IDs by target local element.
143    in_edges: Vec<Vec<ProjectionRelationId>>,
144    /// CSR outgoing neighbor IDs by source local element.
145    successors: Vec<Vec<ProjectionElementId>>,
146    /// CSC incoming neighbor IDs by target local element.
147    predecessors: Vec<Vec<ProjectionElementId>>,
148}
149
150impl GraphProjection {
151    /// Builds a graph projection from canonical state.
152    ///
153    /// # Errors
154    ///
155    /// Returns [`DbError::InvalidProjection`] when a selected relation is not a
156    /// binary relation for the configured source and target roles.
157    ///
158    /// # Performance
159    ///
160    /// This function is `O(r * i + e log n)` for relation count `r`,
161    /// incidence count `i`, and selected edge count `e`.
162    pub(crate) fn from_state(
163        state: &impl StateView,
164        definition: GraphProjectionDefinition,
165    ) -> Result<Self, DbError> {
166        let mut builder = GraphProjectionBuilder::new(definition);
167        for relation in state.relations() {
168            if relation_selected(relation.as_ref(), &builder.definition.relation_types) {
169                builder.push_relation(state, relation.as_ref())?;
170            }
171        }
172        builder.finish()
173    }
174
175    /// Returns the definition used for this projection.
176    ///
177    /// # Performance
178    ///
179    /// This method is `O(1)`.
180    #[must_use]
181    pub const fn definition(&self) -> &GraphProjectionDefinition {
182        &self.definition
183    }
184
185    /// Returns deterministic canonical subjects materialized by this projection.
186    pub(crate) fn subjects(&self) -> Vec<PropertySubject> {
187        let mut subjects = Vec::with_capacity(self.elements.len() + self.relations.len());
188        subjects.extend(self.elements.iter().copied().map(PropertySubject::Element));
189        subjects.extend(
190            self.relations
191                .iter()
192                .copied()
193                .map(PropertySubject::Relation),
194        );
195        subjects.sort_unstable();
196        subjects
197    }
198}
199
200/// Builder used to keep graph projection construction small.
201struct GraphProjectionBuilder {
202    /// Projection definition being built.
203    definition: GraphProjectionDefinition,
204    /// Local-to-canonical elements.
205    elements: Vec<ElementId>,
206    /// Canonical-to-local elements.
207    element_index: BTreeMap<ElementId, ProjectionElementId>,
208    /// Local-to-canonical relations.
209    relations: Vec<RelationId>,
210    /// Canonical-to-local relations.
211    relation_index: BTreeMap<RelationId, ProjectionRelationId>,
212    /// Edge endpoints.
213    endpoints: Vec<(ProjectionElementId, ProjectionElementId)>,
214}
215
216impl GraphProjectionBuilder {
217    /// Creates an empty graph projection builder.
218    const fn new(definition: GraphProjectionDefinition) -> Self {
219        Self {
220            definition,
221            elements: Vec::new(),
222            element_index: BTreeMap::new(),
223            relations: Vec::new(),
224            relation_index: BTreeMap::new(),
225            endpoints: Vec::new(),
226        }
227    }
228
229    /// Adds one selected canonical relation.
230    fn push_relation(
231        &mut self,
232        state: &impl StateView,
233        relation: &RelationRecord,
234    ) -> Result<(), DbError> {
235        let (source, target) = graph_endpoints(
236            state,
237            relation.id,
238            self.definition.source_role,
239            self.definition.target_role,
240        )?;
241        let source = intern_element(&mut self.element_index, &mut self.elements, source)?;
242        let target = intern_element(&mut self.element_index, &mut self.elements, target)?;
243        let edge = projection_relation_id(self.relations.len())?;
244        self.relations.push(relation.id);
245        self.relation_index.insert(relation.id, edge);
246        self.endpoints.push((source, target));
247        Ok(())
248    }
249
250    /// Finishes the graph projection.
251    ///
252    /// The CSR forward adjacency and its CSC transpose are constructed by the
253    /// `oxgraph-csr` substrate builder rather than re-implementing the
254    /// counting-sort/offset pass here. Parallel edges are preserved: every
255    /// interned endpoint becomes one builder edge, so an element with `k`
256    /// outgoing edges to the same target yields `k` successor entries.
257    fn finish(self) -> Result<GraphProjection, DbError> {
258        let node_count = self.elements.len();
259        let mut builder = GraphBuilder::<u32, u32>::new();
260        let mut nodes = Vec::with_capacity(node_count);
261        for _element in &self.elements {
262            nodes.push(
263                builder
264                    .add_node()
265                    .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Element))?,
266            );
267        }
268        for (source, target) in self.endpoints.iter().copied() {
269            builder
270                .add_edge(nodes[local_index(source)], nodes[local_index(target)])
271                .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Relation))?;
272        }
273        let forward = builder
274            .freeze()
275            .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Relation))?;
276        let reverse = forward
277            .transpose()
278            .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Relation))?;
279
280        // Forward freeze: `targets`/`edge_ids` are grouped by source node, so
281        // outgoing successors and outgoing edge IDs read straight off its CSR
282        // arrays. The transpose groups the original edge IDs and original
283        // sources by target node, giving the incoming edge IDs and predecessor
284        // successors with original edge identity preserved.
285        let successors = grouped_elements(forward.offsets(), forward.targets(), node_count)?;
286        let out_edges = grouped_relations(forward.offsets(), forward.edge_ids(), node_count)?;
287        let predecessors = grouped_elements(reverse.offsets(), reverse.targets(), node_count)?;
288        let in_edges = grouped_relations(reverse.offsets(), reverse.edge_ids(), node_count)?;
289
290        Ok(GraphProjection {
291            definition: self.definition,
292            elements: self.elements,
293            element_index: self.element_index,
294            relations: self.relations,
295            relation_index: self.relation_index,
296            endpoints: self.endpoints,
297            out_edges,
298            in_edges,
299            successors,
300            predecessors,
301        })
302    }
303}
304
305/// Splits a flat CSR target array into per-node projection element lists.
306///
307/// `offsets` has `node_count + 1` entries; `flat[offsets[n]..offsets[n + 1]]`
308/// holds the grouped target node indices for node `n`.
309///
310/// # Performance
311///
312/// This function is `O(node_count + flat.len())`.
313fn grouped_elements(
314    offsets: &[u32],
315    flat: &[u32],
316    node_count: usize,
317) -> Result<Vec<Vec<ProjectionElementId>>, DbError> {
318    grouped(offsets, flat, node_count, |raw| {
319        Ok(ProjectionElementId(raw))
320    })
321}
322
323/// Splits a flat CSR edge-ID array into per-node projection relation lists.
324///
325/// # Performance
326///
327/// This function is `O(node_count + flat.len())`.
328fn grouped_relations(
329    offsets: &[u32],
330    flat: &[u32],
331    node_count: usize,
332) -> Result<Vec<Vec<ProjectionRelationId>>, DbError> {
333    grouped(offsets, flat, node_count, |raw| {
334        Ok(ProjectionRelationId(raw))
335    })
336}
337
338/// Splits a flat CSR payload array into `node_count` grouped, mapped lists.
339///
340/// # Performance
341///
342/// This function is `O(node_count + flat.len())`.
343fn grouped<T>(
344    offsets: &[u32],
345    flat: &[u32],
346    node_count: usize,
347    map: impl Fn(u32) -> Result<T, DbError>,
348) -> Result<Vec<Vec<T>>, DbError> {
349    let mut rows = Vec::with_capacity(node_count);
350    for node in 0..node_count {
351        let start = usize::try_from(offsets[node])
352            .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Relation))?;
353        let end = usize::try_from(offsets[node + 1])
354            .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Relation))?;
355        let slice = flat
356            .get(start..end)
357            .ok_or_else(|| DbError::invalid_projection("csr offset range out of bounds"))?;
358        rows.push(
359            slice
360                .iter()
361                .copied()
362                .map(&map)
363                .collect::<Result<Vec<T>, DbError>>()?,
364        );
365    }
366    Ok(rows)
367}
368
369impl TopologyBase for GraphProjection {
370    type ElementId = ProjectionElementId;
371    type RelationId = ProjectionRelationId;
372}
373
374impl TopologyCounts for GraphProjection {
375    fn element_count(&self) -> usize {
376        self.elements.len()
377    }
378
379    fn relation_count(&self) -> usize {
380        self.relations.len()
381    }
382}
383
384impl DenseElementIndex for GraphProjection {
385    fn element_bound(&self) -> usize {
386        self.elements.len()
387    }
388
389    fn element_index(&self, element: Self::ElementId) -> usize {
390        local_index(element)
391    }
392}
393
394impl DenseRelationIndex for GraphProjection {
395    fn relation_bound(&self) -> usize {
396        self.relations.len()
397    }
398
399    fn relation_index(&self, relation: Self::RelationId) -> usize {
400        local_relation_index(relation)
401    }
402}
403
404impl oxgraph_graph::ContainsElement for GraphProjection {
405    fn contains_element(&self, element: Self::ElementId) -> bool {
406        local_index(element) < self.elements.len()
407    }
408}
409
410impl oxgraph_graph::ContainsRelation for GraphProjection {
411    fn contains_relation(&self, relation: Self::RelationId) -> bool {
412        local_relation_index(relation) < self.relations.len()
413    }
414}
415
416impl oxgraph_graph::CanonicalElementIdentity for GraphProjection {
417    type CanonicalElementId = ElementId;
418
419    fn canonical_element_id(&self, element: Self::ElementId) -> Self::CanonicalElementId {
420        self.elements[local_index(element)]
421    }
422}
423
424impl LocalElementIdentity for GraphProjection {
425    fn local_element_id(&self, canonical: Self::CanonicalElementId) -> Option<Self::ElementId> {
426        self.element_index.get(&canonical).copied()
427    }
428}
429
430impl oxgraph_graph::CanonicalRelationIdentity for GraphProjection {
431    type CanonicalRelationId = RelationId;
432
433    fn canonical_relation_id(&self, relation: Self::RelationId) -> Self::CanonicalRelationId {
434        self.relations[local_relation_index(relation)]
435    }
436}
437
438impl LocalRelationIdentity for GraphProjection {
439    fn local_relation_id(&self, canonical: Self::CanonicalRelationId) -> Option<Self::RelationId> {
440        self.relation_index.get(&canonical).copied()
441    }
442}
443
444impl EdgeSourceGraph for GraphProjection {
445    fn source(&self, edge: Self::RelationId) -> Self::ElementId {
446        self.endpoints[local_relation_index(edge)].0
447    }
448}
449
450impl EdgeTargetGraph for GraphProjection {
451    fn target(&self, edge: Self::RelationId) -> Self::ElementId {
452        self.endpoints[local_relation_index(edge)].1
453    }
454}
455
456impl OutgoingGraph for GraphProjection {
457    type OutEdges<'view>
458        = std::iter::Copied<std::slice::Iter<'view, ProjectionRelationId>>
459    where
460        Self: 'view;
461
462    fn outgoing_edges(&self, node: Self::ElementId) -> Self::OutEdges<'_> {
463        self.out_edges[local_index(node)].iter().copied()
464    }
465}
466
467impl IncomingGraph for GraphProjection {
468    type InEdges<'view>
469        = std::iter::Copied<std::slice::Iter<'view, ProjectionRelationId>>
470    where
471        Self: 'view;
472
473    fn incoming_edges(&self, node: Self::ElementId) -> Self::InEdges<'_> {
474        self.in_edges[local_index(node)].iter().copied()
475    }
476}
477
478impl ElementSuccessors for GraphProjection {
479    type Successors<'view>
480        = std::iter::Copied<std::slice::Iter<'view, ProjectionElementId>>
481    where
482        Self: 'view;
483
484    fn element_successors(&self, element: Self::ElementId) -> Self::Successors<'_> {
485        self.successors[local_index(element)].iter().copied()
486    }
487}
488
489impl ElementPredecessors for GraphProjection {
490    type Predecessors<'view>
491        = std::iter::Copied<std::slice::Iter<'view, ProjectionElementId>>
492    where
493        Self: 'view;
494
495    fn element_predecessors(&self, element: Self::ElementId) -> Self::Predecessors<'_> {
496        self.predecessors[local_index(element)].iter().copied()
497    }
498}
499
500impl OutgoingEdgeCount for GraphProjection {
501    fn out_degree(&self, node: Self::ElementId) -> usize {
502        self.out_edges[local_index(node)].len()
503    }
504}
505
506impl IncomingEdgeCount for GraphProjection {
507    fn in_degree(&self, node: Self::ElementId) -> usize {
508        self.in_edges[local_index(node)].len()
509    }
510}
511
512/// Materialized directed hypergraph projection.
513///
514/// This view exposes `OxGraph` canonical topology through `oxgraph-hyper`
515/// traits. Its physical arrays are BCSR-shaped: relation-major participant
516/// arrays plus vertex-major incoming/outgoing hyperedge arrays.
517///
518/// # Performance
519///
520/// Cloning is `O(v + h + p)` for vertices, hyperedges, and participants.
521#[derive(Clone, Debug, Eq, PartialEq)]
522pub struct HypergraphProjection {
523    /// Projection definition used to build this view.
524    definition: HypergraphProjectionDefinition,
525    /// Local-to-canonical elements.
526    elements: Vec<ElementId>,
527    /// Canonical-to-local elements.
528    element_index: BTreeMap<ElementId, ProjectionElementId>,
529    /// Local-to-canonical relations.
530    relations: Vec<RelationId>,
531    /// Canonical-to-local relations.
532    relation_index: BTreeMap<RelationId, ProjectionRelationId>,
533    /// Local-to-canonical incidences.
534    incidences: Vec<IncidenceRecord>,
535    /// Canonical-to-local incidences.
536    incidence_index: BTreeMap<IncidenceId, ProjectionIncidenceId>,
537    /// Relation-major incidence IDs.
538    relation_incidences: Vec<Vec<ProjectionIncidenceId>>,
539    /// Element-major incidence IDs.
540    element_incidences: Vec<Vec<ProjectionIncidenceId>>,
541    /// Source-side incidence IDs by relation.
542    source_incidences: Vec<Vec<ProjectionIncidenceId>>,
543    /// Target-side incidence IDs by relation.
544    target_incidences: Vec<Vec<ProjectionIncidenceId>>,
545    /// Source-side element IDs by relation.
546    source_vertices: Vec<Vec<ProjectionElementId>>,
547    /// Target-side element IDs by relation.
548    target_vertices: Vec<Vec<ProjectionElementId>>,
549    /// Outgoing hyperedges by source-side element.
550    outgoing_hyperedges: Vec<Vec<ProjectionRelationId>>,
551    /// Incoming hyperedges by target-side element.
552    incoming_hyperedges: Vec<Vec<ProjectionRelationId>>,
553    /// Directed successor elements by source-side element.
554    successors: Vec<Vec<ProjectionElementId>>,
555    /// Directed predecessor elements by target-side element.
556    predecessors: Vec<Vec<ProjectionElementId>>,
557}
558
559impl HypergraphProjection {
560    /// Builds a hypergraph projection from canonical state.
561    ///
562    /// # Errors
563    ///
564    /// Returns [`DbError::InvalidProjection`] when source/target role sets do
565    /// not produce directed participant sets for selected relations.
566    ///
567    /// # Performance
568    ///
569    /// This function is `O(r * i + p log v)` for relation count `r`,
570    /// incidence count `i`, and selected participant count `p`.
571    pub(crate) fn from_state(
572        state: &impl StateView,
573        definition: HypergraphProjectionDefinition,
574    ) -> Result<Self, DbError> {
575        if definition.source_roles.is_empty() || definition.target_roles.is_empty() {
576            return Err(DbError::invalid_projection(
577                "hypergraph projection requires non-empty source and target roles",
578            ));
579        }
580        let mut builder = HypergraphProjectionBuilder::new(definition);
581        for relation in state.relations() {
582            if relation_selected(relation.as_ref(), &builder.definition.relation_types) {
583                builder.push_relation(state, relation.as_ref())?;
584            }
585        }
586        builder.finish()
587    }
588
589    /// Returns the definition used for this projection.
590    ///
591    /// # Performance
592    ///
593    /// This method is `O(1)`.
594    #[must_use]
595    pub const fn definition(&self) -> &HypergraphProjectionDefinition {
596        &self.definition
597    }
598
599    /// Returns deterministic canonical subjects materialized by this projection.
600    pub(crate) fn subjects(&self) -> Vec<PropertySubject> {
601        let mut subjects =
602            Vec::with_capacity(self.elements.len() + self.relations.len() + self.incidences.len());
603        subjects.extend(self.elements.iter().copied().map(PropertySubject::Element));
604        subjects.extend(
605            self.relations
606                .iter()
607                .copied()
608                .map(PropertySubject::Relation),
609        );
610        subjects.extend(
611            self.incidences
612                .iter()
613                .map(|record| PropertySubject::Incidence(record.id)),
614        );
615        subjects.sort_unstable();
616        subjects
617    }
618}
619
620/// Builder used to keep hypergraph projection construction small.
621struct HypergraphProjectionBuilder {
622    /// Projection definition being built.
623    definition: HypergraphProjectionDefinition,
624    /// Local-to-canonical elements.
625    elements: Vec<ElementId>,
626    /// Canonical-to-local elements.
627    element_index: BTreeMap<ElementId, ProjectionElementId>,
628    /// Local-to-canonical relations.
629    relations: Vec<RelationId>,
630    /// Canonical-to-local relations.
631    relation_index: BTreeMap<RelationId, ProjectionRelationId>,
632    /// Local incidence records.
633    incidences: Vec<IncidenceRecord>,
634    /// Canonical-to-local incidences.
635    incidence_index: BTreeMap<IncidenceId, ProjectionIncidenceId>,
636    /// Relation incidence arrays.
637    relation_incidences: Vec<Vec<ProjectionIncidenceId>>,
638    /// Source incidence arrays.
639    source_incidences: Vec<Vec<ProjectionIncidenceId>>,
640    /// Target incidence arrays.
641    target_incidences: Vec<Vec<ProjectionIncidenceId>>,
642    /// Source vertex arrays.
643    source_vertices: Vec<Vec<ProjectionElementId>>,
644    /// Target vertex arrays.
645    target_vertices: Vec<Vec<ProjectionElementId>>,
646}
647
648impl HypergraphProjectionBuilder {
649    /// Creates an empty hypergraph projection builder.
650    const fn new(definition: HypergraphProjectionDefinition) -> Self {
651        Self {
652            definition,
653            elements: Vec::new(),
654            element_index: BTreeMap::new(),
655            relations: Vec::new(),
656            relation_index: BTreeMap::new(),
657            incidences: Vec::new(),
658            incidence_index: BTreeMap::new(),
659            relation_incidences: Vec::new(),
660            source_incidences: Vec::new(),
661            target_incidences: Vec::new(),
662            source_vertices: Vec::new(),
663            target_vertices: Vec::new(),
664        }
665    }
666
667    /// Adds one selected canonical hyperedge relation.
668    fn push_relation(
669        &mut self,
670        state: &impl StateView,
671        relation: &RelationRecord,
672    ) -> Result<(), DbError> {
673        let hyperedge = projection_relation_id(self.relations.len())?;
674        self.relations.push(relation.id);
675        self.relation_index.insert(relation.id, hyperedge);
676        let mut relation_ids = Vec::new();
677        let mut source_ids = Vec::new();
678        let mut target_ids = Vec::new();
679        let mut sources = Vec::new();
680        let mut targets = Vec::new();
681        for incidence in state.relation_incidences(relation.id) {
682            let local = self.push_incidence(&incidence)?;
683            let vertex = intern_element(
684                &mut self.element_index,
685                &mut self.elements,
686                incidence.element,
687            )?;
688            relation_ids.push(local);
689            if self.definition.source_roles.contains(&incidence.role) {
690                source_ids.push(local);
691                sources.push(vertex);
692            }
693            if self.definition.target_roles.contains(&incidence.role) {
694                target_ids.push(local);
695                targets.push(vertex);
696            }
697        }
698        if sources.is_empty() || targets.is_empty() {
699            return Err(DbError::invalid_projection(
700                "selected hyperedge lacks source or target participants",
701            ));
702        }
703        self.relation_incidences.push(relation_ids);
704        self.source_incidences.push(source_ids);
705        self.target_incidences.push(target_ids);
706        self.source_vertices.push(sources);
707        self.target_vertices.push(targets);
708        Ok(())
709    }
710
711    /// Adds one canonical incidence to local storage.
712    fn push_incidence(
713        &mut self,
714        incidence: &IncidenceRecord,
715    ) -> Result<ProjectionIncidenceId, DbError> {
716        if let Some(id) = self.incidence_index.get(&incidence.id) {
717            return Ok(*id);
718        }
719        let local = projection_incidence_id(self.incidences.len())?;
720        self.incidences.push(*incidence);
721        self.incidence_index.insert(incidence.id, local);
722        Ok(local)
723    }
724
725    /// Finishes the hypergraph projection.
726    fn finish(self) -> Result<HypergraphProjection, DbError> {
727        let element_incidences =
728            build_element_incidences(self.elements.len(), &self.incidences, &self.element_index)?;
729        let (outgoing_hyperedges, incoming_hyperedges, successors, predecessors) =
730            build_directed_vertex_arrays(
731                self.elements.len(),
732                &self.source_vertices,
733                &self.target_vertices,
734            )?;
735        Ok(HypergraphProjection {
736            definition: self.definition,
737            elements: self.elements,
738            element_index: self.element_index,
739            relations: self.relations,
740            relation_index: self.relation_index,
741            incidences: self.incidences,
742            incidence_index: self.incidence_index,
743            relation_incidences: self.relation_incidences,
744            element_incidences,
745            source_incidences: self.source_incidences,
746            target_incidences: self.target_incidences,
747            source_vertices: self.source_vertices,
748            target_vertices: self.target_vertices,
749            outgoing_hyperedges,
750            incoming_hyperedges,
751            successors,
752            predecessors,
753        })
754    }
755}
756
757impl TopologyBase for HypergraphProjection {
758    type ElementId = ProjectionElementId;
759    type RelationId = ProjectionRelationId;
760}
761
762impl IncidenceBase for HypergraphProjection {
763    type IncidenceId = ProjectionIncidenceId;
764    type Role = RoleId;
765}
766
767impl TopologyCounts for HypergraphProjection {
768    fn element_count(&self) -> usize {
769        self.elements.len()
770    }
771
772    fn relation_count(&self) -> usize {
773        self.relations.len()
774    }
775}
776
777impl IncidenceCounts for HypergraphProjection {
778    fn incidence_count(&self) -> usize {
779        self.incidences.len()
780    }
781}
782
783impl DenseElementIndex for HypergraphProjection {
784    fn element_bound(&self) -> usize {
785        self.elements.len()
786    }
787
788    fn element_index(&self, element: Self::ElementId) -> usize {
789        local_index(element)
790    }
791}
792
793impl DenseRelationIndex for HypergraphProjection {
794    fn relation_bound(&self) -> usize {
795        self.relations.len()
796    }
797
798    fn relation_index(&self, relation: Self::RelationId) -> usize {
799        local_relation_index(relation)
800    }
801}
802
803impl DenseIncidenceIndex for HypergraphProjection {
804    fn incidence_bound(&self) -> usize {
805        self.incidences.len()
806    }
807
808    fn incidence_index(&self, incidence: Self::IncidenceId) -> usize {
809        local_incidence_index(incidence)
810    }
811}
812
813impl oxgraph_hyper::ContainsElement for HypergraphProjection {
814    fn contains_element(&self, element: Self::ElementId) -> bool {
815        local_index(element) < self.elements.len()
816    }
817}
818
819impl oxgraph_hyper::ContainsRelation for HypergraphProjection {
820    fn contains_relation(&self, relation: Self::RelationId) -> bool {
821        local_relation_index(relation) < self.relations.len()
822    }
823}
824
825impl oxgraph_hyper::ContainsIncidence for HypergraphProjection {
826    fn contains_incidence(&self, incidence: Self::IncidenceId) -> bool {
827        local_incidence_index(incidence) < self.incidences.len()
828    }
829}
830
831impl oxgraph_hyper::CanonicalElementIdentity for HypergraphProjection {
832    type CanonicalElementId = ElementId;
833
834    fn canonical_element_id(&self, element: Self::ElementId) -> Self::CanonicalElementId {
835        self.elements[local_index(element)]
836    }
837}
838
839impl oxgraph_hyper::LocalElementIdentity for HypergraphProjection {
840    fn local_element_id(&self, canonical: Self::CanonicalElementId) -> Option<Self::ElementId> {
841        self.element_index.get(&canonical).copied()
842    }
843}
844
845impl oxgraph_hyper::CanonicalRelationIdentity for HypergraphProjection {
846    type CanonicalRelationId = RelationId;
847
848    fn canonical_relation_id(&self, relation: Self::RelationId) -> Self::CanonicalRelationId {
849        self.relations[local_relation_index(relation)]
850    }
851}
852
853impl oxgraph_hyper::LocalRelationIdentity for HypergraphProjection {
854    fn local_relation_id(&self, canonical: Self::CanonicalRelationId) -> Option<Self::RelationId> {
855        self.relation_index.get(&canonical).copied()
856    }
857}
858
859impl oxgraph_hyper::CanonicalIncidenceIdentity for HypergraphProjection {
860    type CanonicalIncidenceId = IncidenceId;
861
862    fn canonical_incidence_id(&self, incidence: Self::IncidenceId) -> Self::CanonicalIncidenceId {
863        self.incidences[local_incidence_index(incidence)].id
864    }
865}
866
867impl oxgraph_hyper::LocalIncidenceIdentity for HypergraphProjection {
868    fn local_incidence_id(
869        &self,
870        canonical: Self::CanonicalIncidenceId,
871    ) -> Option<Self::IncidenceId> {
872        self.incidence_index.get(&canonical).copied()
873    }
874}
875
876impl RelationIncidences for HypergraphProjection {
877    type Incidences<'view>
878        = std::iter::Copied<std::slice::Iter<'view, ProjectionIncidenceId>>
879    where
880        Self: 'view;
881
882    fn relation_incidences(&self, relation: Self::RelationId) -> Self::Incidences<'_> {
883        self.relation_incidences[local_relation_index(relation)]
884            .iter()
885            .copied()
886    }
887}
888
889impl oxgraph_hyper::ElementIncidences for HypergraphProjection {
890    type Incidences<'view>
891        = std::iter::Copied<std::slice::Iter<'view, ProjectionIncidenceId>>
892    where
893        Self: 'view;
894
895    fn element_incidences(&self, element: Self::ElementId) -> Self::Incidences<'_> {
896        self.element_incidences[local_index(element)]
897            .iter()
898            .copied()
899    }
900}
901
902impl IncidenceElement for HypergraphProjection {
903    fn incidence_element(&self, incidence: Self::IncidenceId) -> Self::ElementId {
904        let canonical = self.incidences[local_incidence_index(incidence)].element;
905        self.element_index[&canonical]
906    }
907}
908
909impl IncidenceRelation for HypergraphProjection {
910    fn incidence_relation(&self, incidence: Self::IncidenceId) -> Self::RelationId {
911        let canonical = self.incidences[local_incidence_index(incidence)].relation;
912        self.relation_index[&canonical]
913    }
914}
915
916impl IncidenceRole for HypergraphProjection {
917    fn incidence_role(&self, incidence: Self::IncidenceId) -> Self::Role {
918        self.incidences[local_incidence_index(incidence)].role
919    }
920}
921
922impl RelationIncidenceCount for HypergraphProjection {
923    fn relation_incidence_count(&self, relation: Self::RelationId) -> usize {
924        self.relation_incidences[local_relation_index(relation)].len()
925    }
926}
927
928impl ElementIncidenceCount for HypergraphProjection {
929    fn element_incidence_count(&self, element: Self::ElementId) -> usize {
930        self.element_incidences[local_index(element)].len()
931    }
932}
933
934impl oxgraph_hyper::HyperedgeParticipants for HypergraphProjection {
935    type Participants<'view>
936        = HyperedgeParticipants<'view>
937    where
938        Self: 'view;
939
940    fn hyperedge_participants(&self, hyperedge: Self::RelationId) -> Self::Participants<'_> {
941        HyperedgeParticipants {
942            view: self,
943            inner: self.relation_incidences[local_relation_index(hyperedge)].iter(),
944        }
945    }
946}
947
948impl IncidentHyperedges for HypergraphProjection {
949    type IncidentHyperedges<'view>
950        = IncidentHyperedgesIter<'view>
951    where
952        Self: 'view;
953
954    fn incident_hyperedges(&self, vertex: Self::ElementId) -> Self::IncidentHyperedges<'_> {
955        IncidentHyperedgesIter {
956            view: self,
957            inner: self.element_incidences[local_index(vertex)].iter(),
958        }
959    }
960}
961
962// `HyperedgeParticipantCount` / `IncidentHyperedgeCount` are provided by the
963// blanket impls in `oxgraph-hyper` over the `RelationIncidenceCount` /
964// `ElementIncidenceCount` impls above.
965
966impl DirectedHyperedgeParticipants for HypergraphProjection {
967    type SourceParticipants<'view>
968        = std::iter::Copied<std::slice::Iter<'view, ProjectionElementId>>
969    where
970        Self: 'view;
971
972    type TargetParticipants<'view>
973        = std::iter::Copied<std::slice::Iter<'view, ProjectionElementId>>
974    where
975        Self: 'view;
976
977    fn source_participants(&self, hyperedge: Self::RelationId) -> Self::SourceParticipants<'_> {
978        self.source_vertices[local_relation_index(hyperedge)]
979            .iter()
980            .copied()
981    }
982
983    fn target_participants(&self, hyperedge: Self::RelationId) -> Self::TargetParticipants<'_> {
984        self.target_vertices[local_relation_index(hyperedge)]
985            .iter()
986            .copied()
987    }
988}
989
990impl DirectedHyperedgeIncidences for HypergraphProjection {
991    type SourceIncidences<'view>
992        = std::iter::Copied<std::slice::Iter<'view, ProjectionIncidenceId>>
993    where
994        Self: 'view;
995
996    type TargetIncidences<'view>
997        = std::iter::Copied<std::slice::Iter<'view, ProjectionIncidenceId>>
998    where
999        Self: 'view;
1000
1001    fn source_incidences(&self, hyperedge: Self::RelationId) -> Self::SourceIncidences<'_> {
1002        self.source_incidences[local_relation_index(hyperedge)]
1003            .iter()
1004            .copied()
1005    }
1006
1007    fn target_incidences(&self, hyperedge: Self::RelationId) -> Self::TargetIncidences<'_> {
1008        self.target_incidences[local_relation_index(hyperedge)]
1009            .iter()
1010            .copied()
1011    }
1012}
1013
1014impl DirectedVertexHyperedges for HypergraphProjection {
1015    type OutgoingHyperedges<'view>
1016        = std::iter::Copied<std::slice::Iter<'view, ProjectionRelationId>>
1017    where
1018        Self: 'view;
1019
1020    type IncomingHyperedges<'view>
1021        = std::iter::Copied<std::slice::Iter<'view, ProjectionRelationId>>
1022    where
1023        Self: 'view;
1024
1025    fn outgoing_hyperedges(&self, vertex: Self::ElementId) -> Self::OutgoingHyperedges<'_> {
1026        self.outgoing_hyperedges[local_index(vertex)]
1027            .iter()
1028            .copied()
1029    }
1030
1031    fn incoming_hyperedges(&self, vertex: Self::ElementId) -> Self::IncomingHyperedges<'_> {
1032        self.incoming_hyperedges[local_index(vertex)]
1033            .iter()
1034            .copied()
1035    }
1036}
1037
1038impl ElementSuccessors for HypergraphProjection {
1039    type Successors<'view>
1040        = std::iter::Copied<std::slice::Iter<'view, ProjectionElementId>>
1041    where
1042        Self: 'view;
1043
1044    fn element_successors(&self, element: Self::ElementId) -> Self::Successors<'_> {
1045        self.successors[local_index(element)].iter().copied()
1046    }
1047}
1048
1049impl ElementPredecessors for HypergraphProjection {
1050    type Predecessors<'view>
1051        = std::iter::Copied<std::slice::Iter<'view, ProjectionElementId>>
1052    where
1053        Self: 'view;
1054
1055    fn element_predecessors(&self, element: Self::ElementId) -> Self::Predecessors<'_> {
1056        self.predecessors[local_index(element)].iter().copied()
1057    }
1058}
1059
1060/// Iterator mapping hyperedge incidences to participant vertices.
1061pub struct HyperedgeParticipants<'view> {
1062    /// Hypergraph projection view.
1063    view: &'view HypergraphProjection,
1064    /// Underlying incidence iterator.
1065    inner: std::slice::Iter<'view, ProjectionIncidenceId>,
1066}
1067
1068impl Iterator for HyperedgeParticipants<'_> {
1069    type Item = ProjectionElementId;
1070
1071    fn next(&mut self) -> Option<Self::Item> {
1072        self.inner
1073            .next()
1074            .map(|incidence| self.view.incidence_element(*incidence))
1075    }
1076}
1077
1078/// Iterator mapping vertex incidences to incident hyperedges.
1079pub struct IncidentHyperedgesIter<'view> {
1080    /// Hypergraph projection view.
1081    view: &'view HypergraphProjection,
1082    /// Underlying incidence iterator.
1083    inner: std::slice::Iter<'view, ProjectionIncidenceId>,
1084}
1085
1086impl Iterator for IncidentHyperedgesIter<'_> {
1087    type Item = ProjectionRelationId;
1088
1089    fn next(&mut self) -> Option<Self::Item> {
1090        self.inner
1091            .next()
1092            .map(|incidence| self.view.incidence_relation(*incidence))
1093    }
1094}
1095
1096/// Returns whether a relation belongs to a relation-type filter.
1097fn relation_selected(record: &RelationRecord, selected: &BTreeSet<RelationTypeId>) -> bool {
1098    record
1099        .relation_type
1100        .map_or(selected.is_empty(), |relation_type| {
1101            selected.is_empty() || selected.contains(&relation_type)
1102        })
1103}
1104
1105/// Finds graph source and target endpoints for one canonical relation.
1106fn graph_endpoints(
1107    state: &impl StateView,
1108    relation: RelationId,
1109    source_role: RoleId,
1110    target_role: RoleId,
1111) -> Result<(ElementId, ElementId), DbError> {
1112    let mut source = None;
1113    let mut target = None;
1114    for incidence in state.relation_incidences(relation) {
1115        update_endpoint(&mut source, &incidence, source_role, "source")?;
1116        update_endpoint(&mut target, &incidence, target_role, "target")?;
1117    }
1118    match (source, target) {
1119        (Some(source), Some(target)) => Ok((source, target)),
1120        _missing => Err(DbError::invalid_projection(
1121            "selected graph relation lacks source or target endpoint",
1122        )),
1123    }
1124}
1125
1126/// Updates one graph endpoint slot.
1127fn update_endpoint(
1128    slot: &mut Option<ElementId>,
1129    incidence: &IncidenceRecord,
1130    role: RoleId,
1131    name: &'static str,
1132) -> Result<(), DbError> {
1133    if incidence.role != role {
1134        return Ok(());
1135    }
1136    if slot.replace(incidence.element).is_some() {
1137        return Err(DbError::invalid_projection(format!(
1138            "selected graph relation has duplicate {name} endpoint"
1139        )));
1140    }
1141    Ok(())
1142}
1143
1144/// Interns a canonical element into projection-local storage.
1145fn intern_element(
1146    index: &mut BTreeMap<ElementId, ProjectionElementId>,
1147    elements: &mut Vec<ElementId>,
1148    canonical: ElementId,
1149) -> Result<ProjectionElementId, DbError> {
1150    if let Some(local) = index.get(&canonical) {
1151        return Ok(*local);
1152    }
1153    let local = projection_element_id(elements.len())?;
1154    elements.push(canonical);
1155    index.insert(canonical, local);
1156    Ok(local)
1157}
1158
1159/// Builds element-major incidence arrays.
1160fn build_element_incidences(
1161    element_count: usize,
1162    incidences: &[IncidenceRecord],
1163    element_index: &BTreeMap<ElementId, ProjectionElementId>,
1164) -> Result<Vec<Vec<ProjectionIncidenceId>>, DbError> {
1165    let mut rows = vec![Vec::new(); element_count];
1166    for (index, incidence) in incidences.iter().enumerate() {
1167        let local = projection_incidence_id(index)?;
1168        let element = element_index
1169            .get(&incidence.element)
1170            .ok_or_else(|| DbError::invalid_projection("incidence element not projected"))?;
1171        rows[local_index(*element)].push(local);
1172    }
1173    Ok(rows)
1174}
1175
1176/// Builds directed vertex-major hypergraph arrays on the substrate BCSR builder.
1177///
1178/// The vertex→hyperedge adjacency (`outgoing`/`incoming`) and the directed
1179/// vertex successor/predecessor sets are produced by freezing an
1180/// [`HypergraphBuilder`] rather than re-implementing the BCSR offset pass. The
1181/// builder requires unique participants per side, so each hyperedge's source
1182/// and target vertex lists are deduplicated (first-occurrence order) before
1183/// being added; this matches the directed-adjacency semantic, where a vertex
1184/// incident to one hyperedge in a given role contributes that hyperedge once.
1185/// Vertex successors/predecessors are deduplicated and ordered to preserve the
1186/// projection's prior set-valued adjacency contract.
1187fn build_directed_vertex_arrays(
1188    element_count: usize,
1189    source_vertices: &[Vec<ProjectionElementId>],
1190    target_vertices: &[Vec<ProjectionElementId>],
1191) -> Result<DirectedVertexArrays, DbError> {
1192    let mut builder = HypergraphBuilder::<u32, u32, u32>::new();
1193    let mut vertices = Vec::with_capacity(element_count);
1194    for _element in 0..element_count {
1195        vertices.push(
1196            builder
1197                .add_vertex()
1198                .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Element))?,
1199        );
1200    }
1201    for (sources, targets) in source_vertices.iter().zip(target_vertices) {
1202        let source_vertices = unique_vertices(&vertices, sources);
1203        let target_vertices = unique_vertices(&vertices, targets);
1204        builder
1205            .add_hyperedge(&source_vertices, &target_vertices)
1206            .map_err(|_error| {
1207                DbError::invalid_projection("hyperedge participants are not directed-unique")
1208            })?;
1209    }
1210    let frozen = builder
1211        .freeze()
1212        .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Incidence))?;
1213
1214    let mut outgoing = Vec::with_capacity(element_count);
1215    let mut incoming = Vec::with_capacity(element_count);
1216    let mut successors = Vec::with_capacity(element_count);
1217    let mut predecessors = Vec::with_capacity(element_count);
1218    for vertex in vertices.iter().copied() {
1219        outgoing.push(
1220            DirectedVertexHyperedges::outgoing_hyperedges(&frozen, vertex)
1221                .map(|hyperedge| ProjectionRelationId(hyperedge.get()))
1222                .collect(),
1223        );
1224        incoming.push(
1225            DirectedVertexHyperedges::incoming_hyperedges(&frozen, vertex)
1226                .map(|hyperedge| ProjectionRelationId(hyperedge.get()))
1227                .collect(),
1228        );
1229        successors.push(unique_sorted(
1230            frozen
1231                .element_successors(vertex)
1232                .map(|target| ProjectionElementId(target.get())),
1233        ));
1234        predecessors.push(unique_sorted(
1235            frozen
1236                .element_predecessors(vertex)
1237                .map(|source| ProjectionElementId(source.get())),
1238        ));
1239    }
1240    Ok((outgoing, incoming, successors, predecessors))
1241}
1242
1243/// Maps interned local element IDs to builder vertex handles, dropping repeats.
1244///
1245/// The substrate hypergraph builder rejects a participant side that repeats a
1246/// vertex, so first-occurrence order is preserved while later duplicates are
1247/// elided.
1248///
1249/// # Performance
1250///
1251/// This function is `O(n log n)` for `n` participants.
1252fn unique_vertices(
1253    vertices: &[HyperVertexId<u32>],
1254    locals: &[ProjectionElementId],
1255) -> Vec<HyperVertexId<u32>> {
1256    let mut seen = BTreeSet::new();
1257    locals
1258        .iter()
1259        .filter(|local| seen.insert(**local))
1260        .map(|local| vertices[local_index(*local)])
1261        .collect()
1262}
1263
1264/// Collects an element iterator into a deduplicated, sorted vector.
1265///
1266/// # Performance
1267///
1268/// This function is `O(n log n)` for `n` yielded elements.
1269fn unique_sorted(elements: impl Iterator<Item = ProjectionElementId>) -> Vec<ProjectionElementId> {
1270    elements.collect::<BTreeSet<_>>().into_iter().collect()
1271}
1272
1273/// Directed vertex-array build result.
1274type DirectedVertexArrays = (
1275    Vec<Vec<ProjectionRelationId>>,
1276    Vec<Vec<ProjectionRelationId>>,
1277    Vec<Vec<ProjectionElementId>>,
1278    Vec<Vec<ProjectionElementId>>,
1279);
1280
1281/// Converts a local element count into an ID.
1282fn projection_element_id(index: usize) -> Result<ProjectionElementId, DbError> {
1283    u32::try_from(index)
1284        .map(ProjectionElementId)
1285        .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Element))
1286}
1287
1288/// Converts a local relation count into an ID.
1289fn projection_relation_id(index: usize) -> Result<ProjectionRelationId, DbError> {
1290    u32::try_from(index)
1291        .map(ProjectionRelationId)
1292        .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Element))
1293}
1294
1295/// Converts a local incidence count into an ID.
1296fn projection_incidence_id(index: usize) -> Result<ProjectionIncidenceId, DbError> {
1297    u32::try_from(index)
1298        .map(ProjectionIncidenceId)
1299        .map_err(|_error| DbError::id_overflow(crate::error::IdFamily::Element))
1300}
1301
1302/// Returns a local element index.
1303fn local_index(id: ProjectionElementId) -> usize {
1304    usize::try_from(id.0).unwrap_or(usize::MAX)
1305}
1306
1307/// Returns a local relation index.
1308fn local_relation_index(id: ProjectionRelationId) -> usize {
1309    usize::try_from(id.0).unwrap_or(usize::MAX)
1310}
1311
1312/// Returns a local incidence index.
1313fn local_incidence_index(id: ProjectionIncidenceId) -> usize {
1314    usize::try_from(id.0).unwrap_or(usize::MAX)
1315}