sheeter-engine 1.5.0

Convert, inspect, and transform data files across eleven formats with streaming where it matters and auto-detection where it can.
Documentation
use assert_cmd::Command;
use sheeter_engine::{detect_content, detect_extension, CanonicalValue, Format};
use std::fs;
use std::path::Path;
use tempfile::tempdir;

fn hub(json: &str) -> CanonicalValue {
    CanonicalValue::from_json(json).unwrap()
}

#[test]
fn roundtrip_csv_parquet_csv() {
    let dir = tempdir().unwrap();
    let inp = dir.path().join("in.csv");
    let mid = dir.path().join("mid.parquet");
    let out = dir.path().join("out.csv");
    fs::write(&inp, "name,age\nAlice,30\nBob,25\n").unwrap();

    Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "csv",
            "--to",
            "parquet",
            inp.to_str().unwrap(),
            mid.to_str().unwrap(),
        ])
        .assert()
        .success();
    assert!(mid.exists());

    Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "parquet",
            "--to",
            "csv",
            mid.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .assert()
        .success();

    let csv = fs::read_to_string(out).unwrap();
    assert_eq!(csv, "name,age\nAlice,30\nBob,25\n");
}

#[test]
fn roundtrip_json_parquet_json_types() {
    let json = r#"[{"i":1,"f":2.5,"b":true,"s":"x"},{"i":2,"f":3.5,"b":false,"s":"y"}]"#;
    let bytes = sheeter_engine::to_parquet(&hub(json)).unwrap();
    let back = sheeter_engine::from_parquet(&bytes).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&back.to_json().unwrap()).unwrap();
    let arr = parsed.as_array().unwrap();
    assert_eq!(arr[0]["i"], 1);
    assert_eq!(arr[0]["f"], 2.5);
    assert_eq!(arr[0]["b"], true);
    assert_eq!(arr[0]["s"], "x");
    assert_eq!(arr[1]["i"], 2);
    assert_eq!(arr[1]["f"], 3.5);
    assert_eq!(arr[1]["b"], false);
    assert_eq!(arr[1]["s"], "y");
}

#[test]
fn null_values_preserved() {
    let json = r#"[{"a":1,"b":null},{"a":2,"b":"x"}]"#;
    let bytes = sheeter_engine::to_parquet(&hub(json)).unwrap();
    let back = sheeter_engine::from_parquet(&bytes).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&back.to_json().unwrap()).unwrap();
    let arr = parsed.as_array().unwrap();
    assert_eq!(arr[0]["b"], serde_json::Value::Null);
    assert_eq!(arr[1]["b"], "x");
}

#[test]
fn u64_overflow_falls_back_to_utf8() {
    let big = u64::MAX;
    let json = format!(r#"[{{"n":{}}}]"#, big);
    let bytes = sheeter_engine::to_parquet(&hub(&json)).unwrap();
    let back = sheeter_engine::from_parquet(&bytes).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&back.to_json().unwrap()).unwrap();
    let arr = parsed.as_array().unwrap();
    assert_eq!(arr[0]["n"], serde_json::Value::String(big.to_string()));
}

#[test]
fn empty_array_to_parquet_errors() {
    let empty = CanonicalValue::Array(vec![]);
    assert!(sheeter_engine::to_parquet(&empty).is_err());
}

#[test]
fn parquet_magic_detected() {
    let bytes = sheeter_engine::to_parquet(&hub(r#"[{"a":1}]"#)).unwrap();
    assert_eq!(detect_content(&bytes).unwrap(), Format::Parquet);
}

#[test]
fn parquet_extension_detected() {
    assert_eq!(
        detect_extension(Path::new("x.parquet")),
        Some(Format::Parquet)
    );
    assert_eq!(detect_extension(Path::new("x.pq")), Some(Format::Parquet));
}

#[test]
fn inspect_parquet() {
    let dir = tempdir().unwrap();
    let p = dir.path().join("x.parquet");
    fs::write(
        &p,
        sheeter_engine::to_parquet(&hub(r#"[{"a":1,"b":"x"},{"a":2,"b":"y"}]"#)).unwrap(),
    )
    .unwrap();
    let out = Command::cargo_bin("sheeter")
        .unwrap()
        .args(["inspect", p.to_str().unwrap()])
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "inspect falhou: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(stdout.contains("parquet"), "format: {stdout}");
    assert!(stdout.contains("rows: 2"), "contagem: {stdout}");
    assert!(stdout.contains("integer"), "tipo int: {stdout}");
    assert!(stdout.contains("string"), "tipo str: {stdout}");
}

#[test]
fn query_reads_parquet() {
    let dir = tempdir().unwrap();
    let p = dir.path().join("data.parquet");
    fs::write(
        &p,
        sheeter_engine::to_parquet(&hub(r#"[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]"#))
            .unwrap(),
    )
    .unwrap();
    let out = Command::cargo_bin("sheeter")
        .unwrap()
        .current_dir(dir.path())
        .args(["query", "SELECT name FROM data.parquet WHERE id = 2"])
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "query falhou: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert_eq!(stdout.trim(), r#"[{"name":"Bob"}]"#);
}

#[test]
fn view_reads_parquet() {
    let dir = tempdir().unwrap();
    let p = dir.path().join("x.parquet");
    fs::write(
        &p,
        sheeter_engine::to_parquet(&hub(r#"[{"a":"hello"}]"#)).unwrap(),
    )
    .unwrap();
    let out = Command::cargo_bin("sheeter")
        .unwrap()
        .args(["view", p.to_str().unwrap()])
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "view falhou: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(stdout.contains("hello"), "conteudo: {stdout}");
    assert!(stdout.contains("Output table No. 1"), "cabecalho: {stdout}");
}

#[test]
fn xlsx_concat_accepts_parquet() {
    let dir = tempdir().unwrap();
    let p = dir.path().join("a.parquet");
    fs::write(
        &p,
        sheeter_engine::to_parquet(&hub(r#"[{"v":1},{"v":2}]"#)).unwrap(),
    )
    .unwrap();
    let out = dir.path().join("out.xlsx");
    let status = std::process::Command::new(env!("CARGO_BIN_EXE_sheeter"))
        .current_dir(dir.path())
        .args(["xlsx:concat", "a.parquet", "-o", out.to_str().unwrap()])
        .status()
        .unwrap();
    assert!(status.success(), "xlsx:concat falhou");
    assert!(out.exists());
    let sheets = CanonicalValue::from_xlsx_workbook(&fs::read(&out).unwrap()).unwrap();
    assert_eq!(sheets.len(), 1);
    assert_eq!(sheets[0].0, "data");
}