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
//! QueryFilter: complete query specification with conditions, sorting, search, and pagination
use super::types::{FilterLogical, SortDirection, SortSpec};
use super::condition::FilterCondition;
/// Complete query filter specification
#[derive(Debug, Clone, Default)]
pub struct QueryFilter {
pub conditions: Vec<FilterCondition>,
pub sorts: Vec<SortSpec>,
pub search: Option<String>,
pub search_fields: Vec<String>,
pub limit: Option<u32>,
pub offset: Option<u32>,
pub page: Option<u32>,
/// Keyset paging: the opaque cursor from a previous page's
/// `next_cursor` (forward walk). Takes precedence over page/offset.
pub cursor_after: Option<String>,
/// Keyset paging: the opaque cursor from a previous page's
/// `prev_cursor` (backward walk — the query inverts and the page is
/// reversed so it still reads forward).
pub cursor_before: Option<String>,
/// Replace the exact COUNT (a full scan of the filtered set) with the
/// planner's row estimate. Screens that need the exact figure use the
/// separate count endpoint.
pub estimate_total: bool,
/// Base conditions that are always applied (e.g., soft delete exclusion)
/// These are raw SQL conditions that will be ANDed with other conditions
pub base_conditions: Vec<String>,
}
impl QueryFilter {
/// Create a new empty query filter
pub fn new() -> Self {
Self::default()
}
/// Add a filter condition
pub fn add_condition(&mut self, condition: FilterCondition) {
self.conditions.push(condition);
}
/// Add a sort specification
pub fn add_sort(&mut self, sort: SortSpec) {
self.sorts.push(sort);
}
/// Set search term
pub fn with_search(&mut self, search: String, fields: Vec<String>) {
self.search = Some(search);
self.search_fields = fields;
}
/// Set pagination
pub fn with_pagination(&mut self, page: u32, limit: u32) {
self.page = Some(page);
self.limit = Some(limit);
}
/// Add a base condition (raw SQL that is always ANDed with other conditions)
/// Used for soft delete exclusion, tenant filtering, etc.
pub fn add_base_condition(&mut self, condition: String) {
self.base_conditions.push(condition);
}
/// Build the complete WHERE clause
pub fn build_where_clause(&self) -> (String, Vec<String>) {
let mut parts = Vec::new();
let mut params = Vec::new();
let mut param_idx = 1usize;
// Add base conditions first (e.g., soft delete exclusion)
// These are raw SQL that don't require parameter binding
for base_condition in &self.base_conditions {
parts.push(base_condition.clone());
}
// Add search condition (if specified)
if let Some(search_term) = &self.search {
if !self.search_fields.is_empty() && !search_term.is_empty() {
let search_pattern = format!("%{}%", search_term);
let search_conditions: Vec<String> = self.search_fields.iter()
.map(|field| {
let condition = format!("{} ILIKE ${}", field, param_idx);
param_idx += 1;
params.push(search_pattern.clone());
condition
})
.collect();
if !search_conditions.is_empty() {
if parts.is_empty() {
parts.push(format!("({})", search_conditions.join(" OR ")));
} else {
parts.push(format!(" AND ({})", search_conditions.join(" OR ")));
}
}
}
}
// Add all filter conditions
for condition in &self.conditions {
let condition_sql = condition.build_sql_without_prefix(&mut param_idx);
params.extend(condition.get_params());
// Prepend logical operator if not the first condition
let logical_prefix = match condition.logical {
FilterLogical::And => " AND ",
FilterLogical::Or => " OR ",
};
if !parts.is_empty() {
parts.push(format!("{}{}", logical_prefix, condition_sql));
} else {
parts.push(condition_sql);
}
}
let where_clause = if parts.is_empty() {
String::new()
} else {
format!(" WHERE {}", parts.join(""))
};
(where_clause, params)
}
/// Build ORDER BY clause
pub fn build_order_by_clause(&self) -> String {
if self.sorts.is_empty() {
String::new()
} else {
let order_parts: Vec<String> = self.sorts.iter()
.map(|s| format!("{} {}", s.field, if s.direction == SortDirection::Desc { "DESC" } else { "ASC" }))
.collect();
format!(" ORDER BY {}", order_parts.join(", "))
}
}
/// True when some condition compares a value that carries no cast — see
/// [`FilterCondition::needs_value_cast`]. The repository answers it by reading the table's
/// column types from the catalog before it builds the query.
pub fn has_uncast_value_conditions(&self) -> bool {
self.conditions.iter().any(FilterCondition::needs_value_cast)
}
/// Check if has any conditions
pub fn has_conditions(&self) -> bool {
!self.conditions.is_empty() || self.search.is_some()
}
}