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}