blob: 5afa038401af59016899d8194da6cda02c3b5e4f [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.
use crate::s3_writer::S3Writer;
use anyhow::Result;
use arrow_array::RecordBatch;
use arrow_schema::SchemaRef;
use log::{debug, info};
use parquet::{arrow::ArrowWriter, file::properties::WriterProperties};
use std::{path::PathBuf, sync::Arc, time::Instant};
use super::config::ZoneDfArgs;
use super::stats::ZoneTableStats;
pub struct ParquetWriter {
output_path: PathBuf,
schema: SchemaRef,
props: WriterProperties,
args: ZoneDfArgs,
}
impl ParquetWriter {
pub fn new(args: &ZoneDfArgs, stats: &ZoneTableStats, schema: SchemaRef) -> Self {
let rows_per_group =
stats.compute_rows_per_group(args.parquet_row_group_bytes, 128 * 1024 * 1024);
let props = WriterProperties::builder()
.set_compression(args.parquet_compression)
.set_max_row_group_size(rows_per_group)
.build();
debug!("Using row group size: {} rows", rows_per_group);
Self {
output_path: args.output_filename(),
schema,
props,
args: args.clone(),
}
}
pub async fn write(&self, batches: &[RecordBatch]) -> Result<()> {
if self.args.is_s3() {
self.write_s3(batches).await
} else {
self.write_local(batches)
}
}
/// Write batches to a local file using a temp-file + rename pattern.
fn write_local(&self, batches: &[RecordBatch]) -> Result<()> {
// Create parent directory of output file (handles both zone/ subdirectory and base dir)
let parent_dir = self
.output_path
.parent()
.ok_or_else(|| anyhow::anyhow!("Invalid output path: {:?}", self.output_path))?;
std::fs::create_dir_all(parent_dir)?;
debug!("Created output directory: {:?}", parent_dir);
// Check if file already exists
if self.output_path.exists() {
info!(
"{} already exists, skipping generation",
self.output_path.display()
);
return Ok(());
}
// Write to temp file first
let temp_path = self.output_path.with_extension("inprogress");
let t0 = Instant::now();
let file = std::fs::File::create(&temp_path)?;
let mut writer =
ArrowWriter::try_new(file, Arc::clone(&self.schema), Some(self.props.clone()))?;
for batch in batches {
writer.write(batch)?;
}
writer.close()?;
// Rename temp file to final output
std::fs::rename(&temp_path, &self.output_path).map_err(|e| {
anyhow::anyhow!(
"Failed to rename {:?} to {:?}: {}",
temp_path,
self.output_path,
e
)
})?;
let duration = t0.elapsed();
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
info!(
"Zone -> {} (part {:?}/{:?}). write={:?}, total_rows={}",
self.output_path.display(),
self.args.part,
self.args.parts,
duration,
total_rows
);
Ok(())
}
/// Write batches to S3 using [`S3Writer`].
///
/// S3 writes are atomic (via multipart upload `complete()`), so no
/// temp-file or rename is needed.
async fn write_s3(&self, batches: &[RecordBatch]) -> Result<()> {
let uri = self.args.output_s3_uri();
info!("Writing zone parquet to S3: {}", uri);
let t0 = Instant::now();
let s3_writer = S3Writer::new(&uri)?;
let mut writer = ArrowWriter::try_new(
s3_writer,
Arc::clone(&self.schema),
Some(self.props.clone()),
)?;
for batch in batches {
writer.write(batch)?;
}
let s3_writer = writer.into_inner()?;
let size = s3_writer.finish().await?;
let duration = t0.elapsed();
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
info!(
"Zone -> {} (part {:?}/{:?}). write={:?}, total_rows={}, bytes={}",
uri, self.args.part, self.args.parts, duration, total_rows, size
);
Ok(())
}
}