use std::fmt;
#[derive(Debug, Clone)]
pub struct OrderByClause {
pub field: String,
pub field_source: FieldSource,
pub direction: SortOrder,
pub collation: Option<String>,
pub nulls_handling: Option<NullsHandling>,
}
impl OrderByClause {
pub fn jsonb_field(field: impl Into<String>, direction: SortOrder) -> Self {
Self {
field: field.into(),
field_source: FieldSource::JsonbPayload,
direction,
collation: None,
nulls_handling: None,
}
}
pub fn direct_column(field: impl Into<String>, direction: SortOrder) -> Self {
Self {
field: field.into(),
field_source: FieldSource::DirectColumn,
direction,
collation: None,
nulls_handling: None,
}
}
pub fn with_collation(mut self, collation: impl Into<String>) -> Self {
self.collation = Some(collation.into());
self
}
#[must_use]
pub const fn with_nulls(mut self, handling: NullsHandling) -> Self {
self.nulls_handling = Some(handling);
self
}
pub fn validate(&self) -> Result<(), String> {
if self.field.is_empty() {
return Err("Field name cannot be empty".to_string());
}
if !self.field.chars().all(|c| c.is_alphanumeric() || c == '_')
|| self
.field
.chars()
.next()
.is_some_and(|c| !c.is_alphabetic() && c != '_')
{
return Err(format!("Invalid field name: {}", self.field));
}
if let Some(ref collation) = self.collation {
if collation.is_empty() {
return Err("Collation name cannot be empty".to_string());
}
if !collation
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '@')
{
return Err(format!("Invalid collation name: {}", collation));
}
}
Ok(())
}
pub fn to_sql(&self) -> Result<String, String> {
self.validate()?;
let field_expr = match self.field_source {
FieldSource::JsonbPayload => format!("(data->'{}')", self.field),
FieldSource::DirectColumn => self.field.clone(),
};
let mut sql = field_expr;
if let Some(ref collation) = self.collation {
sql.push_str(&format!(" COLLATE \"{}\"", collation));
}
let direction = match self.direction {
SortOrder::Asc => "ASC",
SortOrder::Desc => "DESC",
};
sql.push(' ');
sql.push_str(direction);
if let Some(nulls) = self.nulls_handling {
let nulls_str = match nulls {
NullsHandling::First => "NULLS FIRST",
NullsHandling::Last => "NULLS LAST",
};
sql.push(' ');
sql.push_str(nulls_str);
}
Ok(sql)
}
}
impl fmt::Display for OrderByClause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.to_sql() {
Ok(sql) => write!(f, "{}", sql),
Err(e) => write!(f, "ERROR: {}", e),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FieldSource {
JsonbPayload,
DirectColumn,
}
impl fmt::Display for FieldSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FieldSource::JsonbPayload => write!(f, "JSONB"),
FieldSource::DirectColumn => write!(f, "DIRECT_COLUMN"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SortOrder {
Asc,
Desc,
}
impl fmt::Display for SortOrder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SortOrder::Asc => write!(f, "ASC"),
SortOrder::Desc => write!(f, "DESC"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NullsHandling {
First,
Last,
}
impl fmt::Display for NullsHandling {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NullsHandling::First => write!(f, "NULLS FIRST"),
NullsHandling::Last => write!(f, "NULLS LAST"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Collation {
C,
Utf8,
Custom(String),
}
impl Collation {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Collation::C => "C",
Collation::Utf8 => "C.UTF-8",
Collation::Custom(name) => name,
}
}
}
impl fmt::Display for Collation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[cfg(test)]
mod tests;