use iron_shell::value::Value;
use std::path::{Path, PathBuf};
fn eval(src: &str) -> anyhow::Result<Value> {
let stmts = iron_shell::parser::parse_program(src).expect("parse");
iron_shell::eval::eval_program(&stmts, &mut iron_shell::env::Env::new())
}
fn s(v: &str) -> Value {
Value::Str(v.to_string())
}
fn arr(items: &[&str]) -> Value {
Value::Array(items.iter().map(|i| s(i)).collect())
}
fn code_of(e: &anyhow::Error) -> String {
let text = e.to_string();
match text.find("E_") {
Some(i) => text[i..]
.split(|c: char| !(c.is_ascii_uppercase() || c == '_'))
.next()
.unwrap_or("E_?")
.to_string(),
None => format!("NO_CODE: {text}"),
}
}
fn fresh_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("ae_round20_{tag}_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn fwd(p: &Path) -> String {
p.to_string_lossy().replace('\\', "/")
}
#[test]
fn to_csv_writes_the_columns_it_is_given_in_that_order() {
assert_eq!(
eval(r#"[{b: 2, a: 1}] | to_csv(["b", "a"])"#).unwrap(),
s("b,a\n2,1\n")
);
assert_eq!(
eval(r#"to_csv([{b: 2, a: 1}], ["b", "a"])"#).unwrap(),
s("b,a\n2,1\n")
);
}
#[test]
fn to_csv_without_columns_keeps_the_sorted_keys() {
assert_eq!(eval(r#"[{b: 2, a: 1}] | to_csv"#).unwrap(), s("a,b\n1,2\n"));
}
#[test]
fn a_key_a_record_lacks_writes_an_empty_field() {
assert_eq!(
eval(r#"[{id: 1}, {id: 2, customer: "Ada"}] | to_csv(["id", "customer"])"#).unwrap(),
s("id,customer\n1,\n2,Ada\n")
);
}
#[test]
fn to_csv_refuses_a_columns_argument_that_is_not_names() {
for src in [
r#"[{a: 1}] | to_csv([1])"#,
r#"[{a: 1}] | to_csv([])"#,
r#"[{a: 1}] | to_csv("a")"#,
] {
let e = eval(src).expect_err(src);
assert_eq!(code_of(&e), "E_BAD_ARG", "{src}: {e}");
}
}
#[test]
fn csv_columns_reads_the_header_of_csv_text_in_file_order() {
assert_eq!(
eval(r#"csv_columns("id,customer,amount\n1,Ada,3\n")"#).unwrap(),
arr(&["id", "customer", "amount"])
);
assert_eq!(
eval(r#""zeta,alpha\n1,2\n" | csv_columns"#).unwrap(),
arr(&["zeta", "alpha"])
);
}
#[test]
fn csv_columns_reads_a_file_header_and_drops_its_byte_order_mark() {
let dir = fresh_dir("cols");
let path = dir.join("rows.csv");
std::fs::write(&path, "\u{feff}customer,id,amount\nAda,1,3\n").unwrap();
let p = fwd(&path);
let got = eval(&format!(r#"csv_columns("{p}")"#));
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(got.unwrap(), arr(&["customer", "id", "amount"]));
}
#[test]
fn a_csv_file_round_trips_through_open_and_to_csv_in_its_own_column_order() {
let dir = fresh_dir("roundtrip");
let path = dir.join("rows.csv");
let text = "customer,id,amount\nAda,1,3\nGrace,2,\n";
std::fs::write(&path, text).unwrap();
let p = fwd(&path);
let got = eval(&format!(r#"open("{p}") | to_csv(csv_columns("{p}"))"#));
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(got.unwrap(), s(text));
}
#[test]
fn csv_columns_refuses_a_subject_that_is_not_text() {
let e = eval("csv_columns(5)").expect_err("an Int is not a path or text");
assert_eq!(code_of(&e), "E_BAD_ARG", "{e}");
}
#[test]
fn sql_binds_a_trailing_array_to_its_placeholders() {
assert_eq!(
eval(r#"[{n: 1}, {n: 2}, {n: 3}] | sql("select n from t where n > ?", [1]) | len"#)
.unwrap(),
Value::Int(2)
);
assert_eq!(
eval(r#"[{s: "open"}, {s: "closed"}] | sql_value("select count(*) from t where s = ?", ["open"])"#)
.unwrap(),
Value::Int(1)
);
}
#[test]
fn each_scalar_kind_binds_as_its_sql_value() {
let cases = [
(
r#"sql_value(":memory:", "select ? is null", [null])"#,
Value::Int(1),
),
(
r#"sql_value(":memory:", "select ? * 2", [1.5])"#,
Value::Float(3.0),
),
(
r#"sql_value(":memory:", "select ?", [true])"#,
Value::Int(1),
),
(
r#"sql_value(":memory:", "select ? || '!'", ["hi"])"#,
s("hi!"),
),
];
for (src, want) in cases {
assert_eq!(eval(src).unwrap(), want, "{src}");
}
}
#[test]
fn a_bound_value_is_data_not_query_text() {
assert_eq!(
eval(r#"[{s: "it's"}] | sql_value("select count(*) from t where s = ?", ["it's"])"#)
.unwrap(),
Value::Int(1)
);
}
#[test]
fn a_placeholder_count_mismatch_names_both_counts() {
let e = eval(r#"sql_value(":memory:", "select ?", [])"#).expect_err("no value for ?");
assert_eq!(code_of(&e), "E_BAD_ARG", "{e}");
let text = e.to_string();
assert!(
text.contains("1 placeholder") && text.contains("0 bind"),
"{text}"
);
let e = eval(r#"sql(":memory:", "select 1", [5])"#).expect_err("a value and no ?");
assert_eq!(code_of(&e), "E_BAD_ARG", "{e}");
}
#[test]
fn a_bind_value_that_is_not_a_scalar_is_refused() {
let e = eval(r#"sql_value(":memory:", "select ?", [[1]])"#).expect_err("an Array binds");
assert_eq!(code_of(&e), "E_BAD_ARG", "{e}");
}
#[test]
fn a_lone_array_is_still_refused_as_the_query() {
let e = eval("[{n: 1}] | sql([1])").expect_err("an Array is not a query");
assert_eq!(code_of(&e), "E_BAD_ARG", "{e}");
}
#[test]
fn a_file_query_takes_bind_parameters() {
let dir = fresh_dir("bind");
let path = dir.join("rows.csv");
std::fs::write(&path, "id,status\n1,open\n2,closed\n3,open\n").unwrap();
let p = fwd(&path);
let got = eval(&format!(
r#"sql("{p}", "select id from rows where status = ?", ["open"]) | len"#
));
let count = eval(&format!(
r#"sql_value("{p}", "select count(*) from rows where status = ?", ["open"])"#
));
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(got.unwrap(), Value::Int(2));
assert_eq!(count.unwrap(), Value::Int(2));
}
#[test]
fn a_blank_cell_in_a_numeric_column_is_null() {
assert_eq!(
eval(r#"from_csv("n,m\n7,\n8,2\n") | first | fn(r) => r.m"#).unwrap(),
Value::Null
);
}
#[test]
fn a_blank_cell_in_a_text_column_is_still_an_empty_string() {
assert_eq!(
eval(r#"from_csv("n,s\n7,\n8,x\n") | first | fn(r) => r.s"#).unwrap(),
s("")
);
assert_eq!(
eval(r#"from_csv("n,s\n7,\n8,\n") | first | fn(r) => r.s"#).unwrap(),
s("")
);
}
#[test]
fn a_numeric_column_with_a_blank_round_trips_through_to_csv() {
assert_eq!(
eval(r#"from_csv("n,m\n7,\n8,2\n") | to_csv(csv_columns("n,m\n7,\n8,2\n"))"#).unwrap(),
s("n,m\n7,\n8,2\n")
);
}
#[test]
fn sql_counts_skip_the_blank_in_a_numeric_column_and_not_in_a_text_one() {
let dir = fresh_dir("blanks");
let path = dir.join("rows.csv");
std::fs::write(
&path,
"id,amount,status,note,mixed\n1,3,open,,\n2,,,,abc\n3,4,,,\n",
)
.unwrap();
let p = fwd(&path);
let answers = [
("select count(amount) from rows", Value::Int(2)),
("select sum(amount) from rows", Value::Int(7)),
("select count(*) from rows where status = ''", Value::Int(2)),
("select count(*) from rows where note = ''", Value::Int(3)),
("select count(*) from rows where mixed = ''", Value::Int(2)),
];
let results: Vec<(String, Value)> = answers
.iter()
.map(|(q, _)| {
let got = eval(&format!(r#"sql_value("{p}", "{q}")"#)).unwrap();
(q.to_string(), got)
})
.collect();
let _ = std::fs::remove_dir_all(&dir);
for ((q, want), (_, got)) in answers.iter().zip(&results) {
assert_eq!(got, want, "{q}");
}
}