parx-cli 0.1.0

CLI tool for building and inspecting PARX sidecar files
/*
 * Copyright 2026 PARX Authors
 *
 * Licensed 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 assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use std::path::Path;
use tempfile::TempDir;

use parquet::data_type::Int32Type;
use parquet::file::properties::WriterProperties;
use parquet::file::writer::SerializedFileWriter;
use parquet::schema::parser::parse_message_type;

fn parxcli_cmd() -> Command {
    Command::new(assert_cmd::cargo::cargo_bin!("parx"))
}

fn write_simple_parquet(path: &Path) -> parquet::errors::Result<()> {
    let message_type = "message schema { REQUIRED INT32 id; }";
    let schema = std::sync::Arc::new(parse_message_type(message_type)?);
    let file = fs::File::create(path)?;
    let props = std::sync::Arc::new(WriterProperties::builder().build());

    let mut writer = SerializedFileWriter::new(file, schema, props)?;
    let mut row_group_writer = writer.next_row_group()?;
    if let Some(mut col_writer) = row_group_writer.next_column()? {
        let values = [1i32, 2, 3];
        col_writer
            .typed::<Int32Type>()
            .write_batch(&values, None, None)?;
        col_writer.close()?;
    }
    row_group_writer.close()?;
    writer.close()?;
    Ok(())
}

#[test]
fn cli_build_inspect_verify_roundtrip() {
    let temp_dir = TempDir::new().expect("temp dir");
    let parquet_path = temp_dir.path().join("data.parquet");
    write_simple_parquet(&parquet_path).expect("write parquet");
    let parquet_str = parquet_path.to_str().unwrap();

    parxcli_cmd()
        .args(["build", parquet_str])
        .assert()
        .success();

    let parx_path = format!("{}.parx", parquet_str);
    assert!(Path::new(&parx_path).exists());

    parxcli_cmd()
        .args(["inspect", &parx_path])
        .assert()
        .success()
        .stdout(predicate::str::contains("PARX File"));

    parxcli_cmd()
        .args(["verify", &parx_path, "--with-source"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Verification passed"));
}

#[test]
fn cli_build_with_compression() {
    let temp_dir = TempDir::new().expect("temp dir");
    let parquet_path = temp_dir.path().join("data.parquet");
    write_simple_parquet(&parquet_path).expect("write parquet");
    let parquet_str = parquet_path.to_str().unwrap();
    let output_path = temp_dir.path().join("data.parquet.zstd.parx");
    let output_str = output_path.to_str().unwrap();

    parxcli_cmd()
        .args(["build", parquet_str, "--compress", "zstd", "-o", output_str])
        .assert()
        .success();

    parxcli_cmd()
        .args(["inspect", output_str])
        .assert()
        .success()
        .stdout(predicate::str::contains("Footer compressed"));
}

#[test]
fn cli_bundle_flow() {
    let temp_dir = TempDir::new().expect("temp dir");
    let parquet_path = temp_dir.path().join("data.parquet");
    write_simple_parquet(&parquet_path).expect("write parquet");
    let dir_str = temp_dir.path().to_str().unwrap();
    parxcli_cmd()
        .args(["bundle", "build", dir_str])
        .assert()
        .success();

    let bundle_path = temp_dir.path().join("_parx_bundle.parx");
    assert!(bundle_path.exists());

    parxcli_cmd()
        .args(["bundle", "inspect", bundle_path.to_str().unwrap()])
        .assert()
        .success();

    parxcli_cmd()
        .args([
            "bundle",
            "verify",
            bundle_path.to_str().unwrap(),
            "--with-sources",
        ])
        .assert()
        .success();

    let extract_dir = temp_dir.path().join("extracted");
    fs::create_dir_all(&extract_dir).unwrap();
    parxcli_cmd()
        .args([
            "bundle",
            "extract",
            bundle_path.to_str().unwrap(),
            "-o",
            extract_dir.to_str().unwrap(),
        ])
        .assert()
        .success();

    let extracted_parx = extract_dir.join("data.parquet.parx");
    assert!(extracted_parx.exists());
}