1use std::fs::File;
8use std::io::{BufReader, BufWriter};
9use std::path::Path;
10
11use arrow::datatypes::SchemaRef;
12use arrow::ipc::reader::{FileReader, StreamReader};
13use arrow::ipc::writer::{FileWriter, StreamWriter};
14use serde::{Deserialize, Serialize};
15
16pub const INDEX: &str = "results.json";
18pub const SCHEMA_VERSION: &str = "dre/saved-results/v1";
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct Index {
23 pub schema_version: String,
24 pub run_id: String,
26 pub results: Vec<SavedResult>,
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct SavedResult {
32 pub query: String,
33 pub name: String,
35 pub connection: String,
36 pub rows: u64,
37 #[serde(default)]
39 pub sql_sha256: Option<String>,
40 pub file: String,
42}
43
44impl Index {
45 pub fn find(&self, query: &str) -> Option<&SavedResult> {
46 self.results.iter().find(|r| r.query == query)
47 }
48}
49
50pub fn write_index(dir: &Path, index: &Index) -> std::io::Result<()> {
52 crate::fs::write_atomic(&dir.join(INDEX), serde_json::to_string_pretty(index)?.as_bytes())
53}
54
55pub fn read_index(dir: &Path) -> Result<Index, String> {
57 let path = dir.join(INDEX);
58 let text = std::fs::read_to_string(&path).map_err(|e| format!("can't read {}: {e}", path.display()))?;
59 let index: Index =
60 serde_json::from_str(&text).map_err(|e| format!("{} is malformed: {e}", path.display()))?;
61 if index.schema_version != SCHEMA_VERSION {
62 return Err(format!(
63 "{} is `{}`, which this DRE doesn't read (it reads `{SCHEMA_VERSION}`)",
64 path.display(),
65 index.schema_version
66 ));
67 }
68 Ok(index)
69}
70
71pub fn spool_to_stream(spool: &Path, to: &Path) -> Result<(), String> {
73 let reader = FileReader::try_new(File::open(spool).map_err(|e| e.to_string())?, None)
74 .map_err(|e| e.to_string())?;
75 let schema = reader.schema();
76 let mut w = StreamWriter::try_new(
77 BufWriter::new(File::create(to).map_err(|e| e.to_string())?),
78 &schema,
79 )
80 .map_err(|e| e.to_string())?;
81 for b in reader {
82 w.write(&b.map_err(|e| e.to_string())?)
83 .map_err(|e| e.to_string())?;
84 }
85 w.finish().map_err(|e| e.to_string())
86}
87
88pub fn stream_to_spool(from: &Path, spool: &Path) -> Result<(SchemaRef, u64), String> {
90 let reader = StreamReader::try_new(
91 BufReader::new(File::open(from).map_err(|e| format!("can't read {}: {e}", from.display()))?),
92 None,
93 )
94 .map_err(|e| format!("{} isn't an Arrow IPC stream: {e}", from.display()))?;
95 let schema = reader.schema();
96 let mut w = FileWriter::try_new(File::create(spool).map_err(|e| e.to_string())?, &schema)
97 .map_err(|e| e.to_string())?;
98 let mut rows = 0u64;
99 for b in reader {
100 let b = b.map_err(|e| format!("{}: {e}", from.display()))?;
101 rows += b.num_rows() as u64;
102 w.write(&b).map_err(|e| e.to_string())?;
103 }
104 w.finish().map_err(|e| e.to_string())?;
105 Ok((schema, rows))
106}
107
108#[cfg(test)]
109mod tests {
110 use std::sync::Arc;
111
112 use arrow::array::{ArrayRef, Decimal128Array, RecordBatch, StringArray};
113
114 use super::*;
115
116 #[test]
117 fn a_result_round_trips_exactly() {
118 let dir = tempfile::tempdir().unwrap();
119 let batch = RecordBatch::try_from_iter([
120 (
121 "amount",
122 Arc::new(
123 Decimal128Array::from(vec![Some(12345), None])
124 .with_precision_and_scale(18, 2)
125 .unwrap(),
126 ) as ArrayRef,
127 ),
128 (
129 "name",
130 Arc::new(StringArray::from(vec![Some("a"), Some("b")])) as ArrayRef,
131 ),
132 ])
133 .unwrap();
134 let spool = dir.path().join("0.arrow");
135 let mut w = FileWriter::try_new(File::create(&spool).unwrap(), &batch.schema()).unwrap();
136 w.write(&batch).unwrap();
137 w.finish().unwrap();
138 let stream = dir.path().join("q.arrows");
139 spool_to_stream(&spool, &stream).unwrap();
140 let back = dir.path().join("1.arrow");
141 let (schema, rows) = stream_to_spool(&stream, &back).unwrap();
142 assert_eq!(rows, 2);
143 assert_eq!(schema, batch.schema());
144 let read: Vec<RecordBatch> = FileReader::try_new(File::open(&back).unwrap(), None)
145 .unwrap()
146 .map(Result::unwrap)
147 .collect();
148 assert_eq!(read, vec![batch]);
149 }
150}