Skip to main content

tablo_core/table/
sort.rs

1//! A table's ordering: the [default sort](Table::default_sort) it declares, and the sort and
2//! order-by each request reads.
3
4use toasty::stmt::{List, OrderBy, OrderByExpr, Path};
5
6use super::{BoxColumn, Sort, Table, TableState};
7use crate::schema::Binding;
8
9/// The field a [`default_sort`](Table::default_sort) orders by, and its order-by in the
10/// default's direction.
11#[derive(Clone)]
12pub(super) struct FieldOrder {
13    binding: Binding,
14    order: OrderByExpr,
15}
16
17impl FieldOrder {
18    /// What the order reads of a record.
19    pub(super) fn reads<M>(&self) -> crate::lens::Reads<M> {
20        self.binding.reads::<M>()
21    }
22}
23
24impl<M> Table<M> {
25    /// Sort by the field `path` names, smallest first, when the URL names no sortable column; by
26    /// the first sortable column, ascending, by default. The field needs no column: the rows
27    /// sort by it all the same, and no header marks the sort.
28    ///
29    /// ```rust
30    /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
31    /// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String }
32    /// use tablo_core::{Table, TextColumn, lens};
33    ///
34    /// Table::new(TextColumn::new(lens!(Post.title)).sortable()).default_sort(lens!(Post.title));
35    /// ```
36    pub fn default_sort<T>(self, path: impl Into<Path<M, T>>) -> Self
37    where
38        M: toasty::schema::Model,
39    {
40        self.sorted_by(path.into(), false)
41    }
42
43    /// Sort by the field `path` names, largest first, when the URL names no sortable column, as
44    /// [`default_sort`](Self::default_sort) does ascending.
45    ///
46    /// ```rust
47    /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
48    /// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String, published_at: jiff::Timestamp }
49    /// use tablo_core::{DateColumn, Table, TextColumn, lens};
50    ///
51    /// // The newest posts first.
52    /// Table::new((
53    ///     TextColumn::new(lens!(Post.title)).sortable(),
54    ///     DateColumn::new(lens!(Post.published_at)).sortable(),
55    /// ))
56    /// .default_sort_desc(lens!(Post.published_at));
57    /// ```
58    pub fn default_sort_desc<T>(self, path: impl Into<Path<M, T>>) -> Self
59    where
60        M: toasty::schema::Model,
61    {
62        self.sorted_by(path.into(), true)
63    }
64
65    /// Sort by the sortable column `sort` names when the URL names none: the default for a
66    /// column of your own, whose [`name`](crate::extend::Column::name) no field path spells.
67    /// A field's column takes [`default_sort`](Self::default_sort), whose path a rename fails
68    /// to compile.
69    ///
70    /// ```rust
71    /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
72    /// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String }
73    /// use tablo_core::{Sort, Table, TextColumn, lens};
74    ///
75    /// // "revenue" is the name a column of your own returns.
76    /// Table::new(TextColumn::new(lens!(Post.title))).default_sort_by(Sort::desc("revenue"));
77    /// ```
78    ///
79    /// A name the table declares no sortable column for is a declaration error.
80    pub fn default_sort_by(mut self, sort: Sort) -> Self {
81        self.default_sort = Some(sort);
82        self.default_field = None;
83        self
84    }
85
86    fn sorted_by<T>(mut self, path: Path<M, T>, descending: bool) -> Self
87    where
88        M: toasty::schema::Model,
89    {
90        let binding = Binding::of(&path);
91        // A path that names no field reports that, not a column named after its raw steps.
92        if let Some(error) = binding.misdeclared() {
93            self.misdeclared.push(error);
94            return self;
95        }
96        self.default_sort = Some(Sort {
97            column: binding.name().to_string(),
98            descending,
99        });
100        let order = if descending { path.desc() } else { path.asc() };
101        self.default_field = Some(FieldOrder { binding, order });
102        self
103    }
104
105    /// Whether `sort` orders the rows: it names a sortable column, or it is the default sort on
106    /// a field.
107    pub(super) fn orders_by(&self, sort: &Sort) -> bool {
108        self.sortable_column(&sort.column).is_some() || self.field_order(sort).is_some()
109    }
110
111    /// The order-by of the default sort on a field, when `sort` is that default.
112    fn field_order(&self, sort: &Sort) -> Option<&OrderByExpr> {
113        let field = self.default_field.as_ref()?;
114        (self.default_sort.as_ref() == Some(sort) && field.binding.name() == sort.column)
115            .then_some(&field.order)
116    }
117
118    /// The sortable column named `name`.
119    pub(super) fn sortable_column(&self, name: &str) -> Option<&BoxColumn<M>> {
120        self.columns
121            .iter()
122            .find(|c| c.name() == name && crate::column::sorts(&***c))
123    }
124
125    /// The sort `state` reads: its own when it names a sortable column, else the
126    /// [default](Self::default_sort), else the first sortable column's, ascending. `None` when
127    /// nothing sorts.
128    pub(crate) fn sort_for(&self, state: &TableState) -> Option<Sort> {
129        state
130            .sort
131            .as_ref()
132            .filter(|sort| self.sortable_column(&sort.column).is_some())
133            .or_else(|| {
134                self.default_sort
135                    .as_ref()
136                    .filter(|sort| self.orders_by(sort))
137            })
138            .cloned()
139            .or_else(|| {
140                let first = self.columns.iter().find(|c| crate::column::sorts(&***c))?;
141                Some(Sort::asc(first.name()))
142            })
143    }
144
145    /// The first sortable column's ascending order-by: the order a relationship's choices list in,
146    /// whatever the list's [default](Self::default_sort).
147    pub(crate) fn first_order_by(&self) -> Option<OrderByExpr>
148    where
149        M: toasty::schema::Model,
150    {
151        self.columns.iter().find_map(|c| c.order_by(false))
152    }
153
154    /// The order-by `sort` names.
155    fn order_by(&self, sort: &Sort) -> Option<OrderByExpr>
156    where
157        M: toasty::schema::Model,
158    {
159        match self.sortable_column(&sort.column) {
160            Some(column) => column.order_by(sort.descending),
161            None => self.field_order(sort).cloned(),
162        }
163    }
164
165    /// Resolve the full query ordering for a request: its [grouping](Self::group_by)'s field,
166    /// unless the sort orders by the same expression, then its [sort](Self::sort_for), or the
167    /// primary key when nothing sorts.
168    pub(crate) fn order_bys_for(&self, state: &TableState) -> Vec<OrderByExpr>
169    where
170        M: toasty::schema::Model,
171    {
172        let sort = self.sort_for(state).and_then(|sort| self.order_by(&sort));
173        let mut order: Vec<OrderByExpr> = self
174            .active_grouping(state)
175            .filter(|group| {
176                sort.as_ref()
177                    .is_none_or(|sort| sort.expr != group.order().expr)
178            })
179            .map(|group| group.order().clone())
180            .into_iter()
181            .collect();
182        match sort {
183            Some(ord) => order.push(ord),
184            None => order.extend(crate::toasty_compat::pk::pk_order_bys::<M>()),
185        }
186        order
187    }
188
189    /// Order `query` as `state` sorts it, replacing any ordering `query` carries: Toasty appends
190    /// an `order_by` after the existing ones, so a
191    /// [`ResourceDef::query`](crate::ResourceDef::query) ordering would otherwise outrank the
192    /// user's sort.
193    pub(crate) fn order(
194        &self,
195        mut query: toasty::stmt::Query<List<M>>,
196        state: &TableState,
197    ) -> toasty::stmt::Query<List<M>>
198    where
199        M: toasty::schema::Model,
200    {
201        query.set_order_by(OrderBy {
202            exprs: self.order_bys_for(state),
203        });
204        query
205    }
206
207    /// The ordering an offset page reads: `state`'s sort, then the primary key.
208    ///
209    /// Toasty breaks ties by the key for a cursor only; without it, rows sharing a sort value may
210    /// come back in a different order for each offset, and repeat or go missing across pages.
211    pub(crate) fn offset_order_bys_for(&self, state: &TableState) -> Vec<OrderByExpr>
212    where
213        M: toasty::schema::Model,
214    {
215        let mut order = self.order_bys_for(state);
216        for key in crate::toasty_compat::pk::pk_order_bys::<M>() {
217            if !order.contains(&key) {
218                order.push(key);
219            }
220        }
221        order
222    }
223}
224
225#[cfg(test)]
226mod tests;