Highly ambitious ATProtocol AppView service and sdks
at main 11 kB view raw
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}