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 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 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}