1use std::fmt;
7
8#[derive(Debug, Clone)]
38pub struct OrderByClause {
39 pub field: String,
41
42 pub field_source: FieldSource,
44
45 pub direction: SortOrder,
47
48 pub collation: Option<String>,
52
53 pub nulls_handling: Option<NullsHandling>,
55}
56
57impl OrderByClause {
58 pub fn jsonb_field(field: impl Into<String>, direction: SortOrder) -> Self {
60 Self {
61 field: field.into(),
62 field_source: FieldSource::JsonbPayload,
63 direction,
64 collation: None,
65 nulls_handling: None,
66 }
67 }
68
69 pub fn direct_column(field: impl Into<String>, direction: SortOrder) -> Self {
71 Self {
72 field: field.into(),
73 field_source: FieldSource::DirectColumn,
74 direction,
75 collation: None,
76 nulls_handling: None,
77 }
78 }
79
80 pub fn with_collation(mut self, collation: impl Into<String>) -> Self {
82 self.collation = Some(collation.into());
83 self
84 }
85
86 pub fn with_nulls(mut self, handling: NullsHandling) -> Self {
88 self.nulls_handling = Some(handling);
89 self
90 }
91
92 pub fn validate(&self) -> Result<(), String> {
94 if self.field.is_empty() {
95 return Err("Field name cannot be empty".to_string());
96 }
97
98 if !self.field.chars().all(|c| c.is_alphanumeric() || c == '_')
100 || self
101 .field
102 .chars()
103 .next()
104 .map(|c| !c.is_alphabetic() && c != '_')
105 .unwrap_or(false)
106 {
107 return Err(format!("Invalid field name: {}", self.field));
108 }
109
110 if let Some(ref collation) = self.collation {
112 if collation.is_empty() {
113 return Err("Collation name cannot be empty".to_string());
114 }
115 if !collation
120 .chars()
121 .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '@')
122 {
123 return Err(format!("Invalid collation name: {}", collation));
124 }
125 }
126
127 Ok(())
128 }
129
130 pub fn to_sql(&self) -> Result<String, String> {
138 self.validate()?;
139
140 let field_expr = match self.field_source {
141 FieldSource::JsonbPayload => format!("(data->'{}')", self.field),
142 FieldSource::DirectColumn => self.field.clone(),
143 };
144
145 let mut sql = field_expr;
146
147 if let Some(ref collation) = self.collation {
149 sql.push_str(&format!(" COLLATE \"{}\"", collation));
150 }
151
152 let direction = match self.direction {
154 SortOrder::Asc => "ASC",
155 SortOrder::Desc => "DESC",
156 };
157 sql.push(' ');
158 sql.push_str(direction);
159
160 if let Some(nulls) = self.nulls_handling {
162 let nulls_str = match nulls {
163 NullsHandling::First => "NULLS FIRST",
164 NullsHandling::Last => "NULLS LAST",
165 };
166 sql.push(' ');
167 sql.push_str(nulls_str);
168 }
169
170 Ok(sql)
171 }
172}
173
174impl fmt::Display for OrderByClause {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 match self.to_sql() {
177 Ok(sql) => write!(f, "{}", sql),
178 Err(e) => write!(f, "ERROR: {}", e),
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum FieldSource {
186 JsonbPayload,
188
189 DirectColumn,
191}
192
193impl fmt::Display for FieldSource {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 match self {
196 FieldSource::JsonbPayload => write!(f, "JSONB"),
197 FieldSource::DirectColumn => write!(f, "DIRECT_COLUMN"),
198 }
199 }
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum SortOrder {
205 Asc,
207
208 Desc,
210}
211
212impl fmt::Display for SortOrder {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 match self {
215 SortOrder::Asc => write!(f, "ASC"),
216 SortOrder::Desc => write!(f, "DESC"),
217 }
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum NullsHandling {
224 First,
226
227 Last,
229}
230
231impl fmt::Display for NullsHandling {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 match self {
234 NullsHandling::First => write!(f, "NULLS FIRST"),
235 NullsHandling::Last => write!(f, "NULLS LAST"),
236 }
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
245pub enum Collation {
246 C,
248
249 Utf8,
251
252 Custom(String),
254}
255
256impl Collation {
257 pub fn as_str(&self) -> &str {
259 match self {
260 Collation::C => "C",
261 Collation::Utf8 => "C.UTF-8",
262 Collation::Custom(name) => name,
263 }
264 }
265}
266
267impl fmt::Display for Collation {
268 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269 write!(f, "{}", self.as_str())
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_jsonb_field_ordering() {
279 let clause = OrderByClause::jsonb_field("name", SortOrder::Asc);
280 let sql = clause.to_sql().unwrap();
281 assert_eq!(sql, "(data->'name') ASC");
282 }
283
284 #[test]
285 fn test_direct_column_ordering() {
286 let clause = OrderByClause::direct_column("created_at", SortOrder::Desc);
287 let sql = clause.to_sql().unwrap();
288 assert_eq!(sql, "created_at DESC");
289 }
290
291 #[test]
292 fn test_ordering_with_collation() {
293 let clause = OrderByClause::jsonb_field("name", SortOrder::Asc).with_collation("en-US");
294 let sql = clause.to_sql().unwrap();
295 assert_eq!(sql, "(data->'name') COLLATE \"en-US\" ASC");
296 }
297
298 #[test]
299 fn test_ordering_with_nulls_last() {
300 let clause =
301 OrderByClause::direct_column("status", SortOrder::Asc).with_nulls(NullsHandling::Last);
302 let sql = clause.to_sql().unwrap();
303 assert_eq!(sql, "status ASC NULLS LAST");
304 }
305
306 #[test]
307 fn test_ordering_with_collation_and_nulls() {
308 let clause = OrderByClause::jsonb_field("email", SortOrder::Desc)
309 .with_collation("C")
310 .with_nulls(NullsHandling::First);
311 let sql = clause.to_sql().unwrap();
312 assert_eq!(sql, "(data->'email') COLLATE \"C\" DESC NULLS FIRST");
313 }
314
315 #[test]
316 fn test_field_validation() {
317 assert!(OrderByClause::jsonb_field("valid_name", SortOrder::Asc)
318 .validate()
319 .is_ok());
320 assert!(OrderByClause::jsonb_field("123invalid", SortOrder::Asc)
321 .validate()
322 .is_err());
323 assert!(OrderByClause::jsonb_field("bad-name", SortOrder::Asc)
324 .validate()
325 .is_err());
326 }
327
328 #[test]
329 fn test_collation_validation() {
330 let clause = OrderByClause::jsonb_field("name", SortOrder::Asc).with_collation("en-US");
331 assert!(clause.validate().is_ok());
332
333 let clause = OrderByClause::jsonb_field("name", SortOrder::Asc).with_collation("C.UTF-8");
334 assert!(clause.validate().is_ok());
335
336 let clause =
337 OrderByClause::jsonb_field("name", SortOrder::Asc).with_collation("invalid!!!special");
338 assert!(clause.validate().is_err());
339 }
340
341 #[test]
342 fn test_sort_order_display() {
343 assert_eq!(SortOrder::Asc.to_string(), "ASC");
344 assert_eq!(SortOrder::Desc.to_string(), "DESC");
345 }
346
347 #[test]
348 fn test_field_source_display() {
349 assert_eq!(FieldSource::JsonbPayload.to_string(), "JSONB");
350 assert_eq!(FieldSource::DirectColumn.to_string(), "DIRECT_COLUMN");
351 }
352
353 #[test]
354 fn test_collation_enum() {
355 assert_eq!(Collation::C.as_str(), "C");
356 assert_eq!(Collation::Utf8.as_str(), "C.UTF-8");
357 assert_eq!(Collation::Custom("de-DE".to_string()).as_str(), "de-DE");
358 }
359}