blob: d721eecc46b1557f26ee9b1706c735f82ea59054 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Parquet output format
use crate::statistics::WriteStatistics;
use arrow::datatypes::SchemaRef;
use futures::StreamExt;
use log::debug;
use parquet::arrow::arrow_writer::{compute_leaves, get_column_writers, ArrowColumnChunk};
use parquet::arrow::ArrowSchemaConverter;
use parquet::basic::Compression;
use parquet::file::properties::WriterProperties;
use parquet::file::writer::SerializedFileWriter;
use parquet::schema::types::SchemaDescPtr;
use spatialbench_arrow::RecordBatchIterator;
use std::io;
use std::io::Write;
use std::sync::Arc;
use tokio::sync::mpsc::{Receiver, Sender};
/// Finalize a writer after all Parquet data has been written.
///
/// This is called from the async context (outside `spawn_blocking`) so
/// that implementations like [`S3Writer`](crate::s3_writer::S3Writer) can
/// `.await` their upload without competing with the tokio runtime for
/// threads — avoiding deadlocks under concurrent plans.
///
/// For local files and stdout the implementation is trivially synchronous.
pub trait AsyncFinalize: Write + Send + 'static {
/// Finalize the writer and return the total bytes written.
fn finalize(self) -> impl std::future::Future<Output = Result<usize, io::Error>> + Send;
}
/// Converts a set of RecordBatchIterators into a Parquet file
///
/// Uses num_threads to generate the data in parallel
///
/// Note the input is an iterator of [`RecordBatchIterator`]; The batches
/// produced by each iterator is encoded as its own row group.
pub async fn generate_parquet<W: AsyncFinalize, I>(
writer: W,
iter_iter: I,
num_threads: usize,
parquet_compression: Compression,
) -> Result<(), io::Error>
where
I: Iterator<Item: RecordBatchIterator> + 'static,
{
debug!(
"Generating Parquet with {num_threads} threads, using {parquet_compression} compression"
);
// Based on example in https://docs.rs/parquet/latest/parquet/arrow/arrow_writer/struct.ArrowColumnWriter.html
let mut iter_iter = iter_iter.peekable();
// get schema from the first iterator
let Some(first_iter) = iter_iter.peek() else {
return Ok(()); // no data shrug
};
let schema = Arc::clone(first_iter.schema());
// Compute the parquet schema
let writer_properties = WriterProperties::builder()
.set_compression(parquet_compression)
.build();
let writer_properties = Arc::new(writer_properties);
let parquet_schema = Arc::new(
ArrowSchemaConverter::new()
.with_coerce_types(writer_properties.coerce_types())
.convert(&schema)
.unwrap(),
);
// create a stream that computes the data for each row group
let mut row_group_stream = futures::stream::iter(iter_iter)
.map(async |iter| {
let parquet_schema = Arc::clone(&parquet_schema);
let writer_properties = Arc::clone(&writer_properties);
let schema = Arc::clone(&schema);
// run on a separate thread
tokio::task::spawn(async move {
encode_row_group(parquet_schema, writer_properties, schema, iter)
})
.await
.expect("Inner task panicked")
})
.buffered(num_threads); // generate row groups in parallel
let mut statistics = WriteStatistics::new("row groups");
// A blocking task that writes the row groups to the file
// done in a blocking task to avoid having a thread waiting on IO
// Now, read each completed row group and write it to the file
let root_schema = parquet_schema.root_schema_ptr();
let writer_properties_captured = Arc::clone(&writer_properties);
let (tx, mut rx): (
Sender<Vec<ArrowColumnChunk>>,
Receiver<Vec<ArrowColumnChunk>>,
) = tokio::sync::mpsc::channel(num_threads);
let writer_task = tokio::task::spawn_blocking(move || {
// Create parquet writer
let mut writer = SerializedFileWriter::new(writer, root_schema, writer_properties_captured)
.map_err(io::Error::from)?;
while let Some(chunks) = rx.blocking_recv() {
// Start row group
let mut row_group_writer = writer.next_row_group().map_err(io::Error::from)?;
// Slap the chunks into the row group
for chunk in chunks {
chunk
.append_to_row_group(&mut row_group_writer)
.map_err(io::Error::from)?;
}
row_group_writer.close().map_err(io::Error::from)?;
statistics.increment_chunks(1);
}
let inner = writer.into_inner().map_err(io::Error::from)?;
Ok((inner, statistics)) as Result<(W, WriteStatistics), io::Error>
});
// now, drive the input stream and send results to the writer task
while let Some(chunks) = row_group_stream.next().await {
// send the chunks to the writer task
if let Err(e) = tx.send(chunks).await {
debug!("Error sending chunks to writer: {e}");
break; // stop early
}
}
// signal the writer task that we are done
drop(tx);
// Wait for the blocking writer task to return the underlying writer
let (inner, mut statistics) = writer_task.await??;
// Finalize in the async context so S3 uploads can .await without
// competing for tokio runtime threads (prevents deadlock under
// concurrent plans).
let size = inner.finalize().await?;
statistics.increment_bytes(size);
Ok(())
}
/// Creates the data for a particular row group
///
/// Note at the moment it does not use multiple tasks/threads but it could
/// potentially encode multiple columns with different threads .
///
/// Returns an array of [`ArrowColumnChunk`]
fn encode_row_group<I>(
parquet_schema: SchemaDescPtr,
writer_properties: Arc<WriterProperties>,
schema: SchemaRef,
iter: I,
) -> Vec<ArrowColumnChunk>
where
I: RecordBatchIterator,
{
// Create writers for each of the leaf columns
let mut col_writers = get_column_writers(&parquet_schema, &writer_properties, &schema).unwrap();
// generate the data and send it to the tasks (via the sender channels)
for batch in iter {
let columns = batch.columns().iter();
let col_writers = col_writers.iter_mut();
let fields = schema.fields().iter();
for ((col_writer, field), arr) in col_writers.zip(fields).zip(columns) {
for leaves in compute_leaves(field.as_ref(), arr).unwrap() {
col_writer.write(&leaves).unwrap();
}
}
}
// finish the writers and create the column chunks
col_writers
.into_iter()
.map(|col_writer| col_writer.close().unwrap())
.collect()
}