1use std::fmt;
7
8#[derive(Debug, Clone)]
40pub struct OrderByClause {
41 pub field: String,
43
44 pub field_source: FieldSource,
46
47 pub direction: SortOrder,
49
50 pub collation: Option<String>,
54
55 pub nulls_handling: Option<NullsHandling>,
57}
58
59impl OrderByClause {
60 pub fn jsonb_field(field: impl Into<String>, direction: SortOrder) -> Self {
62 Self {
63 field: field.into(),
64 field_source: FieldSource::JsonbPayload,
65 direction,
66 collation: None,
67 nulls_handling: None,
68 }
69 }
70
71 pub fn direct_column(field: impl Into<String>, direction: SortOrder) -> Self {
73 Self {
74 field: field.into(),
75 field_source: FieldSource::DirectColumn,
76 direction,
77 collation: None,
78 nulls_handling: None,
79 }
80 }
81
82 pub fn with_collation(mut self, collation: impl Into<String>) -> Self {
84 self.collation = Some(collation.into());
85 self
86 }
87
88 pub const fn with_nulls(mut self, handling: NullsHandling) -> Self {
90 self.nulls_handling = Some(handling);
91 self
92 }
93
94 pub fn validate(&self) -> Result<(), String> {
101 if self.field.is_empty() {
102 return Err("Field name cannot be empty".to_string());
103 }
104
105 if !self.field.chars().all(|c| c.is_alphanumeric() || c == '_')
107 || self
108 .field
109 .chars()
110 .next()
111 .is_some_and(|c| !c.is_alphabetic() && c != '_')
112 {
113 return Err(format!("Invalid field name: {}", self.field));
114 }
115
116 if let Some(ref collation) = self.collation {
118 if collation.is_empty() {
119 return Err("Collation name cannot be empty".to_string());
120 }
121 if !collation
126 .chars()
127 .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '@')
128 {
129 return Err(format!("Invalid collation name: {}", collation));
130 }
131 }
132
133 Ok(())
134 }
135
136 pub fn to_sql(&self) -> Result<String, String> {
148 self.validate()?;
149
150 let field_expr = match self.field_source {
151 FieldSource::JsonbPayload => format!("(data->'{}')", self.field),
152 FieldSource::DirectColumn => self.field.clone(),
153 };
154
155 let mut sql = field_expr;
156
157 if let Some(ref collation) = self.collation {
159 sql.push_str(&format!(" COLLATE \"{}\"", collation));
160 }
161
162 let direction = match self.direction {
164 SortOrder::Asc => "ASC",
165 SortOrder::Desc => "DESC",
166 };
167 sql.push(' ');
168 sql.push_str(direction);
169
170 if let Some(nulls) = self.nulls_handling {
172 let nulls_str = match nulls {
173 NullsHandling::First => "NULLS FIRST",
174 NullsHandling::Last => "NULLS LAST",
175 };
176 sql.push(' ');
177 sql.push_str(nulls_str);
178 }
179
180 Ok(sql)
181 }
182}
183
184impl fmt::Display for OrderByClause {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 match self.to_sql() {
187 Ok(sql) => write!(f, "{}", sql),
188 Err(e) => write!(f, "ERROR: {}", e),
189 }
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum FieldSource {
196 JsonbPayload,
198
199 DirectColumn,
201}
202
203impl fmt::Display for FieldSource {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 match self {
206 FieldSource::JsonbPayload => write!(f, "JSONB"),
207 FieldSource::DirectColumn => write!(f, "DIRECT_COLUMN"),
208 }
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum SortOrder {
215 Asc,
217
218 Desc,
220}
221
222impl fmt::Display for SortOrder {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 match self {
225 SortOrder::Asc => write!(f, "ASC"),
226 SortOrder::Desc => write!(f, "DESC"),
227 }
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum NullsHandling {
234 First,
236
237 Last,
239}
240
241impl fmt::Display for NullsHandling {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 match self {
244 NullsHandling::First => write!(f, "NULLS FIRST"),
245 NullsHandling::Last => write!(f, "NULLS LAST"),
246 }
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Eq)]
255pub enum Collation {
256 C,
258
259 Utf8,
261
262 Custom(String),
264}
265
266impl Collation {
267 pub fn as_str(&self) -> &str {
269 match self {
270 Collation::C => "C",
271 Collation::Utf8 => "C.UTF-8",
272 Collation::Custom(name) => name,
273 }
274 }
275}
276
277impl fmt::Display for Collation {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 write!(f, "{}", self.as_str())
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 #![allow(clippy::unwrap_used)] use super::*;
287
288 #[test]
289 fn test_jsonb_field_ordering() {
290 let clause = OrderByClause::jsonb_field("name", SortOrder::Asc);
291 let sql = clause.to_sql().unwrap();
292 assert_eq!(sql, "(data->'name') ASC");
293 }
294
295 #[test]
296 fn test_direct_column_ordering() {
297 let clause = OrderByClause::direct_column("created_at", SortOrder::Desc);
298 let sql = clause.to_sql().unwrap();
299 assert_eq!(sql, "created_at DESC");
300 }
301
302 #[test]
303 fn test_ordering_with_collation() {
304 let clause = OrderByClause::jsonb_field("name", SortOrder::Asc).with_collation("en-US");
305 let sql = clause.to_sql().unwrap();
306 assert_eq!(sql, "(data->'name') COLLATE \"en-US\" ASC");
307 }
308
309 #[test]
310 fn test_ordering_with_nulls_last() {
311 let clause =
312 OrderByClause::direct_column("status", SortOrder::Asc).with_nulls(NullsHandling::Last);
313 let sql = clause.to_sql().unwrap();
314 assert_eq!(sql, "status ASC NULLS LAST");
315 }
316
317 #[test]
318 fn test_ordering_with_collation_and_nulls() {
319 let clause = OrderByClause::jsonb_field("email", SortOrder::Desc)
320 .with_collation("C")
321 .with_nulls(NullsHandling::First);
322 let sql = clause.to_sql().unwrap();
323 assert_eq!(sql, "(data->'email') COLLATE \"C\" DESC NULLS FIRST");
324 }
325
326 #[test]
327 fn test_field_validation() {
328 assert!(OrderByClause::jsonb_field("valid_name", SortOrder::Asc)
329 .validate()
330 .is_ok());
331 assert!(OrderByClause::jsonb_field("123invalid", SortOrder::Asc)
332 .validate()
333 .is_err());
334 assert!(OrderByClause::jsonb_field("bad-name", SortOrder::Asc)
335 .validate()
336 .is_err());
337 }
338
339 #[test]
340 fn test_collation_validation() {
341 let clause = OrderByClause::jsonb_field("name", SortOrder::Asc).with_collation("en-US");
342 assert!(clause.validate().is_ok());
343
344 let clause = OrderByClause::jsonb_field("name", SortOrder::Asc).with_collation("C.UTF-8");
345 assert!(clause.validate().is_ok());
346
347 let clause =
348 OrderByClause::jsonb_field("name", SortOrder::Asc).with_collation("invalid!!!special");
349 assert!(clause.validate().is_err());
350 }
351
352 #[test]
353 fn test_sort_order_display() {
354 assert_eq!(SortOrder::Asc.to_string(), "ASC");
355 assert_eq!(SortOrder::Desc.to_string(), "DESC");
356 }
357
358 #[test]
359 fn test_field_source_display() {
360 assert_eq!(FieldSource::JsonbPayload.to_string(), "JSONB");
361 assert_eq!(FieldSource::DirectColumn.to_string(), "DIRECT_COLUMN");
362 }
363
364 #[test]
365 fn test_collation_enum() {
366 assert_eq!(Collation::C.as_str(), "C");
367 assert_eq!(Collation::Utf8.as_str(), "C.UTF-8");
368 assert_eq!(Collation::Custom("de-DE".to_string()).as_str(), "de-DE");
369 }
370}