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