sqlx_paginated/paginated_query_as/builders/
paginated_query_builder.rs

1use crate::paginated_query_as::examples::postgres_examples::build_query_with_safe_defaults;
2use crate::paginated_query_as::internal::quote_identifier;
3use crate::paginated_query_as::models::QuerySortDirection;
4use crate::{FlatQueryParams, PaginatedResponse, QueryParams};
5use serde::Serialize;
6use sqlx::postgres::PgArguments;
7use sqlx::{postgres::Postgres, query::QueryAs, Execute, FromRow, IntoArguments, Pool};
8
9pub struct PaginatedQueryBuilder<'q, T, A>
10where
11    T: for<'r> FromRow<'r, <Postgres as sqlx::Database>::Row> + Send + Unpin,
12{
13    query: QueryAs<'q, Postgres, T, A>,
14    params: QueryParams<'q, T>,
15    totals_count_enabled: bool,
16    build_query_fn: fn(&QueryParams<T>) -> (Vec<String>, PgArguments),
17}
18
19/// A builder for constructing and executing paginated queries.
20///
21/// This builder provides a fluent interface for creating paginated queries.
22/// For more examples explore `examples/paginated_query_builder_advanced_examples.rs`
23///
24/// # Type Parameters
25///
26/// * `'q`: The lifetime of the query and its arguments
27/// * `T`: The model type that the query will return
28/// * `A`: The type of the query arguments
29///
30/// # Generic Constraints
31///
32/// * `T`: Must be deserializable from Postgres rows (`FromRow`), `Send`, and `Unpin`
33/// * `A`: Must be compatible with Postgres arguments and `Send`
34///
35/// (Attention: Only `Pool<Postgres>` is supported at the moment)
36impl<'q, T, A> PaginatedQueryBuilder<'q, T, A>
37where
38    T: for<'r> FromRow<'r, <Postgres as sqlx::Database>::Row> + Send + Unpin + Serialize + Default,
39    A: 'q + IntoArguments<'q, Postgres> + Send,
40{
41    /// Creates a new `PaginatedQueryBuilder` with default settings.
42    ///
43    /// # Arguments
44    ///
45    /// * `query` - The base query to paginate
46    ///
47    /// # Default Settings
48    ///
49    /// - Totals calculation is enabled
50    /// - Uses default query parameters
51    /// - Uses safe default query building function
52    ///
53    /// # Examples
54    ///
55    /// ```rust
56    /// use sqlx::{FromRow, Postgres};
57    /// use serde::{Serialize};
58    /// use sqlx_paginated::PaginatedQueryBuilder;
59    ///
60    /// #[derive(Serialize, FromRow, Default)]
61    /// struct UserExample {
62    ///     name: String
63    /// }
64    /// let base_query = sqlx::query_as::<_, UserExample>("SELECT * FROM users");
65    /// let builder = PaginatedQueryBuilder::new(base_query);
66    /// ```
67    pub fn new(query: QueryAs<'q, Postgres, T, A>) -> Self {
68        Self {
69            query,
70            params: FlatQueryParams::default().into(),
71            totals_count_enabled: true,
72            build_query_fn: |params| build_query_with_safe_defaults::<T, Postgres>(params),
73        }
74    }
75
76    pub fn with_query_builder(
77        self,
78        build_query_fn: fn(&QueryParams<T>) -> (Vec<String>, PgArguments),
79    ) -> Self {
80        Self {
81            build_query_fn,
82            ..self
83        }
84    }
85
86    pub fn with_params(mut self, params: impl Into<QueryParams<'q, T>>) -> Self {
87        self.params = params.into();
88        self
89    }
90
91    /// Disables the calculation of total record count.
92    ///
93    /// When disabled, the response will not include total count or total pages.
94    /// This can improve query performance for large datasets where the total
95    /// count is not needed.
96    ///
97    /// # Returns
98    ///
99    /// Returns self for method chaining
100    pub fn disable_totals_count(mut self) -> Self {
101        self.totals_count_enabled = false;
102        self
103    }
104
105    /// Executes the paginated query and returns the results.
106    ///
107    /// # Arguments
108    ///
109    /// * `pool` - Database connection pool (Attention: Only `Pool<Postgres>` is supported at the moment)
110    ///
111    /// # Returns
112    ///
113    /// Returns a Result containing a `PaginatedResponse<T>` with:
114    /// - Records for the requested page
115    /// - Optional Pagination information (if enabled)
116    /// - Optional total count and total pages (if enabled)
117    ///
118    /// # Errors
119    ///
120    /// Returns `sqlx::Error` if the query execution fails
121    pub async fn fetch_paginated(
122        self,
123        pool: &Pool<Postgres>,
124    ) -> Result<PaginatedResponse<T>, sqlx::Error> {
125        let base_sql = self.build_base_query();
126        let (conditions, main_arguments) = (self.build_query_fn)(&self.params);
127        let where_clause = self.build_where_clause(&conditions);
128
129        let (total, total_pages, pagination) = if self.totals_count_enabled {
130            let (_, count_arguments) = (self.build_query_fn)(&self.params);
131            let pagination_arguments = self.params.pagination.clone();
132
133            let count_sql = format!(
134                "{} SELECT COUNT(*) FROM base_query{}",
135                base_sql, where_clause
136            );
137            let count: i64 = sqlx::query_scalar_with(&count_sql, count_arguments)
138                .fetch_one(pool)
139                .await?;
140
141            let available_pages = match count {
142                0 => 0,
143                _ => (count + pagination_arguments.page_size - 1) / pagination_arguments.page_size,
144            };
145
146            (
147                Some(count),
148                Some(available_pages),
149                Some(pagination_arguments),
150            )
151        } else {
152            (None, None, None)
153        };
154
155        let mut main_sql = format!("{} SELECT * FROM base_query{}", base_sql, where_clause);
156
157        main_sql.push_str(&self.build_order_clause());
158        main_sql.push_str(&self.build_limit_offset_clause());
159
160        let records = sqlx::query_as_with::<Postgres, T, _>(&main_sql, main_arguments)
161            .fetch_all(pool)
162            .await?;
163
164        Ok(PaginatedResponse {
165            records,
166            pagination,
167            total,
168            total_pages,
169        })
170    }
171
172    /// Builds the base query with CTE (Common Table Expression).
173    ///
174    /// # Returns
175    ///
176    /// Returns the SQL string for the base query wrapped in a CTE
177    fn build_base_query(&self) -> String {
178        format!("WITH base_query AS ({})", self.query.sql())
179    }
180
181    /// Builds the WHERE clause from the provided conditions.
182    ///
183    /// # Arguments
184    ///
185    /// * `conditions` - Vector of condition strings to join with AND
186    ///
187    /// # Returns
188    ///
189    /// Returns the formatted WHERE clause or empty string if no conditions
190    fn build_where_clause(&self, conditions: &[String]) -> String {
191        if conditions.is_empty() {
192            String::new()
193        } else {
194            format!(" WHERE {}", conditions.join(" AND "))
195        }
196    }
197
198    /// Builds the ORDER BY clause based on sort parameters.
199    ///
200    /// # Returns
201    ///
202    /// Returns the formatted ORDER BY clause with proper column quoting
203    fn build_order_clause(&self) -> String {
204        let order = match self.params.sort.sort_direction {
205            QuerySortDirection::Ascending => "ASC",
206            QuerySortDirection::Descending => "DESC",
207        };
208        let column_name = quote_identifier(&self.params.sort.sort_column);
209
210        format!(" ORDER BY {} {}", column_name, order)
211    }
212
213    fn build_limit_offset_clause(&self) -> String {
214        let pagination = &self.params.pagination;
215        let offset = (pagination.page - 1) * pagination.page_size;
216
217        format!(" LIMIT {} OFFSET {}", pagination.page_size, offset)
218    }
219}