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