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