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}