use std::process::ExitCode;
const SUBSET: &[&str] = &[
"06", "07", "08", "09", "10", "11", "12", "13", "32", "33", "36", "37", "38", "39", "40",
"41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55",
"56", "57", "58", "62", "65", "66", "67",
];
const CORPUS_NOW: i64 = 1_749_988_800 * 1_000_000;
struct Tally {
pass: u32,
refused: u32,
wrong: u32,
refused_select: u32,
}
enum Expect {
Rows(Vec<String>),
StatementOk,
StatementError,
}
pub(crate) fn run_probe(dir: &str) -> ExitCode {
let mut names: Vec<String> = match std::fs::read_dir(dir) {
Ok(rd) => rd
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.ends_with(".test"))
.collect(),
Err(e) => {
eprintln!("kevy-cli sql probe: {dir}: {e}");
return ExitCode::FAILURE;
}
};
names.sort();
let (mut sub_pass, mut sub_total, mut wrong_total) = (0u32, 0u32, 0u32);
let mut sub_foldable = 0u32;
for name in &names {
let src = match std::fs::read_to_string(format!("{dir}/{name}")) {
Ok(s) => s,
Err(e) => {
eprintln!("kevy-cli sql probe: {name}: {e}");
return ExitCode::FAILURE;
}
};
let t = run_file(&src);
let in_subset = SUBSET.iter().any(|p| name.starts_with(p));
if in_subset {
sub_pass += t.pass;
sub_total += t.pass + t.refused + t.wrong;
sub_foldable += t.pass + t.refused_select + t.wrong;
}
wrong_total += t.wrong;
print_file_row(name, &t, in_subset);
}
let pct = |num: u32, den: u32| {
if den == 0 { 0.0 } else { f64::from(num) / f64::from(den) * 100.0 }
};
println!(
"probe-summary: files={} subset-served={sub_pass}/{sub_total} ({:.1}%) \
subset-foldable={sub_pass}/{sub_foldable} ({:.1}%) wrong={wrong_total}",
names.len(),
pct(sub_pass, sub_total),
pct(sub_pass, sub_foldable),
);
ExitCode::SUCCESS
}
fn print_file_row(name: &str, t: &Tally, in_subset: bool) {
println!(
"{name:<44} pass={:<3} refused={:<3} wrong={}{}",
t.pass,
t.refused,
t.wrong,
if in_subset { " [subset]" } else { "" }
);
}
fn run_file(src: &str) -> Tally {
let mut t = Tally { pass: 0, refused: 0, wrong: 0, refused_select: 0 };
for (sql, expect) in parse_records(src) {
classify(&sql, &expect, &mut t);
}
t
}
fn classify(sql: &str, expect: &Expect, t: &mut Tally) {
let foldable = sql.trim_start().to_ascii_lowercase().starts_with("select");
if !foldable {
t.refused += 1;
return;
}
match (kevy_sql::fold_select(sql, CORPUS_NOW), expect) {
(Ok(f), Expect::Rows(want)) => {
let got: Vec<String> = f.columns.iter().map(slt_render).collect();
if &got == want {
t.pass += 1;
} else {
t.wrong += 1;
}
}
(Ok(_), Expect::StatementError) => t.wrong += 1,
(Ok(_), Expect::StatementOk) => t.pass += 1,
(Err(_), Expect::StatementError) => t.pass += 1,
(Err(_), _) => {
t.refused += 1;
t.refused_select += 1;
}
}
}
fn parse_records(src: &str) -> Vec<(String, Expect)> {
let mut out = Vec::new();
let mut lines = src.lines().peekable();
while let Some(line) = lines.next() {
let head = line.trim();
if head.starts_with("query") {
let mut sql = String::new();
for l in lines.by_ref() {
if l.trim() == "----" {
break;
}
push_line(&mut sql, l);
}
let mut rows = Vec::new();
while let Some(l) = lines.peek() {
if l.trim().is_empty() {
break;
}
rows.push(lines.next().expect("peeked").trim_end().to_string());
}
out.push((sql, Expect::Rows(rows)));
} else if head.starts_with("statement") {
let expect =
if head.contains("error") { Expect::StatementError } else { Expect::StatementOk };
let mut sql = String::new();
while let Some(l) = lines.peek() {
if l.trim().is_empty() {
break;
}
push_line(&mut sql, lines.next().expect("peeked"));
}
out.push((sql, expect));
}
}
out
}
fn slt_render(v: &kevy_sql::Scalar) -> String {
match v {
kevy_sql::Scalar::Null => "NULL".to_string(),
kevy_sql::Scalar::Bool(b) => (if *b { "1" } else { "0" }).to_string(),
other => {
let s = other.render();
if s.is_empty() { "(empty)".to_string() } else { s }
}
}
}
fn push_line(sql: &mut String, l: &str) {
if !sql.is_empty() {
sql.push('\n');
}
sql.push_str(l);
}