trueno_graph/storage/
parquet.rs1use super::{CsrGraph, NodeId};
12use anyhow::{Context, Result};
13use arrow::array::{Float32Array, StringArray, UInt32Array};
14use arrow::datatypes::{DataType, Field, Schema};
15use arrow::record_batch::RecordBatch;
16use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
17use parquet::arrow::arrow_writer::ArrowWriter;
18use parquet::file::properties::WriterProperties;
19use std::fs::File;
20use std::path::Path;
21use std::sync::Arc;
22
23impl CsrGraph {
24 #[allow(clippy::unused_async)] pub async fn write_parquet<P: AsRef<Path>>(&self, path: P) -> Result<()> {
35 let base_path = path.as_ref();
36
37 self.write_edges_parquet(base_path)?;
39
40 self.write_nodes_parquet(base_path)?;
42
43 Ok(())
44 }
45
46 #[allow(clippy::unused_async)] pub async fn read_parquet<P: AsRef<Path>>(path: P) -> Result<Self> {
53 let base_path = path.as_ref();
54
55 let edges = Self::read_edges_parquet(base_path)?;
57
58 let mut graph = Self::from_edge_list(&edges)?;
60
61 let node_names = Self::read_nodes_parquet(base_path)?;
63 for (node_id, name) in node_names {
64 graph.set_node_name(node_id, name);
65 }
66
67 Ok(graph)
68 }
69
70 fn write_edges_parquet(&self, base_path: &Path) -> Result<()> {
71 let edges_path = format!("{}_edges.parquet", base_path.display());
72
73 let mut sources = Vec::new();
75 let mut targets = Vec::new();
76 let mut weights = Vec::new();
77
78 for (src, target_nodes, edge_weights) in self.iter_adjacency() {
79 for (dst, weight) in target_nodes.iter().zip(edge_weights.iter()) {
80 sources.push(src.0);
81 targets.push(*dst);
82 weights.push(*weight);
83 }
84 }
85
86 let schema = Arc::new(Schema::new(vec![
88 Field::new("source", DataType::UInt32, false),
89 Field::new("target", DataType::UInt32, false),
90 Field::new("weight", DataType::Float32, false),
91 ]));
92
93 let source_array = Arc::new(UInt32Array::from(sources));
95 let target_array = Arc::new(UInt32Array::from(targets));
96 let weight_array = Arc::new(Float32Array::from(weights));
97
98 let batch =
100 RecordBatch::try_new(schema.clone(), vec![source_array, target_array, weight_array])
101 .context("Failed to create RecordBatch")?;
102
103 let file =
105 File::create(&edges_path).with_context(|| format!("Failed to create {edges_path}"))?;
106
107 let props = WriterProperties::builder()
108 .set_compression(parquet::basic::Compression::ZSTD(parquet::basic::ZstdLevel::try_new(
109 3,
110 )?))
111 .build();
112
113 let mut writer = ArrowWriter::try_new(file, schema, Some(props))?;
114 writer.write(&batch)?;
115 writer.close()?;
116
117 Ok(())
118 }
119
120 fn write_nodes_parquet(&self, base_path: &Path) -> Result<()> {
121 let nodes_path = format!("{}_nodes.parquet", base_path.display());
122
123 let mut node_ids = Vec::new();
125 let mut names = Vec::new();
126
127 for node_id in 0..self.num_nodes() {
128 #[allow(clippy::cast_possible_truncation)] let node_u32 = node_id as u32;
130 node_ids.push(node_u32);
131 let name = self
132 .get_node_name(NodeId(node_u32))
133 .unwrap_or(&format!("node_{node_id}"))
134 .to_string();
135 names.push(name);
136 }
137
138 let schema = Arc::new(Schema::new(vec![
140 Field::new("node_id", DataType::UInt32, false),
141 Field::new("name", DataType::Utf8, false),
142 ]));
143
144 let node_id_array = Arc::new(UInt32Array::from(node_ids));
146 let name_array = Arc::new(StringArray::from(names));
147
148 let batch = RecordBatch::try_new(schema.clone(), vec![node_id_array, name_array])
150 .context("Failed to create nodes RecordBatch")?;
151
152 let file =
154 File::create(&nodes_path).with_context(|| format!("Failed to create {nodes_path}"))?;
155
156 let props = WriterProperties::builder()
157 .set_compression(parquet::basic::Compression::ZSTD(parquet::basic::ZstdLevel::try_new(
158 3,
159 )?))
160 .build();
161
162 let mut writer = ArrowWriter::try_new(file, schema, Some(props))?;
163 writer.write(&batch)?;
164 writer.close()?;
165
166 Ok(())
167 }
168
169 fn read_edges_parquet(base_path: &Path) -> Result<Vec<(NodeId, NodeId, f32)>> {
170 let edges_path = format!("{}_edges.parquet", base_path.display());
171
172 let file =
173 File::open(&edges_path).with_context(|| format!("Failed to open {edges_path}"))?;
174
175 let reader = ParquetRecordBatchReaderBuilder::try_new(file)?.build()?;
176
177 let mut edges = Vec::new();
178
179 for batch_result in reader {
180 let batch: RecordBatch = batch_result?;
181
182 let sources = batch
183 .column(0)
184 .as_any()
185 .downcast_ref::<UInt32Array>()
186 .context("Invalid source column type")?;
187
188 let targets = batch
189 .column(1)
190 .as_any()
191 .downcast_ref::<UInt32Array>()
192 .context("Invalid target column type")?;
193
194 let weights = batch
195 .column(2)
196 .as_any()
197 .downcast_ref::<Float32Array>()
198 .context("Invalid weight column type")?;
199
200 for i in 0..batch.num_rows() {
201 edges.push((NodeId(sources.value(i)), NodeId(targets.value(i)), weights.value(i)));
202 }
203 }
204
205 Ok(edges)
206 }
207
208 fn read_nodes_parquet(base_path: &Path) -> Result<Vec<(NodeId, String)>> {
209 let nodes_path = format!("{}_nodes.parquet", base_path.display());
210
211 let file =
212 File::open(&nodes_path).with_context(|| format!("Failed to open {nodes_path}"))?;
213
214 let reader = ParquetRecordBatchReaderBuilder::try_new(file)?.build()?;
215
216 let mut nodes = Vec::new();
217
218 for batch_result in reader {
219 let batch: RecordBatch = batch_result?;
220
221 let node_ids = batch
222 .column(0)
223 .as_any()
224 .downcast_ref::<UInt32Array>()
225 .context("Invalid node_id column type")?;
226
227 let names = batch
228 .column(1)
229 .as_any()
230 .downcast_ref::<StringArray>()
231 .context("Invalid name column type")?;
232
233 for i in 0..batch.num_rows() {
234 nodes.push((NodeId(node_ids.value(i)), names.value(i).to_string()));
235 }
236 }
237
238 Ok(nodes)
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use tempfile::tempdir;
246
247 #[tokio::test]
248 async fn test_parquet_roundtrip() {
249 let dir = tempdir().unwrap();
250 let path = dir.path().join("test_graph");
251
252 let mut graph = CsrGraph::new();
254 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
255 graph.add_edge(NodeId(0), NodeId(2), 2.0).unwrap();
256 graph.add_edge(NodeId(1), NodeId(2), 3.0).unwrap();
257
258 graph.set_node_name(NodeId(0), "main".to_string());
259 graph.set_node_name(NodeId(1), "parse_args".to_string());
260 graph.set_node_name(NodeId(2), "validate".to_string());
261
262 graph.write_parquet(&path).await.unwrap();
264
265 let loaded = CsrGraph::read_parquet(&path).await.unwrap();
267
268 assert_eq!(loaded.num_nodes(), graph.num_nodes());
270 assert_eq!(loaded.num_edges(), graph.num_edges());
271
272 assert_eq!(loaded.outgoing_neighbors(NodeId(0)).unwrap(), &[1, 2]);
274
275 assert_eq!(loaded.get_node_name(NodeId(0)), Some("main"));
277 assert_eq!(loaded.get_node_name(NodeId(1)), Some("parse_args"));
278 assert_eq!(loaded.get_node_name(NodeId(2)), Some("validate"));
279 }
280
281 #[tokio::test]
282 async fn test_empty_graph_parquet() {
283 let dir = tempdir().unwrap();
284 let path = dir.path().join("empty_graph");
285
286 let graph = CsrGraph::new();
287 graph.write_parquet(&path).await.unwrap();
288
289 let loaded = CsrGraph::read_parquet(&path).await.unwrap();
290 assert_eq!(loaded.num_nodes(), 0);
291 assert_eq!(loaded.num_edges(), 0);
292 }
293}