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