sqlx_paginated/paginated_query_as/builders/query_builders/
query_builder.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
use crate::paginated_query_as::internal::{quote_identifier, ColumnProtection, QueryDialect};
use crate::QueryParams;
use chrono::{DateTime, Utc};
use serde::Serialize;
use sqlx::{Arguments, Database, Encode, Type};
use std::marker::PhantomData;

pub struct QueryBuilder<'q, T, DB: Database> {
    pub conditions: Vec<String>,
    pub arguments: DB::Arguments<'q>,
    pub(crate) valid_columns: Vec<String>,
    pub(crate) protection: Option<ColumnProtection>,
    pub(crate) protection_enabled: bool,
    pub(crate) dialect: Box<dyn QueryDialect>,
    pub(crate) _phantom: PhantomData<&'q T>,
}

impl<'q, T, DB> QueryBuilder<'q, T, DB>
where
    T: Default + Serialize,
    DB: Database,
    String: for<'a> Encode<'a, DB> + Type<DB>,
{
    /// Checks if a column exists in the list of valid columns for T struct.
    ///
    /// # Arguments
    ///
    /// * `column` - The name of the column to check
    ///
    /// # Returns
    ///
    /// Returns `true` if the column exists in the valid columns list, `false` otherwise.
    pub(crate) fn has_column(&self, column: &str) -> bool {
        self.valid_columns.contains(&column.to_string())
    }

    fn is_column_safe(&self, column: &str) -> bool {
        let column_exists = self.has_column(column);

        if !self.protection_enabled {
            return column_exists;
        }

        match &self.protection {
            Some(protection) => column_exists && protection.is_safe(column),
            None => column_exists,
        }
    }

    /// Adds search functionality to the query by creating LIKE conditions for specified columns.
    ///
    /// # Arguments
    ///
    /// * `params` - Query parameters containing search text and columns to search in
    ///
    /// # Details
    ///
    /// - Only searches in columns that are both specified and considered safe
    /// - Creates case-insensitive LIKE conditions with wildcards
    /// - Multiple search columns are combined with OR operators
    /// - Empty search text or no valid columns results in no conditions being added
    ///
    /// # Returns
    ///
    /// Returns self for method chaining
    ///
    /// # Example
    ///
    /// ```rust
    /// use sqlx::Postgres;
    /// use serde::{Serialize};
    /// use sqlx_paginated::{QueryBuilder, QueryParamsBuilder};
    ///
    /// #[derive(Serialize, Default)]
    /// struct UserExample {
    ///     name: String
    /// }
    ///
    /// let initial_params = QueryParamsBuilder::<UserExample>::new()
    ///         .with_search("john", vec!["name", "email"])
    ///         .build();
    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
    ///     .with_search(&initial_params)
    ///     .build();
    /// ```
    pub fn with_search(mut self, params: &QueryParams<T>) -> Self {
        if let Some(search) = &params.search.search {
            if let Some(columns) = &params.search.search_columns {
                let valid_search_columns: Vec<&String> = columns
                    .iter()
                    .filter(|column| self.is_column_safe(column))
                    .collect();

                if !valid_search_columns.is_empty() && !search.trim().is_empty() {
                    let pattern = format!("%{}%", search);
                    let next_argument = self.arguments.len() + 1;

                    let search_conditions: Vec<String> = valid_search_columns
                        .iter()
                        .map(|column| {
                            let table_column = self.dialect.quote_identifier(column);
                            let placeholder = self.dialect.placeholder(next_argument);
                            format!("LOWER({}) LIKE LOWER({})", table_column, placeholder)
                        })
                        .collect();

                    if !search_conditions.is_empty() {
                        self.conditions
                            .push(format!("({})", search_conditions.join(" OR ")));
                        self.arguments.add(pattern).unwrap_or_default();
                    }
                }
            }
        }
        self
    }

    /// Adds equality filters to the query based on provided key-value pairs.
    ///
    /// # Arguments
    ///
    /// * `params` - Query parameters containing filters as key-value pairs
    ///
    /// # Details
    ///
    /// - Only applies filters for columns that exist and are considered safe
    /// - Automatically handles type casting based on the database dialect
    /// - Skips invalid columns with a warning when tracing is enabled
    /// - Null or empty values are ignored
    ///
    /// # Returns
    ///
    /// Returns self for method chaining
    ///
    /// # Example
    ///
    /// ```rust
    /// use sqlx::Postgres;
    /// use serde::{Serialize};
    /// use sqlx_paginated::{QueryBuilder, QueryParamsBuilder};
    ///
    /// #[derive(Serialize, Default)]
    /// struct UserExample {
    ///     name: String
    /// }
    ///
    /// let initial_params = QueryParamsBuilder::<UserExample>::new()
    ///         .with_search("john", vec!["name", "email"])
    ///         .build();
    ///
    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
    ///     .with_filters(&initial_params)
    ///     .build();
    /// ```
    pub fn with_filters(mut self, params: &'q QueryParams<T>) -> Self {
        for (key, value) in &params.filters {
            if self.is_column_safe(key) {
                if let Some(value) = value {
                    let table_column = self.dialect.quote_identifier(key);
                    let type_cast = self.dialect.type_cast(value);
                    let next_argument = self.arguments.len() + 1;
                    let placeholder = self.dialect.placeholder(next_argument);

                    self.conditions
                        .push(format!("{} = {}{}", table_column, placeholder, type_cast));
                    self.arguments.add(value).unwrap_or_default();
                }
            } else {
                #[cfg(feature = "tracing")]
                tracing::warn!(column = %key, "Skipping invalid filter column");
            }
        }
        self
    }

    /// Adds date range conditions to the query for a specified date column.
    ///
    /// # Arguments
    ///
    /// * `params` - Query parameters containing date range information
    ///
    /// # Type Parameters
    ///
    /// Requires `DateTime<Utc>` to be encodable for the target database
    ///
    /// # Details
    ///
    /// - Adds >= condition for date_after if specified
    /// - Adds <= condition for date_before if specified
    /// - Only applies to columns that exist and are considered safe
    /// - Skips invalid date columns with a warning when tracing is enabled
    ///
    /// # Returns
    ///
    /// Returns self for method chaining
    ///
    /// # Example
    ///
    /// ```rust
    /// use sqlx::Postgres;
    /// use serde::{Serialize};
    /// use chrono::{DateTime};
    /// use sqlx_paginated::{QueryBuilder, QueryParamsBuilder, QueryParams};
    ///
    /// #[derive(Serialize, Default)]
    /// struct UserExample {
    ///     name: String
    /// }
    ///
    /// let initial_params = QueryParamsBuilder::<UserExample>::new()
    ///         .with_date_range(None, Some(DateTime::parse_from_rfc3339("2024-12-31T23:59:59Z").unwrap().into()), Some("deleted_at"))
    ///         .build();
    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
    ///     .with_date_range(&initial_params)
    ///     .build();
    /// ```
    pub fn with_date_range(mut self, params: &'q QueryParams<T>) -> Self
    where
        DateTime<Utc>: for<'a> Encode<'a, DB> + Type<DB>,
    {
        if let Some(date_column) = &params.date_range.date_column {
            if self.is_column_safe(date_column) {
                if let Some(after) = params.date_range.date_after {
                    let next_argument = self.arguments.len() + 1;
                    self.conditions
                        .push(format!("{} >= ${}", date_column, next_argument));
                    self.arguments.add(after).unwrap_or_default();
                }

                if let Some(before) = params.date_range.date_before {
                    let next_argument = self.arguments.len() + 1;
                    self.conditions
                        .push(format!("{} <= ${}", date_column, next_argument));
                    self.arguments.add(before).unwrap_or_default();
                }
            } else {
                #[cfg(feature = "tracing")]
                tracing::warn!(column = %date_column, "Skipping invalid date column");
            }
        }

        self
    }

    /// Adds a custom condition for a specific column with a provided operator and value.
    ///
    /// # Arguments
    ///
    /// * `column` - The column name to apply the condition to
    /// * `condition` - The operator or condition to use (e.g., ">", "LIKE", etc.)
    /// * `value` - The value to compare against
    ///
    /// # Details
    ///
    /// - Only applies to columns that exist and are considered safe
    /// - Automatically handles parameter binding
    /// - Skips invalid columns with a warning when tracing is enabled
    ///
    /// # Returns
    ///
    /// Returns self for method chaining
    ///
    /// # Example
    ///
    /// ```rust
    /// use sqlx::Postgres;
    /// use serde::{Serialize};
    /// use sqlx_paginated::{QueryBuilder};
    ///
    /// #[derive(Serialize, Default)]
    /// struct UserExample {
    ///     name: String
    /// }
    ///
    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
    ///     .with_condition("age", ">", "18".to_string())
    ///     .build();
    /// ```
    pub fn with_condition(
        mut self,
        column: &str,
        condition: impl Into<String>,
        value: String,
    ) -> Self {
        if self.is_column_safe(column) {
            let next_argument = self.arguments.len() + 1;
            self.conditions.push(format!(
                "{} {} ${}",
                quote_identifier(column),
                condition.into(),
                next_argument
            ));
            let _ = self.arguments.add(value);
        } else {
            #[cfg(feature = "tracing")]
            tracing::warn!(column = %column, "Skipping invalid condition column");
        }
        self
    }

    /// Adds a raw SQL condition to the query without any safety checks.
    ///
    /// # Arguments
    ///
    /// * `condition` - Raw SQL condition to add to the query
    ///
    /// # Safety
    ///
    /// This method bypasses column safety checks. Use with caution to prevent SQL injection.
    ///
    /// # Returns
    ///
    /// Returns self for method chaining
    ///
    /// # Example
    ///
    /// ```rust
    /// use sqlx::Postgres;
    /// use serde::{Serialize};
    /// use sqlx_paginated::{QueryBuilder};
    ///
    /// #[derive(Serialize, Default)]
    /// struct UserExample {
    ///     name: String
    /// }
    ///
    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
    ///     .with_raw_condition("status != 'deleted'")
    ///     .build();
    /// ```
    pub fn with_raw_condition(mut self, condition: impl Into<String>) -> Self {
        self.conditions.push(condition.into());
        self
    }

    /// Allows adding multiple conditions using a closure.
    ///
    /// # Arguments
    ///
    /// * `f` - Closure that takes a mutable reference to the QueryBuilder
    ///
    /// # Details
    ///
    /// Useful for grouping multiple conditions that are logically related
    ///
    /// # Returns
    ///
    /// Returns self for method chaining
    ///
    /// # Example
    ///
    /// ```rust
    /// use sqlx::Postgres;
    /// use serde::{Serialize};
    /// use sqlx_paginated::{QueryBuilder};
    ///
    /// #[derive(Serialize, Default)]
    /// struct UserExample {
    ///     name: String
    /// }
    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
    ///     .with_combined_conditions(|builder| {
    ///         builder.conditions.push("status = 'active'".to_string());
    ///         builder.conditions.push("age >= 18".to_string());
    ///     })
    ///     .build();
    /// ```
    pub fn with_combined_conditions<F>(mut self, f: F) -> Self
    where
        F: FnOnce(&mut QueryBuilder<T, DB>),
    {
        f(&mut self);
        self
    }

    /// Disables column protection for this query builder instance.
    ///
    /// # Safety
    ///
    /// This removes all column safety checks. Use with caution as it may expose
    /// the application to SQL injection if used with untrusted input.
    ///
    /// # Returns
    ///
    /// Returns self for method chaining
    ///
    /// # Example
    ///
    /// ```rust
    /// use sqlx::Postgres;
    /// use serde::{Serialize};
    /// use sqlx_paginated::{QueryBuilder};
    ///
    /// #[derive(Serialize, Default)]
    /// struct UserExample {
    ///     name: String
    /// }
    ///
    /// let query_builder = QueryBuilder::<UserExample, Postgres>::new()
    ///     .disable_protection()
    ///     .with_raw_condition("custom_column = 'value'")
    ///     .build();
    /// ```
    pub fn disable_protection(mut self) -> Self {
        self.protection_enabled = false;
        self
    }

    /// Builds the final query conditions and arguments.
    ///
    /// # Returns
    ///
    /// Returns a tuple containing:
    /// - Vec<String>: List of SQL conditions
    /// - DB::Arguments: Database-specific arguments for parameter binding
    ///
    /// # Example
    ///
    /// ```rust
    /// use sqlx::Postgres;
    /// use serde::{Serialize};
    /// use sqlx_paginated::{QueryBuilder, QueryParamsBuilder};
    ///
    /// #[derive(Serialize, Default)]
    /// struct UserExample {
    ///     name: String
    /// }
    ///
    /// let initial_params = QueryParamsBuilder::<UserExample>::new()
    ///         .with_search("john", vec!["name", "email"])
    ///         .build();
    /// let (conditions, arguments) = QueryBuilder::<UserExample, Postgres>::new()
    ///     .with_search(&initial_params)
    ///     .build();
    /// ```
    pub fn build(self) -> (Vec<String>, DB::Arguments<'q>) {
        (self.conditions, self.arguments)
    }
}