forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1//! SQL query building utilities for dynamic WHERE and ORDER BY clauses.
2//!
3//! This module provides helpers for constructing SQL queries dynamically
4//! based on user input while preventing SQL injection attacks.
5
6use super::cursor::build_field_reference;
7use super::types::{SortField, WhereClause, WhereCondition};
8use crate::models::Record;
9
10
11/// Builds an ORDER BY clause with optional datetime field information.
12///
13/// # Arguments
14/// * `sort_by` - Optional array of fields to sort by
15/// * `field_is_datetime` - Optional boolean indicating if primary field is datetime
16///
17/// # Returns
18/// SQL ORDER BY clause string (without "ORDER BY" prefix)
19pub fn build_order_by_clause_with_field_info(
20 sort_by: Option<&Vec<SortField>>,
21 field_is_datetime: Option<bool>,
22) -> String {
23 match sort_by {
24 Some(sort_fields) if !sort_fields.is_empty() => {
25 let mut order_clauses = Vec::new();
26 for (index, sort_field) in sort_fields.iter().enumerate() {
27 let field = &sort_field.field;
28 let direction = match sort_field.direction.to_lowercase().as_str() {
29 "desc" => "DESC",
30 _ => "ASC",
31 };
32
33 let is_primary = index == 0;
34 let is_datetime = is_primary && field_is_datetime == Some(true);
35
36 if field
37 .chars()
38 .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
39 {
40 let field_ref = build_field_reference(field, is_datetime);
41
42 if matches!(field.as_str(), "indexed_at" | "uri" | "cid" | "did" | "collection") {
43 order_clauses.push(format!("{field_ref} {direction}"));
44 } else {
45 order_clauses.push(format!("{field_ref} {direction} NULLS LAST"));
46 }
47 }
48 }
49 if !order_clauses.is_empty() {
50 let has_indexed_at = order_clauses
51 .iter()
52 .any(|clause| clause.contains("indexed_at"));
53 if !has_indexed_at {
54 order_clauses.push("indexed_at DESC".to_string());
55 }
56 order_clauses.join(", ")
57 } else {
58 "indexed_at DESC".to_string()
59 }
60 }
61 _ => "indexed_at DESC".to_string(),
62 }
63}
64
65/// Builds WHERE conditions from a WhereClause structure.
66///
67/// Returns separate arrays for AND conditions and OR conditions
68/// to be combined in the final query.
69///
70/// # Arguments
71/// * `where_clause` - Optional where clause with AND/OR conditions
72/// * `param_count` - Mutable counter for parameter numbering ($1, $2, etc)
73///
74/// # Returns
75/// Tuple of (and_conditions, or_conditions) as SQL strings
76pub fn build_where_conditions(
77 where_clause: Option<&WhereClause>,
78 param_count: &mut usize,
79) -> (Vec<String>, Vec<String>) {
80 let mut where_clauses = Vec::new();
81 let mut or_clauses = Vec::new();
82
83 if let Some(clause) = where_clause {
84 for (field, condition) in &clause.conditions {
85 let field_clause = build_single_condition(field, condition, param_count);
86 where_clauses.push(field_clause);
87 }
88
89 if let Some(or_conditions) = &clause.or_conditions {
90 for (field, condition) in or_conditions {
91 let field_clause = build_single_condition(field, condition, param_count);
92 or_clauses.push(field_clause);
93 }
94 }
95 }
96
97 (where_clauses, or_clauses)
98}
99
100/// Builds a single SQL condition clause for a field.
101///
102/// Supports equality (eq), array membership (in_values), and pattern matching (contains)
103/// for both table columns and JSON fields with nested paths.
104///
105/// # Arguments
106/// * `field` - Field name (table column or JSON path)
107/// * `condition` - The condition to apply (eq, in_values, or contains)
108/// * `param_count` - Mutable counter for parameter numbering
109///
110/// # Returns
111/// SQL condition string with parameter placeholder
112pub fn build_single_condition(
113 field: &str,
114 condition: &WhereCondition,
115 param_count: &mut usize,
116) -> String {
117 if let Some(_eq_value) = &condition.eq {
118 let clause = match field {
119 "did" | "collection" | "uri" | "cid" => {
120 format!("{} = ${}", field, param_count)
121 }
122 _ => {
123 let json_path = build_json_path(field);
124 format!("{} = ${}", json_path, param_count)
125 }
126 };
127 *param_count += 1;
128 clause
129 } else if let Some(_in_values) = &condition.in_values {
130 let clause = match field {
131 "did" | "collection" | "uri" | "cid" => {
132 format!("{} = ANY(${})", field, param_count)
133 }
134 _ => {
135 let json_path = build_json_path(field);
136 format!("{} = ANY(${})", json_path, param_count)
137 }
138 };
139 *param_count += 1;
140 clause
141 } else if let Some(_contains_value) = &condition.contains {
142 let clause = if field == "json" {
143 format!("json::text ILIKE '%' || ${} || '%'", param_count)
144 } else {
145 let json_path = build_json_path(field);
146 format!("{} ILIKE '%' || ${} || '%'", json_path, param_count)
147 };
148 *param_count += 1;
149 clause
150 } else {
151 String::new()
152 }
153}
154
155/// Builds a PostgreSQL JSON path accessor string.
156///
157/// Converts dot notation (e.g., "user.name") into PostgreSQL JSON operators
158/// (e.g., "json->'user'->>'name'").
159fn build_json_path(field: &str) -> String {
160 if field.contains('.') {
161 let parts: Vec<&str> = field.split('.').collect();
162 let mut path = String::from("json");
163 for (i, part) in parts.iter().enumerate() {
164 if i == parts.len() - 1 {
165 path.push_str(&format!("->>'{}'", part));
166 } else {
167 path.push_str(&format!("->'{}'", part));
168 }
169 }
170 path
171 } else {
172 format!("json->>'{}'", field)
173 }
174}
175
176/// Binds WHERE clause parameters to a sqlx query.
177///
178/// Iterates through all conditions and binds their values in the correct order.
179///
180/// # Arguments
181/// * `query_builder` - The sqlx query to bind parameters to
182/// * `where_clause` - Optional where clause with parameter values
183///
184/// # Returns
185/// Query builder with all parameters bound
186pub fn bind_where_parameters<'q>(
187 mut query_builder: sqlx::query::QueryAs<
188 'q,
189 sqlx::Postgres,
190 Record,
191 sqlx::postgres::PgArguments,
192 >,
193 where_clause: Option<&'q WhereClause>,
194) -> sqlx::query::QueryAs<'q, sqlx::Postgres, Record, sqlx::postgres::PgArguments> {
195 if let Some(clause) = where_clause {
196 for condition in clause.conditions.values() {
197 query_builder = bind_single_condition(query_builder, condition);
198 }
199
200 if let Some(or_conditions) = &clause.or_conditions {
201 for condition in or_conditions.values() {
202 query_builder = bind_single_condition(query_builder, condition);
203 }
204 }
205 }
206 query_builder
207}
208
209/// Binds parameters for a single condition to a sqlx query.
210///
211/// Handles eq (single value), in_values (array), and contains (pattern) conditions.
212fn bind_single_condition<'q>(
213 mut query_builder: sqlx::query::QueryAs<
214 'q,
215 sqlx::Postgres,
216 Record,
217 sqlx::postgres::PgArguments,
218 >,
219 condition: &'q WhereCondition,
220) -> sqlx::query::QueryAs<'q, sqlx::Postgres, Record, sqlx::postgres::PgArguments> {
221 if let Some(eq_value) = &condition.eq {
222 if let Some(str_val) = eq_value.as_str() {
223 query_builder = query_builder.bind(str_val);
224 } else {
225 query_builder = query_builder.bind(eq_value);
226 }
227 }
228
229 if let Some(in_values) = &condition.in_values {
230 let str_values: Vec<String> = in_values
231 .iter()
232 .filter_map(|v| v.as_str().map(|s| s.to_string()))
233 .collect();
234 query_builder = query_builder.bind(str_values);
235 }
236
237 if let Some(contains_value) = &condition.contains {
238 query_builder = query_builder.bind(contains_value);
239 }
240
241 query_builder
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_build_order_by_clause_default() {
250 let result = build_order_by_clause_with_field_info(None, None);
251 assert_eq!(result, "indexed_at DESC");
252 }
253
254 #[test]
255 fn test_build_order_by_clause_single_field() {
256 let sort_by = vec![SortField {
257 field: "createdAt".to_string(),
258 direction: "desc".to_string(),
259 }];
260
261 let result = build_order_by_clause_with_field_info(Some(&sort_by), None);
262 assert_eq!(result, "json->>'createdAt' DESC NULLS LAST, indexed_at DESC");
263 }
264
265 #[test]
266 fn test_build_order_by_clause_datetime_field() {
267 let sort_by = vec![SortField {
268 field: "createdAt".to_string(),
269 direction: "desc".to_string(),
270 }];
271
272 let result = build_order_by_clause_with_field_info(Some(&sort_by), Some(true));
273 assert_eq!(result, "(json->>'createdAt')::timestamp DESC NULLS LAST, indexed_at DESC");
274 }
275
276 #[test]
277 fn test_build_order_by_clause_table_column() {
278 let sort_by = vec![SortField {
279 field: "indexed_at".to_string(),
280 direction: "asc".to_string(),
281 }];
282
283 let result = build_order_by_clause_with_field_info(Some(&sort_by), None);
284 assert_eq!(result, "indexed_at ASC");
285 }
286
287 #[test]
288 fn test_build_order_by_clause_nested_json() {
289 let sort_by = vec![SortField {
290 field: "author.name".to_string(),
291 direction: "asc".to_string(),
292 }];
293
294 let result = build_order_by_clause_with_field_info(Some(&sort_by), None);
295 assert_eq!(result, "json->'author'->>'name' ASC NULLS LAST, indexed_at DESC");
296 }
297
298 #[test]
299 fn test_build_order_by_clause_multiple_fields() {
300 let sort_by = vec![
301 SortField {
302 field: "text".to_string(),
303 direction: "desc".to_string(),
304 },
305 SortField {
306 field: "createdAt".to_string(),
307 direction: "asc".to_string(),
308 },
309 ];
310
311 let result = build_order_by_clause_with_field_info(Some(&sort_by), None);
312 assert_eq!(
313 result,
314 "json->>'text' DESC NULLS LAST, json->>'createdAt' ASC NULLS LAST, indexed_at DESC"
315 );
316 }
317}