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()
}