Skip to main content

dre_core/
no_data.rs

1//! `on_empty`: what an output does when its queries return no rows.
2//!
3//! Each empty query applies its own `on_empty` first: `continue` keeps its empty section, `skip`
4//! drops its tab (or per-query file), `fail` fails the run. Then, if every query of the output is
5//! empty or dropped, the output's `on_empty` decides. A query's explicit setting wins over the
6//! output's, with a warning that says so. "Empty" means 0 rows: a `count(*)` returning 0 is one
7//! row.
8
9use serde::{Deserialize, Serialize};
10
11/// `on_empty:` on an output or a query entry.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
13#[serde(rename_all = "lowercase")]
14pub enum OnEmpty {
15    /// Write the output (an empty section) and deliver it, with a warning.
16    #[default]
17    Continue,
18    /// Drop the query's tab or file; for an output: write and deliver nothing.
19    Skip,
20    /// Fail the run.
21    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
45/// One query of an output: its name, its rows, and its own `on_empty` if it sets one.
46pub struct Query<'a> {
47    pub name: &'a str,
48    pub rows: u64,
49    pub on_empty: Option<OnEmpty>,
50}
51
52/// What to do with one output.
53#[derive(Debug, Default, PartialEq, Eq)]
54pub struct Decision {
55    /// The queries that returned no rows, in order.
56    pub empty: Vec<String>,
57    /// Queries whose tab (or file) is left out.
58    pub drop: Vec<String>,
59    /// Fail the run, with this message.
60    pub fail: Option<String>,
61    /// Write and deliver nothing, with this reason.
62    pub skip: Option<String>,
63    /// Warnings to show (the output goes out with no data; a conflict was settled).
64    pub warnings: Vec<String>,
65    /// An info line: a query was empty in an output that has data.
66    pub info: Option<String>,
67}
68
69/// Decide what `output` (the output's own setting, if any) does about its queries. `droppable`:
70/// whether a query's section can be left out (false for html, pdf and messages, whose template
71/// decides).
72pub 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    // Each empty query's own setting.
92    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    // Every query is empty (or dropped): the output's setting decides.
120    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    /// The conflict table: output setting x query settings x rows -> outcome.
157    #[test]
158    fn decisions() {
159        // Nothing empty: nothing to do.
160        let d = decide(Some(Fail), &[q("a", 1, None)], true);
161        assert_eq!(d, Decision::default());
162
163        // Default continue, all empty: written, with a warning.
164        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        // Output skip / fail, all empty.
169        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        // Output skip but one query has data: written, the empty one noted.
177        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        // A query's skip drops its tab; the rest goes out.
182        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        // Every tab dropped although the output says continue: skipped, with a warning.
187        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        // A query's fail fails the run even when another query has data.
196        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        // A query's explicit continue wins over the output's skip or fail.
200        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        // html, pdf, messages: a query's skip can't drop a section.
207        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}