use std::borrow::Cow;
use crate::expr::{Expr, IntoExpr};
use crate::writer::{Expression, SqlWriter};
#[derive(Debug, Clone, Default)]
pub struct OrderBy {
pub expressions: Vec<Expr>,
}
impl OrderBy {
pub fn append_order(&mut self, order: impl IntoExpr) {
self.expressions.push(order.into_expr());
}
pub fn clear_order_by(&mut self) {
self.expressions.clear();
}
pub fn is_empty(&self) -> bool {
self.expressions.is_empty()
}
}
impl Expression for OrderBy {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.write_slice(&self.expressions, "ORDER BY ", ", ", "");
}
}
pub trait HasOrderBy {
fn order_by_mut(&mut self) -> &mut OrderBy;
}
impl HasOrderBy for OrderBy {
fn order_by_mut(&mut self) -> &mut OrderBy {
self
}
}
#[derive(Debug, Clone, Default)]
pub struct OrderDef {
pub expression: Option<Expr>,
pub collation: Option<Cow<'static, str>>,
pub direction: Option<OrderDirection>,
pub nulls: Option<NullsPosition>,
}
impl OrderDef {
pub fn new(expression: impl IntoExpr) -> Self {
OrderDef {
expression: Some(expression.into_expr()),
..OrderDef::default()
}
}
pub fn is_empty(&self) -> bool {
self.expression.is_none()
}
}
impl Expression for OrderDef {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
let Some(expression) = &self.expression else {
return;
};
w.write_expr(expression);
if let Some(collation) = &self.collation {
w.push_str(" COLLATE ");
w.push_quoted(&[collation]);
}
match &self.direction {
None => {}
Some(OrderDirection::Asc) => w.push_str(" ASC"),
Some(OrderDirection::Desc) => w.push_str(" DESC"),
Some(OrderDirection::Using(op)) => {
w.push_str(" USING ");
w.push_str(op);
}
}
if let Some(nulls) = &self.nulls {
w.push_str(" NULLS ");
w.push_str(nulls.as_str());
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OrderDirection {
Asc,
Desc,
Using(Cow<'static, str>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NullsPosition {
First,
Last,
}
impl NullsPosition {
pub fn as_str(self) -> &'static str {
match self {
NullsPosition::First => "FIRST",
NullsPosition::Last => "LAST",
}
}
}
#[cfg(test)]
mod tests {
use keelson_sqlcheck::testing::assert_frag_sql;
use super::*;
use crate::dialect::testing::Numbered;
use crate::expr::{arg, quote};
use crate::value::Value;
use crate::writer::build;
const KEY_FRAME: &str = r#"SELECT "id" FROM users ORDER BY {}"#;
const CLAUSE_FRAME: &str = r#"SELECT "id", "name", "age" FROM users {}"#;
fn sql(e: &impl Expression) -> String {
build(&Numbered, e).expect("render").0
}
#[test]
fn an_empty_order_by_writes_nothing() {
assert_frag_sql(CLAUSE_FRAME, &sql(&OrderBy::default()), "");
assert!(OrderBy::default().is_empty());
}
#[test]
fn an_order_def_with_no_expression_writes_nothing() {
let o = OrderDef {
direction: Some(OrderDirection::Desc),
..OrderDef::default()
};
assert_eq!(build(&Numbered, &o).unwrap().0, "");
assert!(o.is_empty());
}
#[test]
fn a_bare_order_def_is_just_its_expression() {
assert_frag_sql(KEY_FRAME, &sql(&OrderDef::new(quote("name"))), r#""name""#);
}
#[test]
fn collation_precedes_the_direction_and_nulls_comes_last() {
let o = OrderDef {
collation: Some("bg-BG-x-icu".into()),
direction: Some(OrderDirection::Asc),
nulls: Some(NullsPosition::Last),
..OrderDef::new(quote("name"))
};
assert_frag_sql(
KEY_FRAME,
&sql(&o),
r#""name" COLLATE "bg-BG-x-icu" ASC NULLS LAST"#,
);
}
#[test]
fn a_direction_can_be_an_operator() {
let o = OrderDef {
direction: Some(OrderDirection::Using(">".into())),
nulls: Some(NullsPosition::First),
..OrderDef::new(quote("name"))
};
assert_frag_sql(KEY_FRAME, &sql(&o), r#""name" USING > NULLS FIRST"#);
}
#[test]
fn keys_are_comma_separated_and_can_be_cleared() {
let mut ob = OrderBy::default();
ob.append_order(Expr::custom(OrderDef::new(quote("name"))));
ob.append_order(Expr::custom(OrderDef {
direction: Some(OrderDirection::Desc),
..OrderDef::new(quote("age"))
}));
ob.append_order("3");
assert_frag_sql(CLAUSE_FRAME, &sql(&ob), r#"ORDER BY "name", "age" DESC, 3"#);
ob.clear_order_by();
assert_frag_sql(CLAUSE_FRAME, &sql(&ob), "");
}
#[test]
fn a_sort_key_may_bind_an_argument() {
let mut ob = OrderBy::default();
ob.append_order(Expr::custom(OrderDef::new(Expr::func(
"coalesce",
(quote("age"), arg(0i32)),
))));
let (rendered, args) = build(&Numbered, &ob).unwrap();
assert_frag_sql(CLAUSE_FRAME, &rendered, r#"ORDER BY coalesce("age", $1)"#);
assert_eq!(args, vec![Value::I32(0)]);
}
}