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) = ¶ms.search.search {
88 if let Some(columns) = ¶ms.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 ¶ms.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) = ¶ms.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}