From 86bc4434e7ec3ce590dd49baf3d718f05e8968e1 Mon Sep 17 00:00:00 2001 From: xelab04 Date: Tue, 24 Feb 2026 22:04:00 +0400 Subject: [PATCH 1/5] add basic code for ordering --- cot/src/db/query.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/cot/src/db/query.rs b/cot/src/db/query.rs index 139a7ca76..7e69307e3 100644 --- a/cot/src/db/query.rs +++ b/cot/src/db/query.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use derive_more::with_trait::Debug; -use sea_query::{ExprTrait, IntoColumnRef}; +use sea_query::{ExprTrait, IntoColumnRef, Order}; use crate::db; use crate::db::{ @@ -33,6 +33,7 @@ use crate::db::{ pub struct Query { filter: Option, limit: Option, + order_by: Option<(String, Order)>, offset: Option, phantom_data: PhantomData T>, } @@ -55,6 +56,7 @@ impl Clone for Query { Self { filter: self.filter.clone(), limit: self.limit, + order_by: self.order_by.clone(), offset: self.offset, phantom_data: PhantomData, } @@ -98,6 +100,7 @@ impl Query { Self { filter: None, limit: None, + order_by: None, offset: None, phantom_data: PhantomData, } @@ -149,6 +152,28 @@ impl Query { self } + /// Set an order for records from the query. + /// + /// # Example + /// + /// ``` + /// use cot::db::model; + /// use cot::db::query::{Expr, Query}; + /// + /// #[model] + /// struct User { + /// #[model(primary_key)] + /// id: i32, + /// age: i32, + /// } + /// + /// let query = Query::::new().order_by("age", Order::Asc); // or Order::Desc + /// ``` + pub fn order_by(&mut self, order_by: (String, Order)) -> &mut Self { + self.order_by = Some(self.order_by); + self + } + /// Set the offset for the query. /// /// # Example @@ -244,6 +269,12 @@ impl Query { } } + pub(super) fn add_order_by_to_statement(&self, statement: &mut sea_query::SelectStatement) { + if let Some(order_by) = self.order_by { + statement.order_by(order_by.0, order_by.1); + } + } + pub(super) fn add_offset_to_statement(&self, statement: &mut sea_query::SelectStatement) { if let Some(offset) = self.offset { statement.offset(offset); From 8fb90e8cca17e5b32eea4377d71277a4ee916934 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 11 Mar 2026 13:12:43 +0400 Subject: [PATCH 2/5] change ordering to use fieldref instead of string --- cot/src/db/query.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cot/src/db/query.rs b/cot/src/db/query.rs index 7e69307e3..6a40f8437 100644 --- a/cot/src/db/query.rs +++ b/cot/src/db/query.rs @@ -33,7 +33,7 @@ use crate::db::{ pub struct Query { filter: Option, limit: Option, - order_by: Option<(String, Order)>, + order_by: Option<(FieldRef, Order)>, offset: Option, phantom_data: PhantomData T>, } @@ -167,10 +167,10 @@ impl Query { /// age: i32, /// } /// - /// let query = Query::::new().order_by("age", Order::Asc); // or Order::Desc + /// let query = Query::::new().order_by(User::age, Order::Asc); // or Order::Desc /// ``` - pub fn order_by(&mut self, order_by: (String, Order)) -> &mut Self { - self.order_by = Some(self.order_by); + pub fn order_by(&mut self, order_by: (FieldRef, Order)) -> &mut Self { + self.order_by = Some(order_by); self } @@ -271,7 +271,8 @@ impl Query { pub(super) fn add_order_by_to_statement(&self, statement: &mut sea_query::SelectStatement) { if let Some(order_by) = self.order_by { - statement.order_by(order_by.0, order_by.1); + let column_name = order_by.0.identifier; + statement.order_by(column_name, order_by.1); } } From 62dbf4efb6d2ef93b932c7e622b6e2e5d77ccc0c Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 26 Aug 2026 00:08:41 +0000 Subject: [PATCH 3/5] get this in a much better shape. Still lacking other expressiveness --- cot/src/db.rs | 1 + cot/src/db/query.rs | 16 ++--- cot/src/db/query/expr.rs | 6 ++ cot/src/db/query/expr/order_by.rs | 115 ++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 cot/src/db/query/expr/order_by.rs diff --git a/cot/src/db.rs b/cot/src/db.rs index f4b1004a1..3414800c6 100644 --- a/cot/src/db.rs +++ b/cot/src/db.rs @@ -2572,6 +2572,7 @@ impl Database { let mut select = sea_query::Query::select(); select.columns(columns_to_get).from(T::TABLE_NAME); query.add_filter_to_statement(&mut select, executor.as_sql_query_builder())?; + query.add_order_by_to_statement(&mut select); query.add_limit_to_statement(&mut select); query.add_offset_to_statement(&mut select); diff --git a/cot/src/db/query.rs b/cot/src/db/query.rs index ca4609e90..8cdce8655 100644 --- a/cot/src/db/query.rs +++ b/cot/src/db/query.rs @@ -5,12 +5,11 @@ pub mod expr; use std::marker::PhantomData; use derive_more::with_trait::Debug; -use sea_query::{ExprTrait, IntoColumnRef, Order}; use thiserror::Error; use crate::db; -use crate::db::query::expr::SqlQueryBuilder; pub use crate::db::query::expr::{Expr, ExprAdd, ExprDiv, ExprMul, ExprOrd, ExprSub}; +use crate::db::query::expr::{OrderByExpr, SqlQueryBuilder}; use crate::db::{Auto, DatabaseBackend, ForeignKey, Model, StatementResult, ToDbFieldValue}; const ERROR_PREFIX: &str = "expression error:"; @@ -48,7 +47,7 @@ pub enum QueryBuildingError { pub struct Query { filter: Option, limit: Option, - order_by: Option<(FieldRef, Order)>, + order_by: Vec, offset: Option, phantom_data: PhantomData T>, } @@ -115,7 +114,7 @@ impl Query { Self { filter: None, limit: None, - order_by: None, + order_by: Vec::new(), offset: None, phantom_data: PhantomData, } @@ -184,8 +183,8 @@ impl Query { /// /// let query = Query::::new().order_by(User::age, Order::Asc); // or Order::Desc /// ``` - pub fn order_by(&mut self, order_by: (FieldRef, Order)) -> &mut Self { - self.order_by = Some(order_by); + pub fn order_by(&mut self, order_by: impl IntoIterator) -> &mut Self { + self.order_by = order_by.into_iter().collect(); self } @@ -276,9 +275,8 @@ impl Query { } pub(super) fn add_order_by_to_statement(&self, statement: &mut sea_query::SelectStatement) { - if let Some(order_by) = self.order_by { - let column_name = order_by.0.identifier; - statement.order_by(column_name, order_by.1); + for order_by in &self.order_by { + order_by.add_to_statement(statement); } } diff --git a/cot/src/db/query/expr.rs b/cot/src/db/query/expr.rs index 8946ccd95..ef8805bc6 100644 --- a/cot/src/db/query/expr.rs +++ b/cot/src/db/query/expr.rs @@ -1,5 +1,6 @@ //! Database expressions. pub mod like; +mod order_by; use std::marker::PhantomData; @@ -7,6 +8,7 @@ use cot::db::query::{IntoField, QueryBuildingError}; use cot::db::{DbFieldValue, DbValue, FromDbValue, Identifier, ToDbFieldValue}; pub use like::ExprLike; use like::{CaseSensitivity, LikeExprBuilder, LikeMode}; +pub use order_by::{ExprSort, NullsOrder, OrderByExpr, SortOrder}; use sea_query::{ExprTrait, IntoColumnRef, SimpleExpr}; /// An expression that can be used to filter, update, or delete rows. @@ -1316,6 +1318,10 @@ impl FieldRef { pub fn as_expr(&self) -> Expr { Expr::Field(self.identifier) } + + pub(crate) fn identifier(&self) -> Identifier { + self.identifier + } } /// A trait for types that can be compared in database expressions. diff --git a/cot/src/db/query/expr/order_by.rs b/cot/src/db/query/expr/order_by.rs new file mode 100644 index 000000000..59e1341ed --- /dev/null +++ b/cot/src/db/query/expr/order_by.rs @@ -0,0 +1,115 @@ +use crate::db::Identifier; +use crate::db::query::expr::FieldRef; + +/// Ordering Options +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SortOrder { + /// Sort in Ascending order. + Asc, + /// Sort in Descending Order. + Desc, +} + +impl From for sea_query::Order { + fn from(value: SortOrder) -> Self { + match value { + SortOrder::Asc => sea_query::Order::Asc, + SortOrder::Desc => sea_query::Order::Desc, + } + } +} + +/// The order to sort null values +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NullsOrder { + /// Null values will appear first + First, + /// Null values will appear last + Last, +} + +impl From for sea_query::NullOrdering { + fn from(value: NullsOrder) -> Self { + match value { + NullsOrder::First => sea_query::NullOrdering::First, + NullsOrder::Last => sea_query::NullOrdering::Last, + } + } +} + +/// An `ORDER BY` term. +/// +/// # Example +/// +/// ``` +/// use cot::db::model; +/// use cot::db::query::{ExprSort, Query}; +/// +/// #[model] +/// struct User { +/// #[model(primary_key)] +/// id: i32, +/// name: String, +/// } +/// +/// let mut query = Query::::new(); +/// query.order_by([ +/// ::Fields::id.asc(), +/// ::Fields::name.desc().nulls_first(), +/// ]); +/// ``` +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct OrderByExpr { + field: Identifier, + order: SortOrder, + nulls: Option, +} + +impl OrderByExpr { + pub(crate) fn new(field: Identifier, order: SortOrder) -> Self { + Self { + field, + order, + nulls: None, + } + } + + #[must_use] + pub fn nulls_first(mut self) -> Self { + self.nulls = Some(NullsOrder::First); + self + } + + #[must_use] + pub fn nulls_last(mut self) -> Self { + self.nulls = Some(NullsOrder::Last); + self + } + + pub(crate) fn add_to_statement(&self, statement: &mut sea_query::SelectStatement) { + match self.nulls { + None => { + statement.order_by(self.field, self.order.into()); + } + Some(nulls) => { + statement.order_by_with_nulls(self.field, self.order.into(), nulls.into()); + } + }; + } +} + +pub trait ExprSort { + fn asc(&self) -> OrderByExpr; + fn desc(&self) -> OrderByExpr; +} + +impl ExprSort for FieldRef { + fn asc(&self) -> OrderByExpr { + OrderByExpr::new(self.identifier(), SortOrder::Asc) + } + + fn desc(&self) -> OrderByExpr { + OrderByExpr::new(self.identifier(), SortOrder::Desc) + } +} From c34fef00c7ffb2666bafe78667b1aa41cd6b2405 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 26 Aug 2026 02:20:39 +0000 Subject: [PATCH 4/5] order_by field. Need a better name --- cot/src/db.rs | 3 ++ cot/src/db/fields.rs | 1 + cot/src/db/query/expr/order_by.rs | 66 ++++++++++++++++++++++++------- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/cot/src/db.rs b/cot/src/db.rs index 3414800c6..b4fdbe814 100644 --- a/cot/src/db.rs +++ b/cot/src/db.rs @@ -1019,6 +1019,9 @@ pub trait FromDbValue { /// An alias for the value type internally used by the [`sea_query`] crate. pub type DbValue = sea_query::Value; +/// An alias for the values type internally used the [`sea_query`] crate. +pub type DbValues = sea_query::Values; + /// A trait for converting a Rust value to a database value. pub trait ToDbValue: Send + Sync { /// Converts the Rust value to a `sea_query` value. diff --git a/cot/src/db/fields.rs b/cot/src/db/fields.rs index e420143b6..bbbde19ac 100644 --- a/cot/src/db/fields.rs +++ b/cot/src/db/fields.rs @@ -249,6 +249,7 @@ impl_db_field!(Vec, Blob); impl_db_field!(Bytes, Blob, with Vec); impl TextField for String {} +impl TextField for &str {} impl ToDbValue for &str { fn to_db_value(&self) -> DbValue { diff --git a/cot/src/db/query/expr/order_by.rs b/cot/src/db/query/expr/order_by.rs index 59e1341ed..3f7573c70 100644 --- a/cot/src/db/query/expr/order_by.rs +++ b/cot/src/db/query/expr/order_by.rs @@ -1,20 +1,26 @@ -use crate::db::Identifier; +use cot::db::{DbFieldValue, ToDbFieldValue}; +use sea_query::Values; + use crate::db::query::expr::FieldRef; +use crate::db::{DbValues, Identifier, ToDbValue}; /// Ordering Options -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq)] pub enum SortOrder { /// Sort in Ascending order. Asc, /// Sort in Descending Order. Desc, + + Custom(DbValues), } -impl From for sea_query::Order { - fn from(value: SortOrder) -> Self { +impl From<&SortOrder> for sea_query::Order { + fn from(value: &SortOrder) -> Self { match value { SortOrder::Asc => sea_query::Order::Asc, SortOrder::Desc => sea_query::Order::Desc, + SortOrder::Custom(v) => sea_query::Order::Field(v.clone()), } } } @@ -77,34 +83,49 @@ impl OrderByExpr { #[must_use] pub fn nulls_first(mut self) -> Self { - self.nulls = Some(NullsOrder::First); + self.set_nulls(NullsOrder::First); self } #[must_use] pub fn nulls_last(mut self) -> Self { - self.nulls = Some(NullsOrder::Last); + self.set_nulls(NullsOrder::Last); self } + #[track_caller] + fn set_nulls(&mut self, nulls: NullsOrder) { + match &mut self.order { + SortOrder::Asc | SortOrder::Desc => self.nulls = Some(nulls), + SortOrder::Custom(_) => panic!( + "`nulls_first`/`nulls_last` can't be combined with `custom_order`: a custom-order term never produces \ + a NULL sort key, so an explicit NULLS placement would have no effect" + ), + } + } + pub(crate) fn add_to_statement(&self, statement: &mut sea_query::SelectStatement) { - match self.nulls { - None => { - statement.order_by(self.field, self.order.into()); - } - Some(nulls) => { - statement.order_by_with_nulls(self.field, self.order.into(), nulls.into()); - } + let order: sea_query::Order = (&self.order).into(); + if let Some(nulls) = self.nulls { + let nulls: sea_query::NullOrdering = nulls.into(); + statement.order_by_with_nulls(self.field, order, nulls); + } else { + statement.order_by(self.field, order); }; } } -pub trait ExprSort { +pub trait ExprSort { fn asc(&self) -> OrderByExpr; fn desc(&self) -> OrderByExpr; + + fn custom(&self, values: I) -> OrderByExpr + where + I: IntoIterator, + I::Item: ToDbValue; } -impl ExprSort for FieldRef { +impl ExprSort for FieldRef { fn asc(&self) -> OrderByExpr { OrderByExpr::new(self.identifier(), SortOrder::Asc) } @@ -112,4 +133,19 @@ impl ExprSort for FieldRef { fn desc(&self) -> OrderByExpr { OrderByExpr::new(self.identifier(), SortOrder::Desc) } + + fn custom(&self, values: I) -> OrderByExpr + where + I: IntoIterator, + I::Item: ToDbValue, + { + let values = values + .into_iter() + .map(|v| match v.to_db_field_value() { + DbFieldValue::Value(value) => value, + DbFieldValue::Auto => panic!("Cannot order by a non-value field"), + }) + .collect::>(); + OrderByExpr::new(self.identifier(), SortOrder::Custom(Values(values))) + } } From c671e624d8268adaafd8cd309cee863824f11766 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 28 Aug 2026 03:46:03 +0000 Subject: [PATCH 5/5] much much better API. no tests yet. Also add proper support for custom order. API name needs to change though --- cot/src/db.rs | 2 +- cot/src/db/query.rs | 18 +++- cot/src/db/query/expr.rs | 63 ++++++++++- cot/src/db/query/expr/order_by.rs | 173 ++++++++++++++++++++++++------ 4 files changed, 215 insertions(+), 41 deletions(-) diff --git a/cot/src/db.rs b/cot/src/db.rs index b4fdbe814..be40cbf59 100644 --- a/cot/src/db.rs +++ b/cot/src/db.rs @@ -2575,7 +2575,7 @@ impl Database { let mut select = sea_query::Query::select(); select.columns(columns_to_get).from(T::TABLE_NAME); query.add_filter_to_statement(&mut select, executor.as_sql_query_builder())?; - query.add_order_by_to_statement(&mut select); + query.add_order_by_to_statement(&mut select, executor.as_sql_query_builder())?; query.add_limit_to_statement(&mut select); query.add_offset_to_statement(&mut select); diff --git a/cot/src/db/query.rs b/cot/src/db/query.rs index 8cdce8655..268d072c6 100644 --- a/cot/src/db/query.rs +++ b/cot/src/db/query.rs @@ -57,6 +57,7 @@ impl Debug for Query { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Query") .field("filter", &self.filter) + .field("order_by", &self.order_by) .field("limit", &self.limit) .field("offset", &self.offset) .field("phantom_data", &self.phantom_data) @@ -183,8 +184,12 @@ impl Query { /// /// let query = Query::::new().order_by(User::age, Order::Asc); // or Order::Desc /// ``` - pub fn order_by(&mut self, order_by: impl IntoIterator) -> &mut Self { - self.order_by = order_by.into_iter().collect(); + pub fn order_by(&mut self, order_by: I) -> &mut Self + where + O: Into, + I: IntoIterator, + { + self.order_by = order_by.into_iter().map(Into::into).collect(); self } @@ -274,10 +279,15 @@ impl Query { } } - pub(super) fn add_order_by_to_statement(&self, statement: &mut sea_query::SelectStatement) { + pub(super) fn add_order_by_to_statement( + &self, + statement: &mut sea_query::SelectStatement, + sql_builder: &dyn SqlQueryBuilder, + ) -> Result<(), QueryBuildingError> { for order_by in &self.order_by { - order_by.add_to_statement(statement); + order_by.add_to_statement(statement, sql_builder)?; } + Ok(()) } pub(super) fn add_offset_to_statement(&self, statement: &mut sea_query::SelectStatement) { diff --git a/cot/src/db/query/expr.rs b/cot/src/db/query/expr.rs index ef8805bc6..5d585649b 100644 --- a/cot/src/db/query/expr.rs +++ b/cot/src/db/query/expr.rs @@ -3,14 +3,18 @@ pub mod like; mod order_by; use std::marker::PhantomData; +use std::ops::Add; use cot::db::query::{IntoField, QueryBuildingError}; -use cot::db::{DbFieldValue, DbValue, FromDbValue, Identifier, ToDbFieldValue}; +use cot::db::{DbFieldValue, DbValue, FromDbValue, Identifier, LimitedString, ToDbFieldValue}; pub use like::ExprLike; use like::{CaseSensitivity, LikeExprBuilder, LikeMode}; pub use order_by::{ExprSort, NullsOrder, OrderByExpr, SortOrder}; use sea_query::{ExprTrait, IntoColumnRef, SimpleExpr}; +use crate::db::ToDbValue; +use crate::db::query::expr::order_by::OrderTarget; + /// An expression that can be used to filter, update, or delete rows. /// /// This is used to create complex queries with multiple conditions. Typically, @@ -1199,6 +1203,38 @@ impl Expr { Self::RawLike(Box::new(lhs), Box::new(rhs), CaseSensitivity::Insensitive) } + /// Builds an ascending `ORDER BY` term from this expression. See the + /// note on [`Query::filter`](crate::db::query::Query::filter) about + /// `Expr` not being restricted to field references — the same applies + /// here; ordering by a boolean-producing expression is legal SQL but + /// rarely what you want. + #[must_use] + pub fn asc(self) -> OrderByExpr { + OrderByExpr::directional(OrderTarget::Expression(self), SortOrder::Asc) + } + + /// The descending counterpart of [`Self::asc`]. + #[must_use] + pub fn desc(self) -> OrderByExpr { + OrderByExpr::directional(OrderTarget::Expression(self), SortOrder::Desc) + } + + /// The [`ExprSort::custom`]-equivalent for a compound expression. + /// + /// Takes plain [`ToDbValue`] items rather than [`IntoField`]: unlike + /// [`FieldRef`], a general `Expr` isn't associated with one Rust + /// field type to convert against, so there's no `T` for `IntoField` + /// to key off of. + #[must_use] + pub fn custom(self, values: I) -> OrderByExpr + where + I: IntoIterator, + I::Item: ToDbValue, + { + let values = values.into_iter().map(|v| v.to_db_value()).collect(); + OrderByExpr::custom(OrderTarget::Expression(self), sea_query::Values(values)) + } + /// Returns the expression as a [`sea_query::SimpleExpr`]. /// /// # Example @@ -1324,6 +1360,17 @@ impl FieldRef { } } +impl Add> for FieldRef +where + FieldRef: ExprAdd>, +{ + type Output = Expr; + + fn add(self, rhs: FieldRef) -> Self::Output { + ExprAdd::add(self, rhs) + } +} + /// A trait for types that can be compared in database expressions. pub trait ExprEq { /// Creates an expression that checks if the field is equal to the given @@ -1651,6 +1698,20 @@ impl_num_expr!(u64); impl_num_expr!(f32); impl_num_expr!(f64); +impl ExprAdd for FieldRef { + fn add>(self, other: V) -> Expr { + Expr::add(Expr::field(self.identifier()), Expr::value(other.into())) + } +} + +impl ExprAdd>> for FieldRef> { + fn add>>>(self, other: V) -> Expr { + Expr::add( + Expr::field(self.identifier()), + Expr::field(other.into().identifier()), + ) + } +} #[cfg(test)] mod test { use super::*; diff --git a/cot/src/db/query/expr/order_by.rs b/cot/src/db/query/expr/order_by.rs index 3f7573c70..19562ab50 100644 --- a/cot/src/db/query/expr/order_by.rs +++ b/cot/src/db/query/expr/order_by.rs @@ -1,18 +1,16 @@ use cot::db::{DbFieldValue, ToDbFieldValue}; -use sea_query::Values; -use crate::db::query::expr::FieldRef; -use crate::db::{DbValues, Identifier, ToDbValue}; +use crate::db::Identifier; +use crate::db::query::expr::{FieldRef, SqlQueryBuilder}; +use crate::db::query::{Expr, IntoField, QueryBuildingError}; /// Ordering Options -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum SortOrder { /// Sort in Ascending order. Asc, /// Sort in Descending Order. Desc, - - Custom(DbValues), } impl From<&SortOrder> for sea_query::Order { @@ -20,7 +18,15 @@ impl From<&SortOrder> for sea_query::Order { match value { SortOrder::Asc => sea_query::Order::Asc, SortOrder::Desc => sea_query::Order::Desc, - SortOrder::Custom(v) => sea_query::Order::Field(v.clone()), + } + } +} + +impl From for sea_query::Order { + fn from(value: SortOrder) -> Self { + match value { + SortOrder::Asc => sea_query::Order::Asc, + SortOrder::Desc => sea_query::Order::Desc, } } } @@ -34,6 +40,15 @@ pub enum NullsOrder { Last, } +impl From<&NullsOrder> for sea_query::NullOrdering { + fn from(value: &NullsOrder) -> Self { + match value { + NullsOrder::First => sea_query::NullOrdering::First, + NullsOrder::Last => sea_query::NullOrdering::Last, + } + } +} + impl From for sea_query::NullOrdering { fn from(value: NullsOrder) -> Self { match value { @@ -43,6 +58,25 @@ impl From for sea_query::NullOrdering { } } +/// The type of the order field +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub(crate) enum OrderTarget { + /// Whether the order field is a column + Column(Identifier), + /// Whether the order field is an expression + Expression(Expr), +} + +#[derive(Debug, Clone, PartialEq)] +enum OrderMode { + Directional { + order: SortOrder, + nulls: Option, + }, + Custom(sea_query::Values), +} + /// An `ORDER BY` term. /// /// # Example @@ -67,26 +101,49 @@ impl From for sea_query::NullOrdering { #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub struct OrderByExpr { - field: Identifier, - order: SortOrder, - nulls: Option, + target: OrderTarget, + mode: OrderMode, } impl OrderByExpr { - pub(crate) fn new(field: Identifier, order: SortOrder) -> Self { + pub(crate) fn directional(target: OrderTarget, order: SortOrder) -> Self { + Self { + target, + mode: OrderMode::Directional { order, nulls: None }, + } + } + + pub(crate) fn custom(target: OrderTarget, values: sea_query::Values) -> Self { + assert!( + !values.0.is_empty(), + "`custom` requires at least one value to rank by" + ); Self { - field, - order, - nulls: None, + target, + mode: OrderMode::Custom(values), } } + /// Places `NULL` values before all non-`NULL` values for this term, + /// regardless of database backend or sort direction. + /// + /// # Panics + /// + /// Panics if this term was built with [`ExprSort::custom_order`]. A + /// custom-order term never produces a `NULL` sort key, + /// so an explicit `NULLS` placement on top of it can never have any + /// effect. #[must_use] pub fn nulls_first(mut self) -> Self { self.set_nulls(NullsOrder::First); self } + /// Places `NULL` values after all non-`NULL` values for this term. + /// + /// # Panics + /// + /// See [`Self::nulls_first`]. #[must_use] pub fn nulls_last(mut self) -> Self { self.set_nulls(NullsOrder::Last); @@ -95,57 +152,103 @@ impl OrderByExpr { #[track_caller] fn set_nulls(&mut self, nulls: NullsOrder) { - match &mut self.order { - SortOrder::Asc | SortOrder::Desc => self.nulls = Some(nulls), - SortOrder::Custom(_) => panic!( - "`nulls_first`/`nulls_last` can't be combined with `custom_order`: a custom-order term never produces \ - a NULL sort key, so an explicit NULLS placement would have no effect" + match &mut self.mode { + OrderMode::Directional { nulls: n, .. } => *n = Some(nulls), + OrderMode::Custom(_) => panic!( + "`nulls_first`/`nulls_last` can't be combined with `custom`: a custom-order \ + term never produces a NULL sort key, so an explicit NULLS placement would \ + have no effect" ), } } - pub(crate) fn add_to_statement(&self, statement: &mut sea_query::SelectStatement) { - let order: sea_query::Order = (&self.order).into(); - if let Some(nulls) = self.nulls { - let nulls: sea_query::NullOrdering = nulls.into(); - statement.order_by_with_nulls(self.field, order, nulls); - } else { - statement.order_by(self.field, order); + pub(crate) fn add_to_statement( + &self, + statement: &mut sea_query::SelectStatement, + sql_builder: &dyn SqlQueryBuilder, + ) -> Result<(), QueryBuildingError> { + let (sea_order, nulls): (sea_query::Order, Option) = match &self.mode { + OrderMode::Directional { order, nulls } => (order.into(), *nulls), + OrderMode::Custom(values) => (sea_query::Order::Field(values.clone()), None), }; + + match &self.target { + OrderTarget::Column(field) => match nulls { + Some(nulls) => { + statement.order_by_with_nulls(*field, sea_order, nulls.into()); + } + None => { + statement.order_by(*field, sea_order); + } + }, + OrderTarget::Expression(expr) => { + let expr = expr.as_sea_query_expr(sql_builder)?; + match nulls { + Some(nulls) => { + statement.order_by_expr_with_nulls(expr, sea_order, nulls.into()); + } + None => { + statement.order_by_expr(expr, sea_order); + } + } + } + } + Ok(()) + } +} + +impl From> for OrderByExpr { + fn from(field: FieldRef) -> Self { + OrderByExpr::directional(OrderTarget::Column(field.identifier()), SortOrder::Asc) + } +} + +impl From for OrderByExpr { + fn from(expr: Expr) -> Self { + expr.asc() } } +/// A trait for database types that support sorting. pub trait ExprSort { + /// Sort by this field in ascending order. fn asc(&self) -> OrderByExpr; + /// Sort by this field in descending order. fn desc(&self) -> OrderByExpr; + /// Sorts rows by the position of this field's value fn custom(&self, values: I) -> OrderByExpr where I: IntoIterator, - I::Item: ToDbValue; + I::Item: IntoField; } -impl ExprSort for FieldRef { +impl ExprSort for FieldRef { fn asc(&self) -> OrderByExpr { - OrderByExpr::new(self.identifier(), SortOrder::Asc) + OrderByExpr::directional(OrderTarget::Column(self.identifier()), SortOrder::Asc) } fn desc(&self) -> OrderByExpr { - OrderByExpr::new(self.identifier(), SortOrder::Desc) + OrderByExpr::directional(OrderTarget::Column(self.identifier()), SortOrder::Desc) } fn custom(&self, values: I) -> OrderByExpr where I: IntoIterator, - I::Item: ToDbValue, + I::Item: IntoField, { let values = values .into_iter() - .map(|v| match v.to_db_field_value() { + .map(|v| match v.into_field().to_db_field_value() { DbFieldValue::Value(value) => value, - DbFieldValue::Auto => panic!("Cannot order by a non-value field"), + DbFieldValue::Auto => { + panic!("cannot use an auto-generated value as a custom ordering key") + } }) - .collect::>(); - OrderByExpr::new(self.identifier(), SortOrder::Custom(Values(values))) + .collect(); + OrderByExpr::custom( + OrderTarget::Column(self.identifier()), + sea_query::Values(values), + ) } }