Skip to main content

keelson_core/clause/
order_by.rs

1use std::borrow::Cow;
2
3use crate::expr::{Expr, IntoExpr};
4use crate::writer::{Expression, SqlWriter};
5
6/// `ORDER BY a, b DESC`
7#[derive(Debug, Clone, Default)]
8pub struct OrderBy {
9    /// The sort keys, in precedence order. Usually [`OrderDef`]s, but a bare
10    /// expression is a sort key too.
11    pub expressions: Vec<Expr>,
12}
13
14impl OrderBy {
15    /// Append one sort key.
16    pub fn append_order(&mut self, order: impl IntoExpr) {
17        self.expressions.push(order.into_expr());
18    }
19
20    /// Drop every sort key. Needed because a mod may have to *replace* an
21    /// inherited ordering rather than add to it.
22    pub fn clear_order_by(&mut self) {
23        self.expressions.clear();
24    }
25
26    /// Whether the clause is absent.
27    pub fn is_empty(&self) -> bool {
28        self.expressions.is_empty()
29    }
30}
31
32impl Expression for OrderBy {
33    fn write_sql(&self, w: &mut SqlWriter<'_>) {
34        w.write_slice(&self.expressions, "ORDER BY ", ", ", "");
35    }
36}
37
38/// Anything with an `ORDER BY`: a statement, a [`Window`](super::Window)
39/// definition, an aggregate's `ORDER BY` inside its own parentheses, or the
40/// trailing ordering of a set operation ([`Combines`](super::Combines)).
41pub trait HasOrderBy {
42    /// The `ORDER BY` clause to modify.
43    fn order_by_mut(&mut self) -> &mut OrderBy;
44}
45
46impl HasOrderBy for OrderBy {
47    fn order_by_mut(&mut self) -> &mut OrderBy {
48        self
49    }
50}
51
52/// One sort key: `expr [COLLATE c] [ASC | DESC | USING op] [NULLS FIRST | LAST]`.
53///
54/// From PostgreSQL 17:
55///
56/// ```text
57/// ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ]
58/// ```
59///
60/// `COLLATE` is formally part of the expression rather than of the sort key, but it
61/// has to be written between the expression and the direction, so it lives here.
62#[derive(Debug, Clone, Default)]
63pub struct OrderDef {
64    /// What to sort by.
65    pub expression: Option<Expr>,
66    /// A collation name, quoted on output.
67    pub collation: Option<Cow<'static, str>>,
68    /// Ascending, descending, or by an operator.
69    pub direction: Option<OrderDirection>,
70    /// Where nulls sort. Defaults, in every dialect, to whichever end the
71    /// direction puts last.
72    pub nulls: Option<NullsPosition>,
73}
74
75impl OrderDef {
76    /// A sort key with no modifiers.
77    pub fn new(expression: impl IntoExpr) -> Self {
78        OrderDef {
79            expression: Some(expression.into_expr()),
80            ..OrderDef::default()
81        }
82    }
83
84    /// Whether there is nothing to sort by.
85    pub fn is_empty(&self) -> bool {
86        self.expression.is_none()
87    }
88}
89
90impl Expression for OrderDef {
91    fn write_sql(&self, w: &mut SqlWriter<'_>) {
92        let Some(expression) = &self.expression else {
93            // A direction with nothing to apply it to is not a fragment.
94            return;
95        };
96        w.write_expr(expression);
97
98        if let Some(collation) = &self.collation {
99            w.push_str(" COLLATE ");
100            w.push_quoted(&[collation]);
101        }
102
103        match &self.direction {
104            None => {}
105            Some(OrderDirection::Asc) => w.push_str(" ASC"),
106            Some(OrderDirection::Desc) => w.push_str(" DESC"),
107            Some(OrderDirection::Using(op)) => {
108                w.push_str(" USING ");
109                w.push_str(op);
110            }
111        }
112
113        if let Some(nulls) = &self.nulls {
114            w.push_str(" NULLS ");
115            w.push_str(nulls.as_str());
116        }
117    }
118}
119
120/// Which way a sort key sorts.
121///
122/// `Using` is why this is not a two-variant enum: PostgreSQL lets a sort key name
123/// a `<` or `>` operator directly, which no closed set could hold.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum OrderDirection {
126    /// `ASC`.
127    Asc,
128    /// `DESC`.
129    Desc,
130    /// PostgreSQL's `USING <operator>`, written verbatim after the keyword.
131    Using(Cow<'static, str>),
132}
133
134/// Where nulls sort relative to everything else.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum NullsPosition {
137    /// `NULLS FIRST`.
138    First,
139    /// `NULLS LAST`.
140    Last,
141}
142
143impl NullsPosition {
144    /// The keyword, as written.
145    pub fn as_str(self) -> &'static str {
146        match self {
147            NullsPosition::First => "FIRST",
148            NullsPosition::Last => "LAST",
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use keelson_sqlcheck::testing::assert_frag_sql;
156
157    use super::*;
158    use crate::dialect::testing::Numbered;
159    use crate::expr::{arg, quote};
160    use crate::value::Value;
161    use crate::writer::build;
162
163    /// A single sort key goes after the keyword; a whole `OrderBy` brings its own.
164    const KEY_FRAME: &str = r#"SELECT "id" FROM users ORDER BY {}"#;
165    const CLAUSE_FRAME: &str = r#"SELECT "id", "name", "age" FROM users {}"#;
166
167    fn sql(e: &impl Expression) -> String {
168        build(&Numbered, e).expect("render").0
169    }
170
171    #[test]
172    fn an_empty_order_by_writes_nothing() {
173        assert_frag_sql(CLAUSE_FRAME, &sql(&OrderBy::default()), "");
174        assert!(OrderBy::default().is_empty());
175    }
176
177    #[test]
178    fn an_order_def_with_no_expression_writes_nothing() {
179        let o = OrderDef {
180            direction: Some(OrderDirection::Desc),
181            ..OrderDef::default()
182        };
183        // Not framed: a sort key that writes nothing leaves `ORDER BY` dangling,
184        // so there is no statement for it to be judged inside. That it renders
185        // nothing at all — not even the direction it was given — is the assertion.
186        assert_eq!(build(&Numbered, &o).unwrap().0, "");
187        assert!(o.is_empty());
188    }
189
190    #[test]
191    fn a_bare_order_def_is_just_its_expression() {
192        assert_frag_sql(KEY_FRAME, &sql(&OrderDef::new(quote("name"))), r#""name""#);
193    }
194
195    #[test]
196    fn collation_precedes_the_direction_and_nulls_comes_last() {
197        // PostgreSQL 17 sql-select: the sort key is
198        //   expression [ COLLATE collation ] [ ASC | DESC ] [ NULLS … ]
199        // and the collation is quoted because `bg-BG-x-icu` is not an identifier.
200        let o = OrderDef {
201            collation: Some("bg-BG-x-icu".into()),
202            direction: Some(OrderDirection::Asc),
203            nulls: Some(NullsPosition::Last),
204            ..OrderDef::new(quote("name"))
205        };
206        assert_frag_sql(
207            KEY_FRAME,
208            &sql(&o),
209            r#""name" COLLATE "bg-BG-x-icu" ASC NULLS LAST"#,
210        );
211    }
212
213    #[test]
214    fn a_direction_can_be_an_operator() {
215        // `USING >` needs an operator that is a btree ordering operator for the
216        // key's type, which `>` is for text.
217        let o = OrderDef {
218            direction: Some(OrderDirection::Using(">".into())),
219            nulls: Some(NullsPosition::First),
220            ..OrderDef::new(quote("name"))
221        };
222        assert_frag_sql(KEY_FRAME, &sql(&o), r#""name" USING > NULLS FIRST"#);
223    }
224
225    #[test]
226    fn keys_are_comma_separated_and_can_be_cleared() {
227        let mut ob = OrderBy::default();
228        ob.append_order(Expr::custom(OrderDef::new(quote("name"))));
229        ob.append_order(Expr::custom(OrderDef {
230            direction: Some(OrderDirection::Desc),
231            ..OrderDef::new(quote("age"))
232        }));
233        // A sort key may also be an ordinal or any expression; 3 is the frame's
234        // third output column.
235        ob.append_order("3");
236
237        assert_frag_sql(CLAUSE_FRAME, &sql(&ob), r#"ORDER BY "name", "age" DESC, 3"#);
238
239        ob.clear_order_by();
240        assert_frag_sql(CLAUSE_FRAME, &sql(&ob), "");
241    }
242
243    #[test]
244    fn a_sort_key_may_bind_an_argument() {
245        let mut ob = OrderBy::default();
246        ob.append_order(Expr::custom(OrderDef::new(Expr::func(
247            "coalesce",
248            (quote("age"), arg(0i32)),
249        ))));
250        let (rendered, args) = build(&Numbered, &ob).unwrap();
251        assert_frag_sql(CLAUSE_FRAME, &rendered, r#"ORDER BY coalesce("age", $1)"#);
252        assert_eq!(args, vec![Value::I32(0)]);
253    }
254}