Skip to main content

tablo_core/table/
summary.rs

1//! [`Table::summary`]: the counts a list shows under its rows.
2
3use derive_where::derive_where;
4use toasty::stmt::{Expr, List, Query};
5use topcoat::{Result, context::Cx};
6
7use super::{Table, TableState};
8use crate::{Text, lens::Reads};
9
10/// A figure a list shows under its rows: how many records it selects under the reader's tab,
11/// search and filters, across every page, or how many of those a predicate selects.
12///
13/// A summary counts in the database, one count query per summary each time the list renders,
14/// so it reads the whole result set, not the page it shows: mind the cost on a
15/// [`Pagination::Cursor`](super::Pagination::Cursor) list too large to count. A summary with no
16/// predicate reuses the count a [`Pagination::Pages`](super::Pagination::Pages) list takes for
17/// its pager, and a list showing no rows counts nothing. Like a [`ListTab`](super::ListTab),
18/// it is not access control: a summary whose predicate reads a field
19/// [`field_access`](crate::ResourceDef::field_access) hides from the user does not show to them,
20/// the panel reading the fields off the predicate.
21///
22/// ```rust
23/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
24/// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String, published: bool }
25/// use tablo_core::{Summary, Table, TextColumn, lens};
26///
27/// Table::new(TextColumn::new(lens!(Post.title)))
28///     .summary(Summary::count("Posts"))
29///     .summary(Summary::count("Published").query(Post::fields().published().eq(true)));
30/// ```
31#[derive_where(Clone)]
32pub struct Summary<M> {
33    label: Text,
34    /// The records the count admits among the list's; `None` admits every one.
35    query: Option<Expr<bool>>,
36    _model: std::marker::PhantomData<fn() -> M>,
37}
38
39impl<M> Summary<M> {
40    /// Count the records the list selects, labelled `label`.
41    pub fn count(label: impl Into<Text>) -> Self {
42        Self {
43            label: label.into(),
44            query: None,
45            _model: std::marker::PhantomData,
46        }
47    }
48
49    /// Count only the records `predicate` selects among the list's.
50    pub fn query(mut self, predicate: Expr<bool>) -> Self {
51        self.query = Some(predicate);
52        self
53    }
54
55    /// The label, as a declaration error names the summary.
56    pub(crate) fn name(&self) -> String {
57        self.label.english()
58    }
59
60    /// The fields the summary's predicate reads off a row; a summary with no predicate reads
61    /// nothing.
62    pub(crate) fn reads(&self) -> Reads<M>
63    where
64        M: toasty::schema::Model,
65    {
66        self.query
67            .as_ref()
68            .map_or_else(Reads::nothing, Reads::of_predicate)
69    }
70}
71
72impl<M> Table<M> {
73    /// Show `summary` under the rows, after the declared ones.
74    pub fn summary(mut self, summary: Summary<M>) -> Self {
75        self.summaries.push(summary);
76        self
77    }
78
79    /// The declared summaries, in order.
80    pub(crate) fn declared_summaries(&self) -> &[Summary<M>] {
81        &self.summaries
82    }
83
84    /// Each summary's label and how many rows of `query` it counts under `state`'s tab, search
85    /// and filters, but the `refused` filters: one count query per summary, in declaration order,
86    /// but a summary with no predicate, which takes `total` when the pager counted the same rows.
87    ///
88    /// # Errors
89    ///
90    /// The database failing a count.
91    pub(crate) async fn summary_counts(
92        &self,
93        cx: &Cx,
94        query: &Query<List<M>>,
95        state: &TableState,
96        refused: &[String],
97        total: Option<u64>,
98    ) -> Result<Vec<(Text, u64)>>
99    where
100        M: toasty::schema::Model,
101    {
102        let mut counts = Vec::with_capacity(self.summaries.len());
103        if self.summaries.is_empty() {
104            return Ok(counts);
105        }
106        let mut db = crate::db::db(cx)?;
107        let narrowed = self.narrow(cx, &db, query.clone(), state, refused);
108        for summary in &self.summaries {
109            let rows = match (&summary.query, total) {
110                (None, Some(total)) => {
111                    counts.push((summary.label.clone(), total));
112                    continue;
113                }
114                (Some(predicate), _) => narrowed.clone().filter(predicate.clone()),
115                (None, None) => narrowed.clone(),
116            };
117            let count = crate::toasty_compat::count(rows, &mut db)
118                .await
119                .map_err(crate::error::unavailable)?;
120            counts.push((summary.label.clone(), count));
121        }
122        Ok(counts)
123    }
124}