sqlx_paginated/paginated_query_as/builders/query_builders/
query_builder.rs

1use crate::paginated_query_as::internal::{quote_identifier, ColumnProtection, QueryDialect};
2use crate::QueryParams;
3use chrono::{DateTime, Utc};
4use serde::Serialize;
5use sqlx::{Arguments, Database, Encode, Type};
6use std::marker::PhantomData;
7
8pub struct QueryBuilder<'q, T, DB: Database> {
9    pub conditions: Vec<String>,
10    pub arguments: DB::Arguments<'q>,
11    pub(crate) valid_columns: Vec<String>,
12    pub(crate) protection: Option<ColumnProtection>,
13    pub(crate) protection_enabled: bool,
14    pub(crate) dialect: Box<dyn QueryDialect>,
15    pub(crate) _phantom: PhantomData<&'q T>,
16}
17
18impl<'q, T, DB> QueryBuilder<'q, T, DB>
19where
20    T: Default + Serialize,
21    DB: Database,
22    String: for<'a> Encode<'a, DB> + Type<DB>,
23{
24    /// Checks if a column exists in the list of valid columns for T struct.
25    ///
26    /// # Arguments
27    ///
28    /// * `column` - The name of the column to check
29    ///
30    /// # Returns
31    ///
32    /// Returns `true` if the column exists in the valid columns list, `false` otherwise.
33    pub(crate) fn has_column(&self, column: &str) -> bool {
34        self.valid_columns.contains(&column.to_string())
35    }
36
37    fn is_column_safe(&self, column: &str) -> bool {
38        let column_exists = self.has_column(column);
39
40        if !self.protection_enabled {
41            return column_exists;
42        }
43
44        match &self.protection {
45            Some(protection) => column_exists && protection.is_safe(column),
46            None => column_exists,
47        }
48    }
49
50    /// Adds search functionality to the query by creating LIKE conditions for specified columns.
51    ///
52    /// # Arguments
53    ///
54    /// * `params` - Query parameters containing search text and columns to search in
55    ///
56    /// # Details
57    ///
58    /// - Only searches in columns that are both specified and considered safe
59    /// - Creates case-insensitive LIKE conditions with wildcards
60    /// - Multiple search columns are combined with OR operators
61    /// - Empty search text or no valid columns results in no conditions being added
62    ///
63    /// # Returns
64    ///
65    /// Returns self for method chaining
66    ///
67    /// # Example
68    ///
69    /// ```rust
70    /// use sqlx::Postgres;
71    /// use serde::{Serialize};
72    /// use sqlx_paginated::{QueryBuilder, QueryParamsBuilder};
73    ///
74    /// #[derive(Serialize, Default)]
75    /// struct UserExample {
76    ///     name: String
77    /// }
78    ///
79    /// let initial_params = QueryParamsBuilder::<UserExample>::new()
80    ///         .with_search("john", vec!["name", "email"])
81    ///         .build();
82    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
83    ///     .with_search(&initial_params)
84    ///     .build();
85    /// ```
86    pub fn with_search(mut self, params: &QueryParams<T>) -> Self {
87        if let Some(search) = &params.search.search {
88            if let Some(columns) = &params.search.search_columns {
89                let valid_search_columns: Vec<&String> = columns
90                    .iter()
91                    .filter(|column| self.is_column_safe(column))
92                    .collect();
93
94                if !valid_search_columns.is_empty() && !search.trim().is_empty() {
95                    let pattern = format!("%{}%", search);
96                    let next_argument = self.arguments.len() + 1;
97
98                    let search_conditions: Vec<String> = valid_search_columns
99                        .iter()
100                        .map(|column| {
101                            let table_column = self.dialect.quote_identifier(column);
102                            let placeholder = self.dialect.placeholder(next_argument);
103                            format!("LOWER({}) LIKE LOWER({})", table_column, placeholder)
104                        })
105                        .collect();
106
107                    if !search_conditions.is_empty() {
108                        self.conditions
109                            .push(format!("({})", search_conditions.join(" OR ")));
110                        self.arguments.add(pattern).unwrap_or_default();
111                    }
112                }
113            }
114        }
115        self
116    }
117
118    /// Adds equality filters to the query based on provided key-value pairs.
119    ///
120    /// # Arguments
121    ///
122    /// * `params` - Query parameters containing filters as key-value pairs
123    ///
124    /// # Details
125    ///
126    /// - Only applies filters for columns that exist and are considered safe
127    /// - Automatically handles type casting based on the database dialect
128    /// - Skips invalid columns with a warning when tracing is enabled
129    /// - Null or empty values are ignored
130    ///
131    /// # Returns
132    ///
133    /// Returns self for method chaining
134    ///
135    /// # Example
136    ///
137    /// ```rust
138    /// use sqlx::Postgres;
139    /// use serde::{Serialize};
140    /// use sqlx_paginated::{QueryBuilder, QueryParamsBuilder};
141    ///
142    /// #[derive(Serialize, Default)]
143    /// struct UserExample {
144    ///     name: String
145    /// }
146    ///
147    /// let initial_params = QueryParamsBuilder::<UserExample>::new()
148    ///         .with_search("john", vec!["name", "email"])
149    ///         .build();
150    ///
151    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
152    ///     .with_filters(&initial_params)
153    ///     .build();
154    /// ```
155    pub fn with_filters(mut self, params: &'q QueryParams<T>) -> Self {
156        for (key, value) in &params.filters {
157            if self.is_column_safe(key) {
158                if let Some(value) = value {
159                    let table_column = self.dialect.quote_identifier(key);
160                    let type_cast = self.dialect.type_cast(value);
161                    let next_argument = self.arguments.len() + 1;
162                    let placeholder = self.dialect.placeholder(next_argument);
163
164                    self.conditions
165                        .push(format!("{} = {}{}", table_column, placeholder, type_cast));
166                    self.arguments.add(value).unwrap_or_default();
167                }
168            } else {
169                #[cfg(feature = "tracing")]
170                tracing::warn!(column = %key, "Skipping invalid filter column");
171            }
172        }
173        self
174    }
175
176    /// Adds date range conditions to the query for a specified date column.
177    ///
178    /// # Arguments
179    ///
180    /// * `params` - Query parameters containing date range information
181    ///
182    /// # Type Parameters
183    ///
184    /// Requires `DateTime<Utc>` to be encodable for the target database
185    ///
186    /// # Details
187    ///
188    /// - Adds >= condition for date_after if specified
189    /// - Adds <= condition for date_before if specified
190    /// - Only applies to columns that exist and are considered safe
191    /// - Skips invalid date columns with a warning when tracing is enabled
192    ///
193    /// # Returns
194    ///
195    /// Returns self for method chaining
196    ///
197    /// # Example
198    ///
199    /// ```rust
200    /// use sqlx::Postgres;
201    /// use serde::{Serialize};
202    /// use chrono::{DateTime};
203    /// use sqlx_paginated::{QueryBuilder, QueryParamsBuilder, QueryParams};
204    ///
205    /// #[derive(Serialize, Default)]
206    /// struct UserExample {
207    ///     name: String
208    /// }
209    ///
210    /// let initial_params = QueryParamsBuilder::<UserExample>::new()
211    ///         .with_date_range(None, Some(DateTime::parse_from_rfc3339("2024-12-31T23:59:59Z").unwrap().into()), Some("deleted_at"))
212    ///         .build();
213    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
214    ///     .with_date_range(&initial_params)
215    ///     .build();
216    /// ```
217    pub fn with_date_range(mut self, params: &'q QueryParams<T>) -> Self
218    where
219        DateTime<Utc>: for<'a> Encode<'a, DB> + Type<DB>,
220    {
221        if let Some(date_column) = &params.date_range.date_column {
222            if self.is_column_safe(date_column) {
223                if let Some(after) = params.date_range.date_after {
224                    let next_argument = self.arguments.len() + 1;
225                    self.conditions
226                        .push(format!("{} >= ${}", date_column, next_argument));
227                    self.arguments.add(after).unwrap_or_default();
228                }
229
230                if let Some(before) = params.date_range.date_before {
231                    let next_argument = self.arguments.len() + 1;
232                    self.conditions
233                        .push(format!("{} <= ${}", date_column, next_argument));
234                    self.arguments.add(before).unwrap_or_default();
235                }
236            } else {
237                #[cfg(feature = "tracing")]
238                tracing::warn!(column = %date_column, "Skipping invalid date column");
239            }
240        }
241
242        self
243    }
244
245    /// Adds a custom condition for a specific column with a provided operator and value.
246    ///
247    /// # Arguments
248    ///
249    /// * `column` - The column name to apply the condition to
250    /// * `condition` - The operator or condition to use (e.g., ">", "LIKE", etc.)
251    /// * `value` - The value to compare against
252    ///
253    /// # Details
254    ///
255    /// - Only applies to columns that exist and are considered safe
256    /// - Automatically handles parameter binding
257    /// - Skips invalid columns with a warning when tracing is enabled
258    ///
259    /// # Returns
260    ///
261    /// Returns self for method chaining
262    ///
263    /// # Example
264    ///
265    /// ```rust
266    /// use sqlx::Postgres;
267    /// use serde::{Serialize};
268    /// use sqlx_paginated::{QueryBuilder};
269    ///
270    /// #[derive(Serialize, Default)]
271    /// struct UserExample {
272    ///     name: String
273    /// }
274    ///
275    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
276    ///     .with_condition("age", ">", "18".to_string())
277    ///     .build();
278    /// ```
279    pub fn with_condition(
280        mut self,
281        column: &str,
282        condition: impl Into<String>,
283        value: String,
284    ) -> Self {
285        if self.is_column_safe(column) {
286            let next_argument = self.arguments.len() + 1;
287            self.conditions.push(format!(
288                "{} {} ${}",
289                quote_identifier(column),
290                condition.into(),
291                next_argument
292            ));
293            let _ = self.arguments.add(value);
294        } else {
295            #[cfg(feature = "tracing")]
296            tracing::warn!(column = %column, "Skipping invalid condition column");
297        }
298        self
299    }
300
301    /// Adds a raw SQL condition to the query without any safety checks.
302    ///
303    /// # Arguments
304    ///
305    /// * `condition` - Raw SQL condition to add to the query
306    ///
307    /// # Safety
308    ///
309    /// This method bypasses column safety checks. Use with caution to prevent SQL injection.
310    ///
311    /// # Returns
312    ///
313    /// Returns self for method chaining
314    ///
315    /// # Example
316    ///
317    /// ```rust
318    /// use sqlx::Postgres;
319    /// use serde::{Serialize};
320    /// use sqlx_paginated::{QueryBuilder};
321    ///
322    /// #[derive(Serialize, Default)]
323    /// struct UserExample {
324    ///     name: String
325    /// }
326    ///
327    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
328    ///     .with_raw_condition("status != 'deleted'")
329    ///     .build();
330    /// ```
331    pub fn with_raw_condition(mut self, condition: impl Into<String>) -> Self {
332        self.conditions.push(condition.into());
333        self
334    }
335
336    /// Allows adding multiple conditions using a closure.
337    ///
338    /// # Arguments
339    ///
340    /// * `f` - Closure that takes a mutable reference to the QueryBuilder
341    ///
342    /// # Details
343    ///
344    /// Useful for grouping multiple conditions that are logically related
345    ///
346    /// # Returns
347    ///
348    /// Returns self for method chaining
349    ///
350    /// # Example
351    ///
352    /// ```rust
353    /// use sqlx::Postgres;
354    /// use serde::{Serialize};
355    /// use sqlx_paginated::{QueryBuilder};
356    ///
357    /// #[derive(Serialize, Default)]
358    /// struct UserExample {
359    ///     name: String
360    /// }
361    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
362    ///     .with_combined_conditions(|builder| {
363    ///         builder.conditions.push("status = 'active'".to_string());
364    ///         builder.conditions.push("age >= 18".to_string());
365    ///     })
366    ///     .build();
367    /// ```
368    pub fn with_combined_conditions<F>(mut self, f: F) -> Self
369    where
370        F: FnOnce(&mut QueryBuilder<T, DB>),
371    {
372        f(&mut self);
373        self
374    }
375
376    /// Disables column protection for this query builder instance.
377    ///
378    /// # Safety
379    ///
380    /// This removes all column safety checks. Use with caution as it may expose
381    /// the application to SQL injection if used with untrusted input.
382    ///
383    /// # Returns
384    ///
385    /// Returns self for method chaining
386    ///
387    /// # Example
388    ///
389    /// ```rust
390    /// use sqlx::Postgres;
391    /// use serde::{Serialize};
392    /// use sqlx_paginated::{QueryBuilder};
393    ///
394    /// #[derive(Serialize, Default)]
395    /// struct UserExample {
396    ///     name: String
397    /// }
398    ///
399    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
400    ///     .disable_protection()
401    ///     .with_raw_condition("custom_column = 'value'")
402    ///     .build();
403    /// ```
404    pub fn disable_protection(mut self) -> Self {
405        self.protection_enabled = false;
406        self
407    }
408
409    /// Builds the final query conditions and arguments.
410    ///
411    /// # Returns
412    ///
413    /// Returns a tuple containing:
414    /// - Vec<String>: List of SQL conditions
415    /// - DB::Arguments: Database-specific arguments for parameter binding
416    ///
417    /// # Example
418    ///
419    /// ```rust
420    /// use sqlx::Postgres;
421    /// use serde::{Serialize};
422    /// use sqlx_paginated::{QueryBuilder, QueryParamsBuilder};
423    ///
424    /// #[derive(Serialize, Default)]
425    /// struct UserExample {
426    ///     name: String
427    /// }
428    ///
429    /// let initial_params = QueryParamsBuilder::<UserExample>::new()
430    ///         .with_search("john", vec!["name", "email"])
431    ///         .build();
432    /// let (conditions, arguments) = QueryBuilder::<UserExample, Postgres>::new()
433    ///     .with_search(&initial_params)
434    ///     .build();
435    /// ```
436    pub fn build(self) -> (Vec<String>, DB::Arguments<'q>) {
437        (self.conditions, self.arguments)
438    }
439}