Skip to main content

sqlx_paginated/paginated_query_as/builders/
paginated_query_builder.rs

1use crate::paginated_query_as::internal::quote_identifier;
2use crate::paginated_query_as::models::QuerySortDirection;
3use crate::{FlatQueryParams, PaginatedResponse, QueryParams};
4use serde::Serialize;
5use sqlx::{query::QueryAs, Database, Execute, Executor, FromRow, IntoArguments, Pool};
6
7type QueryBuilderFn<T, DB> = Box<
8    dyn for<'p> Fn(&'p QueryParams<T>) -> (Vec<String>, <DB as Database>::Arguments<'p>)
9        + Send
10        + Sync,
11>;
12
13pub struct PaginatedQueryBuilder<'q, T, DB, A>
14where
15    DB: Database,
16    T: for<'r> FromRow<'r, <DB as Database>::Row> + Send + Unpin,
17{
18    query: QueryAs<'q, DB, T, A>,
19    params: QueryParams<'q, T>,
20    totals_count_enabled: bool,
21    build_query_fn: QueryBuilderFn<T, DB>,
22}
23
24/// A builder for constructing and executing paginated queries.
25///
26/// This builder provides a fluent interface for creating paginated queries.
27/// For more examples explore `examples/paginated_query_builder_advanced_examples.rs`
28///
29/// # Type Parameters
30///
31/// * `'q`: The lifetime of the query and its arguments
32/// * `T`: The model type that the query will return
33/// * `A`: The type of the query arguments
34///
35/// # Generic Constraints
36///
37/// * `T`: Must be deserializable from Postgres rows (`FromRow`), `Send`, and `Unpin`
38/// * `A`: Must be compatible with Postgres arguments and `Send`
39///
40impl<'q, T, DB, A> PaginatedQueryBuilder<'q, T, DB, A>
41where
42    DB: Database,
43    T: for<'r> FromRow<'r, <DB as Database>::Row> + Send + Unpin + Serialize + Default,
44    A: 'q + IntoArguments<'q, DB> + Send,
45    DB::Arguments<'q>: IntoArguments<'q, DB>,
46    for<'c> &'c Pool<DB>: Executor<'c, Database = DB>,
47    usize: sqlx::ColumnIndex<<DB as Database>::Row>,
48    i64: sqlx::Type<DB> + for<'r> sqlx::Decode<'r, DB> + Send + Unpin,
49{
50    /// Creates a new `PaginatedQueryBuilder` with default settings.
51    ///
52    /// # Arguments
53    ///
54    /// * `query` - The base query to paginate
55    /// * `build_query_fn` - Function to build query conditions and arguments
56    ///
57    /// # Default Settings
58    ///
59    /// - Totals calculation is enabled
60    /// - Uses default query parameters
61    ///
62    /// # Examples
63    ///
64    /// ```rust
65    /// use sqlx::{FromRow, Postgres};
66    /// use serde::{Serialize};
67    /// use sqlx_paginated::PaginatedQueryBuilder;
68    ///
69    /// #[derive(Serialize, FromRow, Default)]
70    /// struct UserExample {
71    ///     name: String
72    /// }
73    /// let base_query = sqlx::query_as::<Postgres, UserExample>("SELECT * FROM users");
74    /// let builder = PaginatedQueryBuilder::new(base_query, |params| {
75    ///     sqlx_paginated::QueryBuilder::<UserExample, Postgres>::new()
76    ///         .with_search(params)
77    ///         .with_filters(params)
78    ///         .with_date_range(params)
79    ///         .build()
80    /// });
81    /// ```
82    pub fn new<F>(query: QueryAs<'q, DB, T, A>, build_query_fn: F) -> Self
83    where
84        F: for<'p> Fn(&'p QueryParams<T>) -> (Vec<String>, DB::Arguments<'p>)
85            + Send
86            + Sync
87            + 'static,
88    {
89        Self {
90            query,
91            params: FlatQueryParams::default().into(),
92            totals_count_enabled: true,
93            build_query_fn: Box::new(build_query_fn),
94        }
95    }
96
97    pub fn with_query_builder<F>(mut self, build_query_fn: F) -> Self
98    where
99        F: for<'p> Fn(&'p QueryParams<T>) -> (Vec<String>, DB::Arguments<'p>)
100            + Send
101            + Sync
102            + 'static,
103    {
104        self.build_query_fn = Box::new(build_query_fn);
105        self
106    }
107
108    pub fn with_params(mut self, params: impl Into<QueryParams<'q, T>>) -> Self {
109        self.params = params.into();
110        self
111    }
112
113    /// Disables the calculation of total record count.
114    ///
115    /// When disabled, the response will not include total count or total pages.
116    /// This can improve query performance for large datasets where the total
117    /// count is not needed.
118    ///
119    /// # Returns
120    ///
121    /// Returns self for method chaining
122    pub fn disable_totals_count(mut self) -> Self {
123        self.totals_count_enabled = false;
124        self
125    }
126
127    /// Builds the base query with CTE (Common Table Expression).
128    ///
129    /// # Returns
130    ///
131    /// Returns the SQL string for the base query wrapped in a CTE
132    fn build_base_query(&self) -> String {
133        format!("WITH base_query AS ({})", self.query.sql())
134    }
135
136    /// Builds the WHERE clause from the provided conditions.
137    ///
138    /// # Arguments
139    ///
140    /// * `conditions` - Vector of condition strings to join with AND
141    ///
142    /// # Returns
143    ///
144    /// Returns the formatted WHERE clause or empty string if no conditions
145    fn build_where_clause(&self, conditions: &[String]) -> String {
146        if conditions.is_empty() {
147            String::new()
148        } else {
149            format!(" WHERE {}", conditions.join(" AND "))
150        }
151    }
152
153    /// Builds the ORDER BY clause based on sort parameters.
154    ///
155    /// # Returns
156    ///
157    /// Returns the formatted ORDER BY clause with proper column quoting
158    fn build_order_clause(&self) -> String {
159        let order = match self.params.sort.sort_direction {
160            QuerySortDirection::Ascending => "ASC",
161            QuerySortDirection::Descending => "DESC",
162        };
163        let column_name = quote_identifier(&self.params.sort.sort_column);
164
165        format!(" ORDER BY {} {}", column_name, order)
166    }
167
168    fn build_limit_offset_clause(&self) -> String {
169        let pagination = &self.params.pagination;
170        let offset = (pagination.page - 1) * pagination.page_size;
171
172        format!(" LIMIT {} OFFSET {}", pagination.page_size, offset)
173    }
174}
175
176#[cfg(feature = "postgres")]
177impl<'q, T, A> PaginatedQueryBuilder<'q, T, sqlx::Postgres, A>
178where
179    T: for<'r> FromRow<'r, <sqlx::Postgres as sqlx::Database>::Row>
180        + Send
181        + Unpin
182        + Serialize
183        + Default,
184    A: 'q + IntoArguments<'q, sqlx::Postgres> + Send,
185{
186    /// Creates a new `PaginatedQueryBuilder` for PostgreSQL with default settings.
187    ///
188    /// # Arguments
189    ///
190    /// * `query` - The base query to paginate
191    ///
192    /// # Default Settings
193    ///
194    /// - Totals calculation is enabled
195    /// - Uses default query parameters
196    /// - Uses safe default query building function
197    ///
198    /// # Examples
199    ///
200    /// ```rust
201    /// use sqlx::{FromRow, Postgres};
202    /// use serde::{Serialize};
203    /// use sqlx_paginated::PaginatedQueryBuilder;
204    ///
205    /// #[derive(Serialize, FromRow, Default)]
206    /// struct UserExample {
207    ///     name: String
208    /// }
209    /// let base_query = sqlx::query_as::<Postgres, UserExample>("SELECT * FROM users");
210    /// let builder = PaginatedQueryBuilder::<UserExample, Postgres, _>::new_with_defaults(base_query);
211    /// ```
212    pub fn new_with_defaults(query: sqlx::query::QueryAs<'q, sqlx::Postgres, T, A>) -> Self {
213        use crate::paginated_query_as::examples::postgres_examples::build_query_with_safe_defaults;
214        Self::new(query, |params| {
215            build_query_with_safe_defaults::<T, sqlx::Postgres>(params)
216        })
217    }
218
219    /// Executes the paginated query and returns the results.
220    ///
221    /// # Arguments
222    ///
223    /// * `pool` - PostgreSQL database connection pool
224    ///
225    /// # Returns
226    ///
227    /// Returns a Result containing a `PaginatedResponse<T>` with:
228    /// - Records for the requested page
229    /// - Optional Pagination information (if enabled)
230    /// - Optional total count and total pages (if enabled)
231    ///
232    /// # Errors
233    ///
234    /// Returns `sqlx::Error` if the query execution fails
235    ///
236    /// # Example
237    ///
238    /// ```rust,no_run
239    /// use sqlx::{FromRow, PgPool, Postgres};
240    /// use serde::Serialize;
241    /// use sqlx_paginated::{PaginatedQueryBuilder, QueryParamsBuilder};
242    ///
243    /// #[derive(Serialize, FromRow, Default)]
244    /// struct User {
245    ///     id: i32,
246    ///     name: String,
247    /// }
248    ///
249    /// # async fn example(pool: PgPool) -> Result<(), sqlx::Error> {
250    /// let params = QueryParamsBuilder::<User>::new()
251    ///     .with_pagination(1, 10)
252    ///     .build();
253    ///
254    /// let result = PaginatedQueryBuilder::<User, Postgres, _>::new_with_defaults(
255    ///     sqlx::query_as::<Postgres, User>("SELECT * FROM users")
256    /// )
257    /// .with_params(params)
258    /// .fetch_paginated(&pool)
259    /// .await?;
260    /// # Ok(())
261    /// # }
262    /// ```
263    pub async fn fetch_paginated(
264        self,
265        pool: &sqlx::PgPool,
266    ) -> Result<PaginatedResponse<T>, sqlx::Error> {
267        let base_sql = self.build_base_query();
268        let params_ref = &self.params;
269        let (conditions, main_arguments) = (self.build_query_fn)(params_ref);
270        let where_clause = self.build_where_clause(&conditions);
271
272        let count_sql = if self.totals_count_enabled {
273            Some(format!(
274                "{} SELECT COUNT(*) FROM base_query{}",
275                base_sql, where_clause
276            ))
277        } else {
278            None
279        };
280
281        let mut main_sql = format!("{} SELECT * FROM base_query{}", base_sql, where_clause);
282        main_sql.push_str(&self.build_order_clause());
283        main_sql.push_str(&self.build_limit_offset_clause());
284
285        let (total, total_pages, pagination) = if self.totals_count_enabled {
286            let (_, count_arguments) = (self.build_query_fn)(params_ref);
287            let pagination_arguments = self.params.pagination.clone();
288            let count_sql_str = count_sql.as_ref().unwrap();
289
290            let count: i64 = sqlx::query_scalar_with(count_sql_str, count_arguments)
291                .fetch_one(pool)
292                .await?;
293
294            let available_pages = match count {
295                0 => 0,
296                _ => (count + pagination_arguments.page_size - 1) / pagination_arguments.page_size,
297            };
298
299            (
300                Some(count),
301                Some(available_pages),
302                Some(pagination_arguments),
303            )
304        } else {
305            (None, None, None)
306        };
307
308        // For PostgreSQL, PgArguments doesn't have lifetime constraints
309        let records = sqlx::query_as_with::<sqlx::Postgres, T, _>(&main_sql, main_arguments)
310            .fetch_all(pool)
311            .await?;
312
313        Ok(PaginatedResponse {
314            records,
315            pagination,
316            total,
317            total_pages,
318        })
319    }
320}
321
322#[cfg(feature = "sqlite")]
323impl<'q, T, A> PaginatedQueryBuilder<'q, T, sqlx::Sqlite, A>
324where
325    T: for<'r> FromRow<'r, <sqlx::Sqlite as sqlx::Database>::Row>
326        + Send
327        + Unpin
328        + Serialize
329        + Default,
330    A: 'q + IntoArguments<'q, sqlx::Sqlite> + Send,
331{
332    /// Creates a new `PaginatedQueryBuilder` for SQLite with default settings.
333    ///
334    /// # Arguments
335    ///
336    /// * `query` - The base query to paginate
337    ///
338    /// # Default Settings
339    ///
340    /// - Totals calculation is enabled
341    /// - Uses default query parameters
342    /// - Uses safe default query building function
343    ///
344    /// # Examples
345    ///
346    /// ```rust
347    /// use sqlx::{FromRow, Sqlite};
348    /// use serde::{Serialize};
349    /// use sqlx_paginated::PaginatedQueryBuilder;
350    ///
351    /// #[derive(Serialize, FromRow, Default)]
352    /// struct UserExample {
353    ///     name: String
354    /// }
355    /// let base_query = sqlx::query_as::<Sqlite, UserExample>("SELECT * FROM users");
356    /// let builder = PaginatedQueryBuilder::<UserExample, Sqlite, _>::new_with_defaults(base_query);
357    /// ```
358    pub fn new_with_defaults(query: sqlx::query::QueryAs<'q, sqlx::Sqlite, T, A>) -> Self {
359        use crate::QueryBuilder;
360        Self::new(query, |params| {
361            QueryBuilder::<T, sqlx::Sqlite>::new()
362                .with_search(params)
363                .with_filters(params)
364                .with_date_range(params)
365                .build()
366        })
367    }
368
369    /// Executes the paginated query and returns the results.
370    ///
371    /// # Arguments
372    ///
373    /// * `pool` - SQLite database connection pool
374    ///
375    /// # Returns
376    ///
377    /// Returns a Result containing a `PaginatedResponse<T>` with:
378    /// - Records for the requested page
379    /// - Optional Pagination information (if enabled)
380    /// - Optional total count and total pages (if enabled)
381    ///
382    /// # Errors
383    ///
384    /// Returns `sqlx::Error` if the query execution fails
385    ///
386    /// # Implementation Note
387    ///
388    /// This specialized implementation for SQLite handles lifetime requirements correctly.
389    /// SQLite's `SqliteArguments<'q>` requires that SQL strings live long enough, so this
390    /// implementation ensures all SQL strings are created and kept in scope before executing queries.
391    ///
392    /// # Example
393    ///
394    /// ```rust,no_run
395    /// use sqlx::{FromRow, SqlitePool, Sqlite};
396    /// use serde::Serialize;
397    /// use sqlx_paginated::{PaginatedQueryBuilder, QueryParamsBuilder};
398    ///
399    /// #[derive(Serialize, FromRow, Default)]
400    /// struct User {
401    ///     id: i32,
402    ///     name: String,
403    /// }
404    ///
405    /// # async fn example(pool: SqlitePool) -> Result<(), sqlx::Error> {
406    /// let params = QueryParamsBuilder::<User>::new()
407    ///     .with_pagination(1, 10)
408    ///     .build();
409    ///
410    /// let result = PaginatedQueryBuilder::<User, Sqlite, _>::new_with_defaults(
411    ///     sqlx::query_as::<Sqlite, User>("SELECT * FROM users")
412    /// )
413    /// .with_params(params)
414    /// .fetch_paginated(&pool)
415    /// .await?;
416    /// # Ok(())
417    /// # }
418    /// ```
419    pub async fn fetch_paginated(
420        self,
421        pool: &sqlx::SqlitePool,
422    ) -> Result<PaginatedResponse<T>, sqlx::Error> {
423        let base_sql = self.build_base_query();
424        let params_ref = &self.params;
425        let (conditions, main_arguments) = (self.build_query_fn)(params_ref);
426        let where_clause = self.build_where_clause(&conditions);
427
428        // Build all SQL strings first and keep them in scope
429        // This ensures they live long enough for SqliteArguments<'q>
430        let count_sql = if self.totals_count_enabled {
431            Some(format!(
432                "{} SELECT COUNT(*) FROM base_query{}",
433                base_sql, where_clause
434            ))
435        } else {
436            None
437        };
438
439        let mut main_sql = format!("{} SELECT * FROM base_query{}", base_sql, where_clause);
440        main_sql.push_str(&self.build_order_clause());
441        main_sql.push_str(&self.build_limit_offset_clause());
442
443        let (total, total_pages, pagination) = if self.totals_count_enabled {
444            let (_, count_arguments) = (self.build_query_fn)(params_ref);
445            let pagination_arguments = self.params.pagination.clone();
446            let count_sql_str = count_sql.as_ref().unwrap();
447            let count: i64 = sqlx::query_scalar_with(count_sql_str, count_arguments)
448                .fetch_one(pool)
449                .await?;
450
451            let available_pages = match count {
452                0 => 0,
453                _ => (count + pagination_arguments.page_size - 1) / pagination_arguments.page_size,
454            };
455
456            (
457                Some(count),
458                Some(available_pages),
459                Some(pagination_arguments),
460            )
461        } else {
462            (None, None, None)
463        };
464
465        let records = sqlx::query_as_with::<sqlx::Sqlite, T, _>(&main_sql, main_arguments)
466            .fetch_all(pool)
467            .await?;
468
469        Ok(PaginatedResponse {
470            records,
471            pagination,
472            total,
473            total_pages,
474        })
475    }
476}