use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum OnEmpty {
#[default]
Continue,
Skip,
Fail,
}
impl OnEmpty {
pub const NAMES: &[&str] = &["continue", "skip", "fail"];
pub fn parse(s: &str) -> Option<OnEmpty> {
match s {
"continue" => Some(OnEmpty::Continue),
"skip" => Some(OnEmpty::Skip),
"fail" => Some(OnEmpty::Fail),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
OnEmpty::Continue => "continue",
OnEmpty::Skip => "skip",
OnEmpty::Fail => "fail",
}
}
}
pub struct Query<'a> {
pub name: &'a str,
pub rows: u64,
pub on_empty: Option<OnEmpty>,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Decision {
pub empty: Vec<String>,
pub drop: Vec<String>,
pub fail: Option<String>,
pub skip: Option<String>,
pub warnings: Vec<String>,
pub info: Option<String>,
}
pub fn decide(output: Option<OnEmpty>, queries: &[Query<'_>], droppable: bool) -> Decision {
let mut d = Decision {
empty: queries
.iter()
.filter(|q| q.rows == 0)
.map(|q| q.name.to_string())
.collect(),
..Decision::default()
};
if d.empty.is_empty() {
return d;
}
let list = |names: &[String]| {
names
.iter()
.map(|n| format!("`{n}`"))
.collect::<Vec<_>>()
.join(", ")
};
let mut kept_explicitly = Vec::new();
for q in queries.iter().filter(|q| q.rows == 0) {
match q.on_empty {
Some(OnEmpty::Fail) => {
d.fail = Some(format!("query `{}` returned no rows (`on_empty: fail`)", q.name));
return d;
}
Some(OnEmpty::Skip) if droppable => d.drop.push(q.name.to_string()),
Some(OnEmpty::Continue) => kept_explicitly.push(q.name.to_string()),
_ => {}
}
}
let all_empty = queries.iter().all(|q| q.rows == 0);
if !all_empty {
let kept: Vec<String> = d.empty.iter().filter(|q| !d.drop.contains(q)).cloned().collect();
d.info = Some(match (kept.is_empty(), d.drop.is_empty()) {
(false, true) => format!("{} returned no rows; written empty", list(&kept)),
(true, false) => format!("{} returned no rows; left out (`on_empty: skip`)", list(&d.drop)),
_ => format!(
"{} returned no rows; {} written empty, {} left out (`on_empty: skip`)",
list(&d.empty),
list(&kept),
list(&d.drop)
),
});
return d;
}
let empty = list(&d.empty);
if d.drop.len() == queries.len() {
if output.is_some_and(|o| o != OnEmpty::Skip) {
d.warnings.push(format!(
"every query has `on_empty: skip`, so the output is skipped although it says `on_empty: {}`",
output.unwrap_or_default().as_str()
));
}
d.skip = Some(format!("no data: {empty} returned no rows"));
return d;
}
match output.unwrap_or_default() {
OnEmpty::Continue => d
.warnings
.push(format!("no data: {empty} returned no rows; the output is written empty")),
o if !kept_explicitly.is_empty() => d.warnings.push(format!(
"no data: {empty} returned no rows; {} {} `on_empty: continue`, which wins over the output's `on_empty: {}`, so the output is written empty",
list(&kept_explicitly),
if kept_explicitly.len() == 1 { "has" } else { "have" },
o.as_str()
)),
OnEmpty::Skip => d.skip = Some(format!("no data: {empty} returned no rows")),
OnEmpty::Fail => d.fail = Some(format!("no data: {empty} returned no rows (`on_empty: fail`)")),
}
d
}
#[cfg(test)]
mod tests {
use super::*;
use OnEmpty::*;
fn q(name: &str, rows: u64, on_empty: Option<OnEmpty>) -> Query<'_> {
Query { name, rows, on_empty }
}
#[test]
fn decisions() {
let d = decide(Some(Fail), &[q("a", 1, None)], true);
assert_eq!(d, Decision::default());
let d = decide(None, &[q("a", 0, None)], true);
assert!(d.skip.is_none() && d.fail.is_none());
assert!(d.warnings[0].contains("`a` returned no rows; the output is written empty"));
assert!(
decide(Some(Skip), &[q("a", 0, None), q("b", 0, None)], true)
.skip
.is_some()
);
assert!(decide(Some(Fail), &[q("a", 0, None)], true).fail.is_some());
let d = decide(Some(Skip), &[q("a", 0, None), q("b", 3, None)], true);
assert!(d.skip.is_none() && d.warnings.is_empty());
assert!(d.info.unwrap().contains("`a` returned no rows; written empty"));
let d = decide(None, &[q("a", 0, Some(Skip)), q("b", 3, None)], true);
assert_eq!(d.drop, vec!["a"]);
assert!(d.skip.is_none());
let d = decide(
Some(Continue),
&[q("a", 0, Some(Skip)), q("b", 0, Some(Skip))],
true,
);
assert!(d.skip.is_some());
assert!(d.warnings[0].contains("although it says `on_empty: continue`"));
let d = decide(None, &[q("a", 0, Some(Fail)), q("b", 3, None)], true);
assert!(d.fail.unwrap().contains("query `a` returned no rows"));
for o in [Skip, Fail] {
let d = decide(Some(o), &[q("a", 0, Some(Continue))], true);
assert!(d.skip.is_none() && d.fail.is_none(), "{o:?}");
assert!(d.warnings[0].contains("which wins over the output's"), "{o:?}");
}
let d = decide(Some(Skip), &[q("a", 0, Some(Skip)), q("b", 3, None)], false);
assert!(d.drop.is_empty() && d.skip.is_none());
let d = decide(Some(Skip), &[q("a", 0, Some(Skip))], false);
assert!(d.skip.is_some());
}
}