1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
13#[serde(rename_all = "lowercase")]
14pub enum OnEmpty {
15 #[default]
17 Continue,
18 Skip,
20 Fail,
22}
23
24impl OnEmpty {
25 pub const NAMES: &[&str] = &["continue", "skip", "fail"];
26
27 pub fn parse(s: &str) -> Option<OnEmpty> {
28 match s {
29 "continue" => Some(OnEmpty::Continue),
30 "skip" => Some(OnEmpty::Skip),
31 "fail" => Some(OnEmpty::Fail),
32 _ => None,
33 }
34 }
35
36 pub fn as_str(self) -> &'static str {
37 match self {
38 OnEmpty::Continue => "continue",
39 OnEmpty::Skip => "skip",
40 OnEmpty::Fail => "fail",
41 }
42 }
43}
44
45pub struct Query<'a> {
47 pub name: &'a str,
48 pub rows: u64,
49 pub on_empty: Option<OnEmpty>,
50}
51
52#[derive(Debug, Default, PartialEq, Eq)]
54pub struct Decision {
55 pub empty: Vec<String>,
57 pub drop: Vec<String>,
59 pub fail: Option<String>,
61 pub skip: Option<String>,
63 pub warnings: Vec<String>,
65 pub info: Option<String>,
67}
68
69pub fn decide(output: Option<OnEmpty>, queries: &[Query<'_>], droppable: bool) -> Decision {
73 let mut d = Decision {
74 empty: queries
75 .iter()
76 .filter(|q| q.rows == 0)
77 .map(|q| q.name.to_string())
78 .collect(),
79 ..Decision::default()
80 };
81 if d.empty.is_empty() {
82 return d;
83 }
84 let list = |names: &[String]| {
85 names
86 .iter()
87 .map(|n| format!("`{n}`"))
88 .collect::<Vec<_>>()
89 .join(", ")
90 };
91 let mut kept_explicitly = Vec::new();
93 for q in queries.iter().filter(|q| q.rows == 0) {
94 match q.on_empty {
95 Some(OnEmpty::Fail) => {
96 d.fail = Some(format!("query `{}` returned no rows (`on_empty: fail`)", q.name));
97 return d;
98 }
99 Some(OnEmpty::Skip) if droppable => d.drop.push(q.name.to_string()),
100 Some(OnEmpty::Continue) => kept_explicitly.push(q.name.to_string()),
101 _ => {}
102 }
103 }
104 let all_empty = queries.iter().all(|q| q.rows == 0);
105 if !all_empty {
106 let kept: Vec<String> = d.empty.iter().filter(|q| !d.drop.contains(q)).cloned().collect();
107 d.info = Some(match (kept.is_empty(), d.drop.is_empty()) {
108 (false, true) => format!("{} returned no rows; written empty", list(&kept)),
109 (true, false) => format!("{} returned no rows; left out (`on_empty: skip`)", list(&d.drop)),
110 _ => format!(
111 "{} returned no rows; {} written empty, {} left out (`on_empty: skip`)",
112 list(&d.empty),
113 list(&kept),
114 list(&d.drop)
115 ),
116 });
117 return d;
118 }
119 let empty = list(&d.empty);
121 if d.drop.len() == queries.len() {
122 if output.is_some_and(|o| o != OnEmpty::Skip) {
123 d.warnings.push(format!(
124 "every query has `on_empty: skip`, so the output is skipped although it says `on_empty: {}`",
125 output.unwrap_or_default().as_str()
126 ));
127 }
128 d.skip = Some(format!("no data: {empty} returned no rows"));
129 return d;
130 }
131 match output.unwrap_or_default() {
132 OnEmpty::Continue => d
133 .warnings
134 .push(format!("no data: {empty} returned no rows; the output is written empty")),
135 o if !kept_explicitly.is_empty() => d.warnings.push(format!(
136 "no data: {empty} returned no rows; {} {} `on_empty: continue`, which wins over the output's `on_empty: {}`, so the output is written empty",
137 list(&kept_explicitly),
138 if kept_explicitly.len() == 1 { "has" } else { "have" },
139 o.as_str()
140 )),
141 OnEmpty::Skip => d.skip = Some(format!("no data: {empty} returned no rows")),
142 OnEmpty::Fail => d.fail = Some(format!("no data: {empty} returned no rows (`on_empty: fail`)")),
143 }
144 d
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use OnEmpty::*;
151
152 fn q(name: &str, rows: u64, on_empty: Option<OnEmpty>) -> Query<'_> {
153 Query { name, rows, on_empty }
154 }
155
156 #[test]
158 fn decisions() {
159 let d = decide(Some(Fail), &[q("a", 1, None)], true);
161 assert_eq!(d, Decision::default());
162
163 let d = decide(None, &[q("a", 0, None)], true);
165 assert!(d.skip.is_none() && d.fail.is_none());
166 assert!(d.warnings[0].contains("`a` returned no rows; the output is written empty"));
167
168 assert!(
170 decide(Some(Skip), &[q("a", 0, None), q("b", 0, None)], true)
171 .skip
172 .is_some()
173 );
174 assert!(decide(Some(Fail), &[q("a", 0, None)], true).fail.is_some());
175
176 let d = decide(Some(Skip), &[q("a", 0, None), q("b", 3, None)], true);
178 assert!(d.skip.is_none() && d.warnings.is_empty());
179 assert!(d.info.unwrap().contains("`a` returned no rows; written empty"));
180
181 let d = decide(None, &[q("a", 0, Some(Skip)), q("b", 3, None)], true);
183 assert_eq!(d.drop, vec!["a"]);
184 assert!(d.skip.is_none());
185
186 let d = decide(
188 Some(Continue),
189 &[q("a", 0, Some(Skip)), q("b", 0, Some(Skip))],
190 true,
191 );
192 assert!(d.skip.is_some());
193 assert!(d.warnings[0].contains("although it says `on_empty: continue`"));
194
195 let d = decide(None, &[q("a", 0, Some(Fail)), q("b", 3, None)], true);
197 assert!(d.fail.unwrap().contains("query `a` returned no rows"));
198
199 for o in [Skip, Fail] {
201 let d = decide(Some(o), &[q("a", 0, Some(Continue))], true);
202 assert!(d.skip.is_none() && d.fail.is_none(), "{o:?}");
203 assert!(d.warnings[0].contains("which wins over the output's"), "{o:?}");
204 }
205
206 let d = decide(Some(Skip), &[q("a", 0, Some(Skip)), q("b", 3, None)], false);
208 assert!(d.drop.is_empty() && d.skip.is_none());
209 let d = decide(Some(Skip), &[q("a", 0, Some(Skip))], false);
210 assert!(d.skip.is_some());
211 }
212}