oxgraph_postgres/
build.rs1use alloc::{
4 collections::{BTreeMap, BTreeSet},
5 vec::Vec,
6};
7
8use oxgraph_csr::build::{GraphBuilder, GraphNodeId, export_csr_snapshot};
9
10use crate::{
11 artifact::{PostgresMetadata, attach_postgres_sections},
12 catalog::{NodeKey, RegisteredEdge},
13 error::{BuildError, PostgresGraphError},
14};
15
16#[expect(
25 clippy::redundant_pub_crate,
26 reason = "shared build/sync helper in a private module"
27)]
28pub(crate) fn distinct_node_keys(edges: &[EdgeRow]) -> BTreeSet<NodeKey> {
29 let mut keys = BTreeSet::new();
30 for edge in edges {
31 keys.insert(edge.source);
32 keys.insert(edge.target);
33 }
34 keys
35}
36
37#[expect(
51 clippy::redundant_pub_crate,
52 reason = "shared build/sync dense-assignment primitive in a private module"
53)]
54pub(crate) fn dense_node_map_from_keys(
55 keys: BTreeSet<NodeKey>,
56) -> Result<BTreeMap<NodeKey, u32>, BuildError> {
57 let mut map = BTreeMap::new();
58 for (index, key) in keys.into_iter().enumerate() {
59 let dense = u32::try_from(index).map_err(|_| BuildError::NodeCountOverflow)?;
60 map.insert(key, dense);
61 }
62 Ok(map)
63}
64
65pub fn dense_node_map_from_edges(edges: &[EdgeRow]) -> Result<BTreeMap<NodeKey, u32>, BuildError> {
77 dense_node_map_from_keys(distinct_node_keys(edges))
78}
79
80#[must_use]
86pub const fn edge_row_from_scan(edge: &RegisteredEdge, source_pk: u64, target_pk: u64) -> EdgeRow {
87 EdgeRow {
88 source: NodeKey::registered(edge.source_table, source_pk),
89 target: NodeKey::registered(edge.target_table, target_pk),
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub struct EdgeRow {
96 pub source: NodeKey,
98 pub target: NodeKey,
100}
101
102#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
104pub struct DualTopologySnapshot;
105
106impl DualTopologySnapshot {
107 pub fn from_edge_rows(
121 edges: &[EdgeRow],
122 built_at_unix: u64,
123 ) -> Result<Vec<u8>, PostgresGraphError> {
124 if edges.is_empty() {
125 return Err(BuildError::EmptyEdges.into());
126 }
127
128 let key_to_dense = dense_node_map_from_keys(distinct_node_keys(edges))?;
129 let key_count = key_to_dense.len();
130
131 let mut builder = GraphBuilder::<u32, u32>::new();
132 for _ in 0..key_count {
133 builder.add_node()?;
134 }
135 for edge in edges {
136 let source = *key_to_dense
137 .get(&edge.source)
138 .ok_or(BuildError::MissingNodeKey)?;
139 let target = *key_to_dense
140 .get(&edge.target)
141 .ok_or(BuildError::MissingNodeKey)?;
142 builder.add_edge(GraphNodeId::new(source), GraphNodeId::new(target))?;
143 }
144
145 let frozen = builder.freeze()?;
146 let node_count = u32::try_from(key_count).map_err(|_| BuildError::NodeCountOverflow)?;
147 let edge_count =
148 u32::try_from(frozen.edge_ids().len()).map_err(|_| BuildError::EdgeCountOverflow)?;
149
150 let inbound_frozen = frozen.transpose()?;
151 let forward_bytes = export_csr_snapshot(&frozen)?;
152 let inbound_bytes = export_csr_snapshot(&inbound_frozen)?;
153 let metadata =
154 PostgresMetadata::new(node_count, edge_count, built_at_unix, true).with_reverse_index();
155 attach_postgres_sections(&forward_bytes, Some(&inbound_bytes), &metadata)
156 }
157
158 pub fn from_dense_u32_edges(
168 edges: &[(u32, u32)],
169 built_at_unix: u64,
170 ) -> Result<Vec<u8>, PostgresGraphError> {
171 if edges.is_empty() {
172 return Err(BuildError::EmptyEdges.into());
173 }
174 let max_index = edges
175 .iter()
176 .flat_map(|(source, target)| [*source, *target])
177 .max()
178 .unwrap_or(0);
179 let node_count = max_index
180 .checked_add(1)
181 .ok_or(BuildError::NodeCountOverflow)?;
182 Self::from_dense_u32_edges_with_node_count(node_count, edges, built_at_unix)
183 }
184
185 pub fn from_dense_u32_edges_with_node_count(
196 node_count: u32,
197 edges: &[(u32, u32)],
198 built_at_unix: u64,
199 ) -> Result<Vec<u8>, PostgresGraphError> {
200 if node_count == 0 {
201 return Err(BuildError::EmptyEdges.into());
202 }
203 let node_count_usize = node_count as usize;
204 for &(source, target) in edges {
205 if source >= node_count || target >= node_count {
206 return Err(BuildError::MissingNodeKey.into());
207 }
208 }
209
210 let mut forward_builder = GraphBuilder::<u32, u32>::new();
211 for _ in 0..node_count_usize {
212 forward_builder.add_node()?;
213 }
214 for &(source, target) in edges {
215 forward_builder.add_edge(GraphNodeId::new(source), GraphNodeId::new(target))?;
216 }
217 let forward_frozen = forward_builder.freeze()?;
218 let edge_count = u32::try_from(forward_frozen.edge_ids().len())
219 .map_err(|_| BuildError::EdgeCountOverflow)?;
220
221 let inbound_frozen = forward_frozen.transpose()?;
222 let forward_bytes = export_csr_snapshot(&forward_frozen)?;
223 let inbound_bytes = export_csr_snapshot(&inbound_frozen)?;
224 let metadata =
225 PostgresMetadata::new(node_count, edge_count, built_at_unix, true).with_reverse_index();
226 attach_postgres_sections(&forward_bytes, Some(&inbound_bytes), &metadata)
227 }
228}
229
230#[must_use]
236pub fn estimate_build(edges: &[EdgeRow]) -> BuildEstimate {
237 BuildEstimate {
238 node_count: distinct_node_keys(edges).len(),
239 edge_count: edges.len(),
240 }
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
245pub struct BuildEstimate {
246 pub node_count: usize,
248 pub edge_count: usize,
250}