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;