datafusion-ducklake-cli 0.1.0

Interactive and scriptable SQL shell for datafusion-ducklake-provider.
Documentation
use std::io::Write;
use std::process::{Command, Stdio};

fn ducklake_cli() -> Command {
    Command::new(env!("CARGO_BIN_EXE_ducklake-cli"))
}

#[test]
fn execute_select_prints_table() {
    let output = ducklake_cli()
        .args(["--execute", "SELECT 1 AS one"])
        .output()
        .expect("ducklake-cli should run");

    assert!(output.status.success(), "stderr: {}", stderr(&output));
    let stdout = stdout(&output);
    assert!(stdout.contains("one"), "stdout: {stdout}");
    assert!(stdout.contains("1"), "stdout: {stdout}");
}

#[test]
fn file_executes_multiple_statements() {
    let temp = tempfile::tempdir().unwrap();
    let script = temp.path().join("script.sql");
    std::fs::write(&script, "SELECT 1 AS one; SELECT 'x' AS letter;").unwrap();

    let output = ducklake_cli()
        .arg("--file")
        .arg(&script)
        .output()
        .expect("ducklake-cli should run");

    assert!(output.status.success(), "stderr: {}", stderr(&output));
    let stdout = stdout(&output);
    assert!(stdout.contains("one"), "stdout: {stdout}");
    assert!(stdout.contains("letter"), "stdout: {stdout}");
    assert!(stdout.contains("x"), "stdout: {stdout}");
}

#[test]
fn stdin_executes_script() {
    let mut child = ducklake_cli()
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("ducklake-cli should start");
    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(b"SELECT 42 AS answer;")
        .unwrap();
    let output = child.wait_with_output().unwrap();

    assert!(output.status.success(), "stderr: {}", stderr(&output));
    let stdout = stdout(&output);
    assert!(stdout.contains("answer"), "stdout: {stdout}");
    assert!(stdout.contains("42"), "stdout: {stdout}");
}

#[test]
fn invalid_sql_exits_nonzero() {
    let output = ducklake_cli()
        .args(["--execute", "SELECT FROM"])
        .output()
        .expect("ducklake-cli should run");

    assert!(!output.status.success());
    assert!(stderr(&output).contains("command line"));
}

#[test]
fn continue_on_error_runs_later_statements() {
    let output = ducklake_cli()
        .args([
            "--continue-on-error",
            "--execute",
            "SELECT FROM; SELECT 7 AS seven;",
        ])
        .output()
        .expect("ducklake-cli should run");

    assert!(output.status.success(), "stderr: {}", stderr(&output));
    let stdout = stdout(&output);
    assert!(stdout.contains("seven"), "stdout: {stdout}");
    assert!(stdout.contains("7"), "stdout: {stdout}");
    assert!(stderr(&output).contains("command line"));
}

#[test]
fn init_runs_before_execute() {
    let temp = tempfile::tempdir().unwrap();
    let catalog = temp.path().join("metadata.sqlite");
    let data = temp.path().join("data");
    let init = temp.path().join("init.sql");
    std::fs::write(
        &init,
        format!(
            "ATTACH 'ducklake:sqlite:{}' AS lake (DATA_PATH '{}');\
             CREATE TABLE lake.main.numbers (id BIGINT, name VARCHAR);\
             INSERT INTO lake.main.numbers VALUES (1, 'one'), (2, 'two');",
            sql_path(&catalog),
            sql_path(&data)
        ),
    )
    .unwrap();

    let output = ducklake_cli()
        .arg("--init")
        .arg(&init)
        .args([
            "--execute",
            "SELECT id, name FROM lake.main.numbers ORDER BY id",
        ])
        .output()
        .expect("ducklake-cli should run");

    assert!(output.status.success(), "stderr: {}", stderr(&output));
    let stdout = stdout(&output);
    assert!(stdout.contains("one"), "stdout: {stdout}");
    assert!(stdout.contains("two"), "stdout: {stdout}");
}

#[test]
fn relative_sqlite_data_path_can_be_selected_after_insert() {
    let temp = tempfile::tempdir().unwrap();

    let output = ducklake_cli()
        .current_dir(temp.path())
        .args([
            "--execute",
            "ATTACH 'ducklake:sqlite:metadata.sqlite' AS lake (DATA_PATH 'data/');\
             CALL ducklake_set_option('lake', 'data_inlining_row_limit', 0);\
             CREATE TABLE lake.main.numbers (id BIGINT, name VARCHAR);\
             INSERT INTO lake.main.numbers VALUES (1, 'one'), (2, 'two');\
             SELECT * FROM lake.main.numbers ORDER BY id;",
        ])
        .output()
        .expect("ducklake-cli should run");

    assert!(output.status.success(), "stderr: {}", stderr(&output));
    let stdout = stdout(&output);
    assert!(stdout.contains("one"), "stdout: {stdout}");
    assert!(stdout.contains("two"), "stdout: {stdout}");
    assert!(temp.path().join("data/main/numbers").is_dir());
}

fn sql_path(path: &std::path::Path) -> String {
    path.display().to_string().replace('\'', "''")
}

fn stdout(output: &std::process::Output) -> String {
    String::from_utf8_lossy(&output.stdout).into_owned()
}

fn stderr(output: &std::process::Output) -> String {
    String::from_utf8_lossy(&output.stderr).into_owned()
}