Skip to main content

hawdb_core/
projection.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Storage-neutral projection contract shared by the graph kernel, the
16//! analytics crate, and the embedded facade.
17//!
18//! These items live below both `hawdb-storage` and `hawdb-analytics` so the
19//! kernel can materialize projected graphs without an inverted dependency on
20//! the analytics crate.
21
22use crate::ids::{NodeRecord, RelRecord};
23
24/// Control flow a projection scan visitor returns to the scan driver.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ProjectionScanControl {
27    Continue,
28    Stop,
29}
30
31/// Storage-neutral source consumed while building an immutable projection.
32pub trait ProjectionSource {
33    /// Admit cumulative graph storage before node/adjacency allocation. Query
34    /// sources retain this reservation until the projection's owner takes it.
35    /// Standalone sources may keep their existing projection-only local budget.
36    fn admit_projection_memory(&self, _total_bytes: usize) -> Result<(), String> {
37        Ok(())
38    }
39
40    fn visit_projection_nodes(
41        &self,
42        visitor: &mut dyn FnMut(NodeRecord) -> ProjectionScanControl,
43    ) -> Result<ProjectionScanControl, String>;
44
45    fn visit_projection_relationships(
46        &self,
47        visitor: &mut dyn FnMut(RelRecord) -> ProjectionScanControl,
48    ) -> Result<ProjectionScanControl, String>;
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ProjectionLayout {
53    Outgoing,
54    Incoming,
55    Bidirectional,
56    Undirected,
57}
58
59impl ProjectionLayout {
60    pub fn as_str(self) -> &'static str {
61        match self {
62            Self::Outgoing => "outgoing",
63            Self::Incoming => "incoming",
64            Self::Bidirectional => "bidirectional",
65            Self::Undirected => "undirected",
66        }
67    }
68
69    #[doc(hidden)]
70    pub fn stores_outgoing(self) -> bool {
71        matches!(
72            self,
73            Self::Outgoing | Self::Bidirectional | Self::Undirected
74        )
75    }
76
77    #[doc(hidden)]
78    pub fn stores_incoming(self) -> bool {
79        matches!(self, Self::Incoming | Self::Bidirectional)
80    }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct ProjectionMemoryBudget {
85    max_bytes: Option<std::num::NonZeroUsize>,
86}
87
88impl ProjectionMemoryBudget {
89    pub const fn unlimited() -> Self {
90        Self { max_bytes: None }
91    }
92
93    pub const fn new(max_bytes: std::num::NonZeroUsize) -> Self {
94        Self {
95            max_bytes: Some(max_bytes),
96        }
97    }
98
99    pub fn max_bytes(self) -> Option<usize> {
100        self.max_bytes.map(std::num::NonZeroUsize::get)
101    }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct ProjectionMemoryEstimate {
106    pub layout: ProjectionLayout,
107    pub node_count: usize,
108    pub relationship_count: usize,
109    pub projected_edge_count: usize,
110    pub estimated_bytes: usize,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct GraphAlgorithmMemoryEstimate {
115    pub projection_bytes: usize,
116    pub algorithm_peak_bytes: usize,
117    pub result_bytes: usize,
118    pub total_peak_bytes: usize,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct ProjectionMemoryAdmissionError {
123    pub estimate: ProjectionMemoryEstimate,
124    pub budget_bytes: usize,
125    pub storage_error: Option<String>,
126}
127
128impl std::fmt::Display for ProjectionMemoryAdmissionError {
129    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        if let Some(error) = &self.storage_error {
131            return write!(
132                formatter,
133                "analytics projection storage scan failed: {error}"
134            );
135        }
136        write!(
137            formatter,
138            "analytics projection layout '{}' requires an estimated {} bytes for {} nodes and {} relationships, exceeding the {} byte budget",
139            self.estimate.layout.as_str(),
140            self.estimate.estimated_bytes,
141            self.estimate.node_count,
142            self.estimate.relationship_count,
143            self.budget_bytes,
144        )
145    }
146}
147
148impl std::error::Error for ProjectionMemoryAdmissionError {}
149
150#[doc(hidden)]
151impl ProjectionMemoryAdmissionError {
152    pub fn storage(error: impl std::fmt::Display) -> Self {
153        Self {
154            estimate: ProjectionMemoryEstimate {
155                layout: ProjectionLayout::Bidirectional,
156                node_count: 0,
157                relationship_count: 0,
158                projected_edge_count: 0,
159                estimated_bytes: 0,
160            },
161            budget_bytes: 0,
162            storage_error: Some(error.to_string()),
163        }
164    }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq)]
168pub struct PageRankOptions {
169    pub iterations: usize,
170    pub damping: f64,
171}
172
173impl Default for PageRankOptions {
174    fn default() -> Self {
175        Self {
176            iterations: 20,
177            damping: 0.85,
178        }
179    }
180}
181
182#[derive(Debug, Clone, Copy, PartialEq)]
183pub struct PageRankScore {
184    pub node: crate::ids::NodeId,
185    pub score: f64,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq)]
189pub struct LouvainOptions {
190    pub max_iterations: usize,
191    pub max_levels: usize,
192}
193
194impl Default for LouvainOptions {
195    fn default() -> Self {
196        Self {
197            max_iterations: 20,
198            max_levels: 1,
199        }
200    }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub struct CommunityAssignment {
205    pub node: crate::ids::NodeId,
206    pub community: crate::ids::NodeId,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub struct HierarchicalCommunityAssignment {
211    pub level: usize,
212    pub node: crate::ids::NodeId,
213    pub community: crate::ids::NodeId,
214}