lsor_core/
page.rs

1use async_graphql::SimpleObject;
2use serde::{Deserialize, Serialize};
3
4use crate::{
5    aggregate::Aggregate,
6    column::col,
7    cond::{and, gt, lt},
8    cursor::Cursor,
9    driver::PushPrql,
10    either::if_then_else,
11    expr::{case, count, sum, when},
12    filter::Filtered,
13    sort::SortedBy,
14    var::{one, zero},
15};
16
17#[derive(
18    Clone,
19    Copy,
20    Debug,
21    Default,
22    Deserialize,
23    Eq,
24    Hash,
25    PartialEq,
26    PartialOrd,
27    Ord,
28    Serialize,
29    SimpleObject,
30)]
31#[graphql(rename_fields = "snake_case")]
32pub struct TotalCount {
33    pub total_count: i64,
34}
35
36#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
37pub struct Pagination {
38    pub cursor: Cursor,
39    pub after: Option<String>,
40    pub before: Option<String>,
41    pub first: usize,
42    pub last: usize,
43}
44
45pub fn select_page_info<Query>(
46    query: Query,
47    cursor: Cursor,
48    start: String,
49    end: String,
50) -> SelectPageInfo<Query> {
51    SelectPageInfo {
52        query,
53        cursor,
54        start,
55        end,
56    }
57}
58
59pub fn select_page_items<Query>(query: Query, pagination: Pagination) -> SelectPageItems<Query> {
60    SelectPageItems { query, pagination }
61}
62
63pub struct SelectPageInfo<Query> {
64    pub query: Query,
65    pub cursor: Cursor,
66    pub start: String,
67    pub end: String,
68}
69
70impl<Query> PushPrql for SelectPageInfo<Query>
71where
72    Query: SortedBy + PushPrql,
73{
74    fn push_to_driver(&self, driver: &mut crate::driver::Driver) {
75        use super::sort::Sorting;
76
77        let sorting = self.query.sorting();
78        let order = sorting.order();
79        let start = self.cursor.decode(&self.start);
80        let end = self.cursor.decode(&self.end);
81
82        Aggregate {
83            query: &self.query,
84            aggregations: vec![
85                (col("total_count"), &count() as &dyn PushPrql),
86                (
87                    col("has_prev_page"),
88                    &gt(
89                        sum(case([when(if_then_else(
90                            order.is_asc(),
91                            || lt(&sorting, &start),
92                            || gt(&sorting, &start),
93                        ))
94                        .then(one())])),
95                        zero(),
96                    ) as &dyn PushPrql,
97                ),
98                (
99                    col("has_next_page"),
100                    &gt(
101                        sum(case([when(if_then_else(
102                            order.is_asc(),
103                            || gt(&sorting, &end),
104                            || lt(&sorting, &end),
105                        ))
106                        .then(one())])),
107                        zero(),
108                    ) as &dyn PushPrql,
109                ),
110            ],
111        }
112        .push_to_driver(driver);
113    }
114}
115
116pub struct SelectPageItems<Query> {
117    pub query: Query,
118    pub pagination: Pagination,
119}
120
121impl<Query> PushPrql for SelectPageItems<Query>
122where
123    Query: SortedBy + PushPrql,
124{
125    fn push_to_driver(&self, driver: &mut crate::driver::Driver) {
126        use super::sort::Sorting;
127
128        let sorting = self.query.sorting();
129        let order = sorting.order();
130
131        let after = self
132            .pagination
133            .after
134            .as_ref()
135            .map(|v| self.pagination.cursor.decode(v))
136            .unwrap_or(self.pagination.cursor.min());
137
138        let before = self
139            .pagination
140            .before
141            .as_ref()
142            .map(|v| self.pagination.cursor.decode(v))
143            .unwrap_or(self.pagination.cursor.max());
144
145        Filtered {
146            query: &self.query,
147            filter: and(
148                if_then_else(
149                    order.is_asc(),
150                    || gt(&sorting, &after),
151                    || lt(&sorting, &after),
152                ),
153                if_then_else(
154                    order.is_asc(),
155                    || lt(&sorting, &before),
156                    || gt(&sorting, &before),
157                ),
158            ),
159        }
160        .take(self.pagination.first)
161        .sort(sorting.flip())
162        .take(self.pagination.last)
163        .sort(&sorting)
164        .push_to_driver(driver);
165    }
166}
167
168#[cfg(test)]
169mod test {
170    use super::*;
171    use crate::{driver::Driver, from::from, table::table};
172
173    #[test]
174    fn test_select_page_info() {
175        let mut driver = Driver::new();
176        {
177            let query = from(table("page"))
178                .derive("cursor", col("created_at"))
179                .sort(col("cursor").asc());
180            let cursor = Cursor::String;
181            let start = "start".to_string();
182            let end = "end".to_string();
183            let select_page_info = SelectPageInfo {
184                query,
185                cursor,
186                start,
187                end,
188            };
189            select_page_info.push_to_driver(&mut driver);
190        }
191        assert_eq!(driver.sql(), "WITH table_2 AS (SELECT COUNT(*) AS total_count, created_at AS _expr_0, COALESCE(SUM(CASE WHEN created_at < $1 THEN 1 ELSE NULL END), 0) AS _expr_2 FROM page), table_0 AS (SELECT total_count, _expr_2 > 0 AS has_prev_page, _expr_0, COALESCE(SUM(CASE WHEN _expr_0 > $2 THEN 1 ELSE NULL END), 0) AS _expr_1 FROM table_2), table_1 AS (SELECT total_count, has_prev_page, _expr_1 > 0 AS has_next_page, _expr_0 FROM table_0) SELECT total_count, has_prev_page, has_next_page FROM table_1");
192    }
193
194    #[test]
195    fn test_select_page_items() {
196        let mut driver = Driver::new();
197        {
198            let query = from(table("page"))
199                .derive("cursor", col("created_at"))
200                .sort(col("cursor").desc());
201            let cursor = Cursor::String;
202            let after = Some("after".to_string());
203            let before = Some("before".to_string());
204            let select_page_info = SelectPageItems {
205                query,
206                pagination: Pagination {
207                    cursor,
208                    after,
209                    before,
210                    first: 10,
211                    last: 5,
212                },
213            };
214            select_page_info.push_to_driver(&mut driver);
215        }
216        assert_eq!(driver.sql(), "WITH table_2 AS (SELECT *, created_at AS cursor FROM page), table_1 AS (SELECT * FROM table_2 WHERE cursor < $1 AND cursor > $2 ORDER BY cursor DESC LIMIT 10), table_0 AS (SELECT * FROM table_1 ORDER BY cursor LIMIT 5) SELECT * FROM table_0 ORDER BY cursor DESC");
217    }
218}