1use crate::export::arrow_convert::{build_arrow_schema, convert_to_arrays, ArrowConvertError};
22use crate::query::{ColumnInfo, QueryMetadata, QueryResult, QueryRow};
23use arrow::datatypes::Schema;
24use arrow::record_batch::RecordBatch;
25use parquet::arrow::ArrowWriter;
26use parquet::basic::{Compression, ZstdLevel};
27use parquet::file::properties::WriterProperties;
28use std::fs::File;
29use std::io::Write;
30use std::sync::Arc;
31use thiserror::Error;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum ParquetCompression {
42 #[default]
44 Snappy,
45 Zstd,
47 Uncompressed,
49}
50
51impl ParquetCompression {
52 fn to_parquet(self) -> Compression {
54 match self {
55 ParquetCompression::Snappy => Compression::SNAPPY,
56 ParquetCompression::Zstd => Compression::ZSTD(ZstdLevel::default()),
57 ParquetCompression::Uncompressed => Compression::UNCOMPRESSED,
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
68pub struct ParquetExportOptions {
69 pub row_limit: Option<usize>,
71 pub row_group_size: usize,
73 pub compression: ParquetCompression,
75}
76
77impl Default for ParquetExportOptions {
78 fn default() -> Self {
79 Self {
80 row_limit: None,
81 row_group_size: 10_000,
82 compression: ParquetCompression::default(),
83 }
84 }
85}
86
87#[derive(Debug, Error)]
93pub enum ParquetExportError {
94 #[error("I/O error: {0}")]
96 Io(#[from] std::io::Error),
97 #[error("Arrow error: {0}")]
99 Arrow(#[from] arrow::error::ArrowError),
100 #[error("Parquet error: {0}")]
102 Parquet(#[from] ::parquet::errors::ParquetError),
103 #[error("{0}")]
105 InvalidValue(String),
106 #[error("invalid Parquet export options: {0}")]
108 InvalidOptions(String),
109}
110
111impl From<ArrowConvertError> for ParquetExportError {
112 fn from(e: ArrowConvertError) -> Self {
113 match e {
114 ArrowConvertError::Arrow(a) => ParquetExportError::Arrow(a),
115 ArrowConvertError::InvalidValue(s) => ParquetExportError::InvalidValue(s),
116 }
117 }
118}
119
120pub struct ParquetWriter;
125
126impl ParquetWriter {
127 pub fn write(
138 result: &QueryResult,
139 options: &ParquetExportOptions,
140 ) -> Result<Vec<u8>, ParquetExportError> {
141 if result.metadata.columns.is_empty() {
143 return Self::write_empty_parquet(options);
144 }
145
146 let schema = build_arrow_schema(&result.metadata.columns)?;
148
149 let rows_to_process = if let Some(limit) = options.row_limit {
151 &result.rows[..result.rows.len().min(limit)]
152 } else {
153 &result.rows
154 };
155
156 let arrays = convert_to_arrays(&result.metadata.columns, rows_to_process)?;
158
159 let batch = RecordBatch::try_new(Arc::new(schema), arrays)?;
161
162 Self::write_parquet(&batch, options.compression)
164 }
165
166 fn write_empty_parquet(options: &ParquetExportOptions) -> Result<Vec<u8>, ParquetExportError> {
168 let schema = Schema::empty();
169 let batch = RecordBatch::new_empty(Arc::new(schema));
170 Self::write_parquet(&batch, options.compression)
171 }
172
173 pub(crate) fn build_schema(columns: &[ColumnInfo]) -> Result<Schema, ParquetExportError> {
179 build_arrow_schema(columns).map_err(Into::into)
180 }
181
182 pub(crate) fn convert_to_arrays(
186 columns: &[ColumnInfo],
187 rows: &[QueryRow],
188 ) -> Result<Vec<arrow::array::ArrayRef>, ParquetExportError> {
189 convert_to_arrays(columns, rows).map_err(Into::into)
190 }
191
192 fn write_parquet(
194 batch: &RecordBatch,
195 compression: ParquetCompression,
196 ) -> Result<Vec<u8>, ParquetExportError> {
197 let mut buffer = Vec::new();
198
199 let props = WriterProperties::builder()
200 .set_compression(compression.to_parquet())
201 .build();
202
203 let mut writer = ArrowWriter::try_new(&mut buffer, batch.schema(), Some(props))?;
204 writer.write(batch)?;
205 writer.close()?;
206
207 Ok(buffer)
208 }
209}
210
211pub struct StreamingParquetWriter<W: Write + Send> {
250 writer: Option<ArrowWriter<W>>,
252 schema: Arc<Schema>,
254 columns: Vec<ColumnInfo>,
256 row_buffer: Vec<QueryRow>,
258 row_group_size: usize,
260 rows_written: u64,
262}
263
264impl<W: Write + Send> StreamingParquetWriter<W> {
265 pub fn new(
277 output: W,
278 metadata: &QueryMetadata,
279 options: &ParquetExportOptions,
280 ) -> Result<Self, ParquetExportError> {
281 if options.row_group_size == 0 {
282 return Err(ParquetExportError::InvalidOptions(
283 "row_group_size must be greater than 0".to_string(),
284 ));
285 }
286
287 let schema = Arc::new(ParquetWriter::build_schema(&metadata.columns)?);
289
290 let props = WriterProperties::builder()
295 .set_compression(options.compression.to_parquet())
296 .set_max_row_group_size(options.row_group_size)
297 .build();
298
299 let arrow_writer = ArrowWriter::try_new(output, Arc::clone(&schema), Some(props))?;
300
301 Ok(Self {
302 writer: Some(arrow_writer),
303 schema,
304 columns: metadata.columns.clone(),
305 row_buffer: Vec::with_capacity(options.row_group_size),
306 row_group_size: options.row_group_size,
307 rows_written: 0,
308 })
309 }
310
311 pub fn write_chunk(&mut self, rows: &[QueryRow]) -> Result<usize, ParquetExportError> {
316 self.row_buffer.extend(rows.iter().cloned());
317 self.rows_written += rows.len() as u64;
318
319 let mut flushed = 0;
320 while self.row_buffer.len() >= self.row_group_size {
321 let chunk: Vec<QueryRow> = self.row_buffer.drain(..self.row_group_size).collect();
322 self.write_row_group(&chunk)?;
323 flushed += self.row_group_size;
324 }
325
326 Ok(flushed)
327 }
328
329 pub fn finalize(&mut self) -> Result<(), ParquetExportError> {
334 if !self.row_buffer.is_empty() {
335 let remaining = std::mem::take(&mut self.row_buffer);
336 self.write_row_group(&remaining)?;
337 }
338
339 if let Some(writer) = self.writer.take() {
340 writer.close()?;
341 }
342
343 Ok(())
344 }
345
346 pub fn rows_written(&self) -> u64 {
348 self.rows_written
349 }
350
351 fn write_row_group(&mut self, rows: &[QueryRow]) -> Result<(), ParquetExportError> {
353 let writer = self.writer.as_mut().ok_or_else(|| {
354 ParquetExportError::InvalidOptions(
355 "writer already finalized - cannot write more rows".to_string(),
356 )
357 })?;
358
359 let arrays = ParquetWriter::convert_to_arrays(&self.columns, rows)?;
360 let batch = RecordBatch::try_new(Arc::clone(&self.schema), arrays)?;
361 writer.write(&batch)?;
362
363 Ok(())
364 }
365}
366
367pub fn create_streaming_parquet_writer(
371 file: File,
372 metadata: &QueryMetadata,
373 options: &ParquetExportOptions,
374) -> Result<StreamingParquetWriter<File>, ParquetExportError> {
375 StreamingParquetWriter::new(file, metadata, options)
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::query::{ColumnInfo, QueryRow};
382 use crate::{RowKey, Value};
383 use arrow::array::BinaryArray;
384 use arrow::array::{Array, FixedSizeBinaryArray, StringArray};
385 use bytes::Bytes;
386 use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
387 use std::collections::HashMap;
388 use std::error::Error as StdError;
389
390 fn default_options() -> ParquetExportOptions {
391 ParquetExportOptions::default()
392 }
393
394 fn read_parquet_back(bytes: &[u8]) -> Result<RecordBatch, Box<dyn StdError>> {
396 let bytes = Bytes::copy_from_slice(bytes);
398 let builder = ParquetRecordBatchReaderBuilder::try_new(bytes)?;
399 let mut reader = builder.build()?;
400 reader
401 .next()
402 .ok_or_else(|| "No batches in Parquet file".to_string())?
403 .map_err(|e| Box::new(e) as Box<dyn StdError>)
404 }
405
406 #[test]
407 fn test_empty_result() {
408 let result = QueryResult::new();
409 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
410
411 assert!(!bytes.is_empty());
413 assert_eq!(&bytes[0..4], b"PAR1");
415 }
416
417 #[test]
418 fn test_boolean_values() {
419 use crate::types::DataType;
420 let mut result = QueryResult::new();
421 result.metadata.columns = vec![ColumnInfo::new(
422 "bool_col".to_string(),
423 DataType::Boolean,
424 true,
425 0,
426 )];
427
428 let mut values = HashMap::new();
429 values.insert("bool_col".to_string(), Value::Boolean(true));
430 let row = QueryRow::with_values(RowKey::new(vec![1]), values);
431 result.rows.push(row);
432
433 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
434 let batch = read_parquet_back(&bytes).unwrap();
435
436 assert_eq!(batch.num_rows(), 1);
437 assert_eq!(batch.num_columns(), 1);
438 }
439
440 #[test]
441 fn test_integer_types() {
442 use crate::types::DataType;
443 let mut result = QueryResult::new();
444 result.metadata.columns = vec![
445 ColumnInfo::new("tiny".to_string(), DataType::TinyInt, false, 0),
446 ColumnInfo::new("small".to_string(), DataType::SmallInt, false, 1),
447 ColumnInfo::new("int".to_string(), DataType::Integer, false, 2),
448 ColumnInfo::new("big".to_string(), DataType::BigInt, false, 3),
449 ];
450
451 let mut values = HashMap::new();
452 values.insert("tiny".to_string(), Value::TinyInt(127));
453 values.insert("small".to_string(), Value::SmallInt(32767));
454 values.insert("int".to_string(), Value::Integer(2147483647));
455 values.insert("big".to_string(), Value::BigInt(9223372036854775807));
456 let row = QueryRow::with_values(RowKey::new(vec![1]), values);
457 result.rows.push(row);
458
459 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
460 let batch = read_parquet_back(&bytes).unwrap();
461
462 assert_eq!(batch.num_rows(), 1);
463 assert_eq!(batch.num_columns(), 4);
464 }
465
466 #[test]
467 fn test_float_types() {
468 use crate::types::DataType;
469 let mut result = QueryResult::new();
470 result.metadata.columns = vec![
471 ColumnInfo::new("f32".to_string(), DataType::Float32, false, 0),
472 ColumnInfo::new("f64".to_string(), DataType::Float, false, 1),
473 ];
474
475 let mut values = HashMap::new();
476 values.insert("f32".to_string(), Value::Float32(3.5));
477 values.insert("f64".to_string(), Value::Float(2.75));
478 let row = QueryRow::with_values(RowKey::new(vec![1]), values);
479 result.rows.push(row);
480
481 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
482 let batch = read_parquet_back(&bytes).unwrap();
483
484 assert_eq!(batch.num_rows(), 1);
485 assert_eq!(batch.num_columns(), 2);
486 }
487
488 #[test]
489 fn test_text_values() {
490 use crate::types::DataType;
491 let mut result = QueryResult::new();
492 result.metadata.columns = vec![ColumnInfo::new(
493 "text_col".to_string(),
494 DataType::Text,
495 false,
496 0,
497 )];
498
499 let mut values = HashMap::new();
500 values.insert(
501 "text_col".to_string(),
502 Value::Text("Hello, Parquet!".to_string()),
503 );
504 let row = QueryRow::with_values(RowKey::new(vec![1]), values);
505 result.rows.push(row);
506
507 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
508 let batch = read_parquet_back(&bytes).unwrap();
509
510 assert_eq!(batch.num_rows(), 1);
511 let col = batch.column(0);
512 let string_array = col.as_any().downcast_ref::<StringArray>().unwrap();
513 assert_eq!(string_array.value(0), "Hello, Parquet!");
514 }
515
516 #[test]
517 fn test_blob_values() {
518 use crate::types::DataType;
519 let mut result = QueryResult::new();
520 result.metadata.columns = vec![ColumnInfo::new(
521 "blob_col".to_string(),
522 DataType::Blob,
523 false,
524 0,
525 )];
526
527 let mut values = HashMap::new();
528 values.insert(
529 "blob_col".to_string(),
530 Value::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF]),
531 );
532 let row = QueryRow::with_values(RowKey::new(vec![1]), values);
533 result.rows.push(row);
534
535 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
536 let batch = read_parquet_back(&bytes).unwrap();
537
538 assert_eq!(batch.num_rows(), 1);
539 let col = batch.column(0);
540 let binary_array = col.as_any().downcast_ref::<BinaryArray>().unwrap();
541 assert_eq!(binary_array.value(0), &[0xDE, 0xAD, 0xBE, 0xEF]);
542 }
543
544 #[test]
545 fn test_timestamp_values() {
546 use crate::types::DataType;
547 let mut result = QueryResult::new();
548 result.metadata.columns = vec![ColumnInfo::new(
549 "ts_col".to_string(),
550 DataType::Timestamp,
551 false,
552 0,
553 )];
554
555 let mut values = HashMap::new();
557 values.insert("ts_col".to_string(), Value::Timestamp(1673778645123));
558 let row = QueryRow::with_values(RowKey::new(vec![1]), values);
559 result.rows.push(row);
560
561 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
562 let batch = read_parquet_back(&bytes).unwrap();
563
564 assert_eq!(batch.num_rows(), 1);
565 }
566
567 #[test]
568 fn test_uuid_values() {
569 use crate::types::DataType;
570 let mut result = QueryResult::new();
571 result.metadata.columns = vec![ColumnInfo::new(
572 "uuid_col".to_string(),
573 DataType::Uuid,
574 false,
575 0,
576 )];
577
578 let uuid_bytes = [
579 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
580 0x77, 0x88,
581 ];
582
583 let mut values = HashMap::new();
584 values.insert("uuid_col".to_string(), Value::Uuid(uuid_bytes));
585 let row = QueryRow::with_values(RowKey::new(vec![1]), values);
586 result.rows.push(row);
587
588 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
589 let batch = read_parquet_back(&bytes).unwrap();
590
591 assert_eq!(batch.num_rows(), 1);
592 let col = batch.column(0);
593 let uuid_array = col.as_any().downcast_ref::<FixedSizeBinaryArray>().unwrap();
594 assert_eq!(uuid_array.value(0), uuid_bytes);
595 }
596
597 #[test]
598 fn test_null_values() {
599 use crate::types::DataType;
600 let mut result = QueryResult::new();
601 result.metadata.columns = vec![ColumnInfo::new(
602 "nullable_col".to_string(),
603 DataType::Text,
604 true,
605 0,
606 )];
607
608 let mut values1 = HashMap::new();
610 values1.insert(
611 "nullable_col".to_string(),
612 Value::Text("present".to_string()),
613 );
614 result
615 .rows
616 .push(QueryRow::with_values(RowKey::new(vec![1]), values1));
617
618 let mut values2 = HashMap::new();
619 values2.insert("nullable_col".to_string(), Value::Null);
620 result
621 .rows
622 .push(QueryRow::with_values(RowKey::new(vec![2]), values2));
623
624 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
625 let batch = read_parquet_back(&bytes).unwrap();
626
627 assert_eq!(batch.num_rows(), 2);
628 let col = batch.column(0);
629 let string_array = col.as_any().downcast_ref::<StringArray>().unwrap();
630 assert!(string_array.is_valid(0));
631 assert!(!string_array.is_valid(1)); }
633
634 #[test]
635 fn test_list_values() {
636 use crate::types::DataType;
637 let mut result = QueryResult::new();
638 result.metadata.columns = vec![ColumnInfo::new(
639 "list_col".to_string(),
640 DataType::List,
641 false,
642 0,
643 )];
644
645 let mut values = HashMap::new();
646 values.insert(
647 "list_col".to_string(),
648 Value::List(vec![
649 Value::Integer(1),
650 Value::Integer(2),
651 Value::Integer(3),
652 ]),
653 );
654 let row = QueryRow::with_values(RowKey::new(vec![1]), values);
655 result.rows.push(row);
656
657 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
658 let batch = read_parquet_back(&bytes).unwrap();
659
660 assert_eq!(batch.num_rows(), 1);
661 }
662
663 #[test]
664 fn test_map_values() {
665 use crate::types::DataType;
666 let mut result = QueryResult::new();
667 result.metadata.columns = vec![ColumnInfo::new(
668 "map_col".to_string(),
669 DataType::Map,
670 false,
671 0,
672 )];
673
674 let mut values = HashMap::new();
675 values.insert(
676 "map_col".to_string(),
677 Value::Map(vec![
678 (Value::Text("key1".to_string()), Value::Integer(1)),
679 (Value::Text("key2".to_string()), Value::Integer(2)),
680 ]),
681 );
682 let row = QueryRow::with_values(RowKey::new(vec![1]), values);
683 result.rows.push(row);
684
685 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
686 let batch = read_parquet_back(&bytes).unwrap();
687
688 assert_eq!(batch.num_rows(), 1);
689 }
690
691 #[test]
692 fn test_config_limit() {
693 use crate::types::DataType;
694 let mut result = QueryResult::new();
695 result.metadata.columns = vec![ColumnInfo::new(
696 "id".to_string(),
697 DataType::Integer,
698 false,
699 0,
700 )];
701
702 for i in 1..=10 {
704 let mut values = HashMap::new();
705 values.insert("id".to_string(), Value::Integer(i));
706 let row = QueryRow::with_values(RowKey::new(vec![i as u8]), values);
707 result.rows.push(row);
708 }
709
710 let options = ParquetExportOptions {
712 row_limit: Some(3),
713 ..Default::default()
714 };
715 let bytes = ParquetWriter::write(&result, &options).unwrap();
716 let batch = read_parquet_back(&bytes).unwrap();
717
718 assert_eq!(
719 batch.num_rows(),
720 3,
721 "Limit should restrict output to 3 rows"
722 );
723 }
724
725 #[test]
726 fn test_multiple_rows() {
727 use crate::types::DataType;
728 let mut result = QueryResult::new();
729 result.metadata.columns = vec![
730 ColumnInfo::new("id".to_string(), DataType::Integer, false, 0),
731 ColumnInfo::new("name".to_string(), DataType::Text, false, 1),
732 ];
733
734 for i in 1..=5 {
735 let mut values = HashMap::new();
736 values.insert("id".to_string(), Value::Integer(i));
737 values.insert("name".to_string(), Value::Text(format!("row_{i}")));
738 let row = QueryRow::with_values(RowKey::new(vec![i as u8]), values);
739 result.rows.push(row);
740 }
741
742 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
743 let batch = read_parquet_back(&bytes).unwrap();
744
745 assert_eq!(batch.num_rows(), 5);
746 assert_eq!(batch.num_columns(), 2);
747 }
748
749 #[test]
750 fn test_counter_values() {
751 use crate::types::DataType;
752 let mut result = QueryResult::new();
753 result.metadata.columns = vec![ColumnInfo::new(
754 "counter_col".to_string(),
755 DataType::BigInt, false,
757 0,
758 )];
759
760 let mut values = HashMap::new();
761 values.insert("counter_col".to_string(), Value::Counter(1000000));
762 let row = QueryRow::with_values(RowKey::new(vec![1]), values);
763 result.rows.push(row);
764
765 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
766 let batch = read_parquet_back(&bytes).unwrap();
767
768 assert_eq!(batch.num_rows(), 1);
769 }
770
771 #[test]
772 fn test_parquet_magic_bytes() {
773 use crate::types::DataType;
774 let mut result = QueryResult::new();
775 result.metadata.columns = vec![ColumnInfo::new(
776 "col".to_string(),
777 DataType::Integer,
778 false,
779 0,
780 )];
781
782 let mut values = HashMap::new();
783 values.insert("col".to_string(), Value::Integer(42));
784 result
785 .rows
786 .push(QueryRow::with_values(RowKey::new(vec![1]), values));
787
788 let bytes = ParquetWriter::write(&result, &default_options()).unwrap();
789
790 assert_eq!(&bytes[0..4], b"PAR1", "Should start with PAR1 magic bytes");
792 assert_eq!(
793 &bytes[bytes.len() - 4..],
794 b"PAR1",
795 "Should end with PAR1 magic bytes"
796 );
797 }
798}