Skip to main content

query_forge/
writers.rs

1use std::{fs, path::Path};
2
3use anyhow::{Context, Result};
4use rust_xlsxwriter::Workbook;
5
6use crate::{QueryResult, QueryValue};
7
8pub fn write_xlsx(result: &QueryResult, output_path: &Path) -> Result<()> {
9    let mut workbook = Workbook::new();
10    let worksheet = workbook.add_worksheet();
11
12    for (column, header) in result.columns.iter().enumerate() {
13        worksheet.write_string(0, column as u16, header)?;
14    }
15
16    for (row_index, row) in result.rows.iter().enumerate() {
17        for (column_index, value) in row.iter().enumerate() {
18            let excel_row = (row_index + 1) as u32;
19            let excel_column = column_index as u16;
20
21            match value {
22                QueryValue::Null => {}
23                QueryValue::Integer(number) => {
24                    worksheet.write_number(excel_row, excel_column, *number as f64)?;
25                }
26                QueryValue::Real(number) => {
27                    worksheet.write_number(excel_row, excel_column, *number)?;
28                }
29                QueryValue::Text(text) => {
30                    worksheet.write_string(excel_row, excel_column, text)?;
31                }
32            }
33        }
34    }
35
36    workbook
37        .save(output_path)
38        .with_context(|| format!("failed to write {}", output_path.display()))
39}
40
41pub fn write_parquet(result: &QueryResult, output_path: &Path) -> Result<()> {
42    use std::sync::Arc;
43
44    use parquet::basic::{ConvertedType, Repetition, Type as PhysicalType};
45    use parquet::column::writer::ColumnWriter;
46    use parquet::data_type::ByteArray;
47    use parquet::file::properties::WriterProperties;
48    use parquet::file::writer::SerializedFileWriter;
49    use parquet::schema::types::Type as ParquetType;
50
51    #[derive(Clone, Copy)]
52    enum ColKind {
53        Int64,
54        Double,
55        Bytes,
56    }
57
58    // Infer the physical type for each column from its values.
59    let col_kinds: Vec<ColKind> = (0..result.columns.len())
60        .map(|ci| {
61            let mut has_real = false;
62            let mut has_text = false;
63            for row in &result.rows {
64                match row.get(ci).unwrap_or(&QueryValue::Null) {
65                    QueryValue::Real(_) => has_real = true,
66                    QueryValue::Text(_) => has_text = true,
67                    _ => {}
68                }
69            }
70            if has_text {
71                ColKind::Bytes
72            } else if has_real {
73                ColKind::Double
74            } else {
75                ColKind::Int64
76            }
77        })
78        .collect();
79
80    // Build the Parquet schema.
81    let fields: Vec<Arc<ParquetType>> = result
82        .columns
83        .iter()
84        .zip(&col_kinds)
85        .map(|(name, kind)| {
86            let physical = match kind {
87                ColKind::Int64 => PhysicalType::INT64,
88                ColKind::Double => PhysicalType::DOUBLE,
89                ColKind::Bytes => PhysicalType::BYTE_ARRAY,
90            };
91            let mut builder = ParquetType::primitive_type_builder(name, physical)
92                .with_repetition(Repetition::OPTIONAL);
93            if matches!(kind, ColKind::Bytes) {
94                builder = builder.with_converted_type(ConvertedType::UTF8);
95            }
96            Arc::new(
97                builder
98                    .build()
99                    .with_context(|| format!("failed to build Parquet field '{name}'"))
100                    .expect("valid field"),
101            )
102        })
103        .collect();
104
105    let schema = Arc::new(
106        ParquetType::group_type_builder("schema")
107            .with_fields(fields)
108            .build()
109            .context("failed to build Parquet schema")?,
110    );
111
112    let props = Arc::new(WriterProperties::builder().build());
113    let file = fs::File::create(output_path)
114        .with_context(|| format!("failed to create {}", output_path.display()))?;
115    let mut file_writer = SerializedFileWriter::new(file, schema, props)
116        .context("failed to initialize Parquet writer")?;
117
118    let mut rg = file_writer
119        .next_row_group()
120        .context("failed to start Parquet row group")?;
121
122    for (col_idx, kind) in col_kinds.iter().enumerate() {
123        let def_levels: Vec<i16> = result
124            .rows
125            .iter()
126            .map(|row| match row.get(col_idx).unwrap_or(&QueryValue::Null) {
127                QueryValue::Null => 0,
128                _ => 1,
129            })
130            .collect();
131
132        let Some(mut col_writer) = rg
133            .next_column()
134            .context("failed to open Parquet column writer")?
135        else {
136            break;
137        };
138
139        match (kind, col_writer.untyped()) {
140            (ColKind::Int64, ColumnWriter::Int64ColumnWriter(w)) => {
141                let values: Vec<i64> = result
142                    .rows
143                    .iter()
144                    .filter_map(|row| match row.get(col_idx).unwrap_or(&QueryValue::Null) {
145                        QueryValue::Integer(v) => Some(*v),
146                        _ => None,
147                    })
148                    .collect();
149                w.write_batch(&values, Some(&def_levels), None)?;
150            }
151            (ColKind::Double, ColumnWriter::DoubleColumnWriter(w)) => {
152                let values: Vec<f64> = result
153                    .rows
154                    .iter()
155                    .filter_map(|row| match row.get(col_idx).unwrap_or(&QueryValue::Null) {
156                        QueryValue::Real(v) => Some(*v),
157                        QueryValue::Integer(v) => Some(*v as f64),
158                        _ => None,
159                    })
160                    .collect();
161                w.write_batch(&values, Some(&def_levels), None)?;
162            }
163            (ColKind::Bytes, ColumnWriter::ByteArrayColumnWriter(w)) => {
164                let values: Vec<ByteArray> = result
165                    .rows
166                    .iter()
167                    .filter_map(|row| match row.get(col_idx).unwrap_or(&QueryValue::Null) {
168                        QueryValue::Text(s) => Some(ByteArray::from(s.as_bytes().to_vec())),
169                        QueryValue::Integer(v) => Some(ByteArray::from(v.to_string().into_bytes())),
170                        QueryValue::Real(v) => Some(ByteArray::from(v.to_string().into_bytes())),
171                        _ => None,
172                    })
173                    .collect();
174                w.write_batch(&values, Some(&def_levels), None)?;
175            }
176            _ => unreachable!("column kind and writer type must agree"),
177        }
178
179        col_writer.close()?;
180    }
181
182    rg.close()?;
183    file_writer.close()?;
184
185    Ok(())
186}