blob: a6d28bba8f3d19c36efbaf72cff3f65872cdc5bc [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 anyhow::{anyhow, Result};
use parquet::basic::Compression as ParquetCompression;
use std::path::PathBuf;
#[derive(Clone)]
pub struct ZoneDfArgs {
pub scale_factor: f64,
pub output_dir: PathBuf,
pub parts: Option<i32>,
pub part: Option<i32>,
pub output_file_size_mb: Option<f32>,
pub parquet_row_group_bytes: i64,
pub parquet_compression: ParquetCompression,
}
impl ZoneDfArgs {
pub fn new(
scale_factor: f64,
output_dir: PathBuf,
parts: Option<i32>,
part: Option<i32>,
output_file_size_mb: Option<f32>,
parquet_row_group_bytes: i64,
parquet_compression: ParquetCompression,
) -> Self {
Self {
scale_factor,
output_dir,
parts,
part,
output_file_size_mb,
parquet_row_group_bytes,
parquet_compression,
}
}
pub fn validate(&self) -> Result<()> {
if let (Some(part), Some(parts)) = (self.part, self.parts) {
if part < 1 || part > parts {
return Err(anyhow!("Invalid --part={} for --parts={}", part, parts));
}
}
if self.output_file_size_mb.is_some() && (self.parts.is_some() || self.part.is_some()) {
return Err(anyhow!(
"Cannot specify --parts/--part with --max-file-size-mb"
));
}
Ok(())
}
pub fn output_filename(&self) -> PathBuf {
if self.parts.unwrap_or(1) > 1 {
// Create zone subdirectory and write parts within it
self.output_dir
.join("zone")
.join(format!("zone.{}.parquet", self.part.unwrap_or(1)))
} else {
self.output_dir.join("zone.parquet")
}
}
/// Whether the output directory is an S3 URI (starts with `s3://`)
pub fn is_s3(&self) -> bool {
self.output_dir.to_string_lossy().starts_with("s3://")
}
/// Compute the S3 object key for this zone output.
///
/// Returns the full S3 URI (e.g. `s3://bucket/prefix/zone.parquet`).
pub fn output_s3_uri(&self) -> String {
let base = self.output_dir.to_string_lossy();
let base = base.trim_end_matches('/');
if self.parts.unwrap_or(1) > 1 {
format!("{}/zone/zone.{}.parquet", base, self.part.unwrap_or(1))
} else {
format!("{}/zone.parquet", base)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_args(output_dir: &str) -> ZoneDfArgs {
ZoneDfArgs::new(
1.0,
PathBuf::from(output_dir),
None,
None,
None,
128 * 1024 * 1024,
ParquetCompression::ZSTD(Default::default()),
)
}
#[test]
fn is_s3_with_s3_uri() {
let args = default_args("s3://my-bucket/output");
assert!(args.is_s3());
}
#[test]
fn is_s3_with_local_path() {
let args = default_args("/tmp/output");
assert!(!args.is_s3());
}
#[test]
fn is_s3_with_relative_path() {
let args = default_args("./output");
assert!(!args.is_s3());
}
#[test]
fn output_s3_uri_single_file() {
let args = default_args("s3://bucket/prefix");
assert_eq!(args.output_s3_uri(), "s3://bucket/prefix/zone.parquet");
}
#[test]
fn output_s3_uri_single_file_trailing_slash() {
let args = default_args("s3://bucket/prefix/");
assert_eq!(args.output_s3_uri(), "s3://bucket/prefix/zone.parquet");
}
#[test]
fn output_s3_uri_with_partitions() {
let mut args = default_args("s3://bucket/prefix");
args.parts = Some(10);
args.part = Some(3);
assert_eq!(
args.output_s3_uri(),
"s3://bucket/prefix/zone/zone.3.parquet"
);
}
#[test]
fn output_s3_uri_partition_defaults_to_part_1() {
let mut args = default_args("s3://bucket/prefix");
args.parts = Some(5);
// part not set — should default to 1
assert_eq!(
args.output_s3_uri(),
"s3://bucket/prefix/zone/zone.1.parquet"
);
}
}