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;