sheeter-engine 1.2.0

Convert, inspect, and transform data files across ten formats with streaming where it matters and auto-detection where it can.
Documentation
use assert_cmd::Command;
use std::fs;
use tempfile::tempdir;

fn run(args: &[&str], input: &str, in_ext: &str, out_ext: &str) -> Result<String, String> {
    let dir = tempdir().unwrap();
    let inp = dir.path().join(format!("in.{in_ext}"));
    let out = dir.path().join(format!("out.{out_ext}"));
    fs::write(&inp, input).unwrap();
    let mut cmd = Command::cargo_bin("sheeter").unwrap();
    cmd.args(args);
    cmd.arg(inp.to_str().unwrap());
    cmd.arg(out.to_str().unwrap());
    let result = cmd.output().unwrap();
    if !result.status.success() {
        return Err(String::from_utf8_lossy(&result.stderr).to_string());
    }
    Ok(fs::read_to_string(out).unwrap())
}

#[test]
fn sql_insert_to_csv() {
    let sql = "CREATE TABLE pokemon (id INTEGER, name TEXT);\nINSERT INTO pokemon (id, name) VALUES (1, 'Bulbasaur'), (2, 'Ivysaur');";
    let csv = run(
        &["convert", "--from", "sql", "--to", "csv"],
        sql,
        "sql",
        "csv",
    )
    .unwrap();
    assert_eq!(csv, "id,name\n1,Bulbasaur\n2,Ivysaur\n");
}

#[test]
fn sql_without_create_table_uses_column_names() {
    let sql = "INSERT INTO t (a, b) VALUES (1, 'x');";
    let json = run(
        &["convert", "--from", "sql", "--to", "json"],
        sql,
        "sql",
        "json",
    )
    .unwrap();
    assert!(json.contains(r#""a":1"#), "column names: {json}");
    assert!(json.contains(r#""b":"x""#), "column names: {json}");
}

#[test]
fn sql_null_and_boolean() {
    let sql = "INSERT INTO t (a, b, c) VALUES (NULL, TRUE, 1.5);";
    let json = run(
        &["convert", "--from", "sql", "--to", "json"],
        sql,
        "sql",
        "json",
    )
    .unwrap();
    assert!(json.contains("null"), "null: {json}");
    assert!(json.contains("true"), "bool: {json}");
    assert!(json.contains("1.5"), "float: {json}");
}

#[test]
fn csv_to_sql_derives_table_from_filename() {
    let dir = tempdir().unwrap();
    let inp = dir.path().join("in.csv");
    let out = dir.path().join("users.sql");
    fs::write(&inp, "id,name\n1,Alice\n2,Bob\n").unwrap();
    Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "csv",
            "--to",
            "sql",
            inp.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .assert()
        .success();
    let sql = fs::read_to_string(out).unwrap();
    assert!(
        sql.contains(r#"INSERT INTO "users""#),
        "table from filename: {sql}"
    );
    assert!(sql.contains("'Alice'"), "values: {sql}");
}

#[test]
fn sql_escapes_single_quotes() {
    let sql = "INSERT INTO t (name) VALUES ('O''Brien');";
    let json = run(
        &["convert", "--from", "sql", "--to", "json"],
        sql,
        "sql",
        "json",
    )
    .unwrap();
    assert!(json.contains("O'Brien"), "escaped quote: {json}");
}

#[test]
fn sql_multi_table_to_csv_n_outputs() {
    let dir = tempdir().unwrap();
    let inp = dir.path().join("in.sql");
    let out = dir.path().join("out.csv");
    fs::write(
        &inp,
        "INSERT INTO users (id) VALUES (1);\nINSERT INTO orders (id) VALUES (2);\n",
    )
    .unwrap();
    Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "sql",
            "--to",
            "csv",
            inp.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .assert()
        .success();
    assert!(dir.path().join("out.users.csv").exists());
    assert!(dir.path().join("out.orders.csv").exists());
}

#[test]
fn dialect_flag_accepted() {
    let sql = "INSERT INTO t (a) VALUES (1);";
    let json = run(
        &[
            "convert",
            "--from",
            "sql",
            "--to",
            "json",
            "--dialect",
            "postgresql",
        ],
        sql,
        "sql",
        "json",
    )
    .unwrap();
    assert!(json.contains("\"a\":1"));
}

#[test]
fn select_without_insert_errors() {
    let sql = "SELECT 1;";
    let err = run(
        &["convert", "--from", "sql", "--to", "json"],
        sql,
        "sql",
        "json",
    )
    .unwrap_err();
    assert!(
        err.contains("INSERT") || err.contains("no INSERT"),
        "expected no-data error: {err}"
    );
}