diff --git a/cot/src/db.rs b/cot/src/db.rs index 0b4c2ca3..57aa8e4b 100644 --- a/cot/src/db.rs +++ b/cot/src/db.rs @@ -1022,6 +1022,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. @@ -2575,6 +2578,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, 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/fields.rs b/cot/src/db/fields.rs index e420143b..bbbde19a 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.rs b/cot/src/db/query.rs index 3bc4a50a..268d072c 100644 --- a/cot/src/db/query.rs +++ b/cot/src/db/query.rs @@ -8,8 +8,8 @@ use derive_more::with_trait::Debug; 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:"; @@ -47,6 +47,7 @@ pub enum QueryBuildingError { pub struct Query { filter: Option, limit: Option, + order_by: Vec, offset: Option, phantom_data: PhantomData T>, } @@ -56,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) @@ -69,6 +71,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, } @@ -112,6 +115,7 @@ impl Query { Self { filter: None, limit: None, + order_by: Vec::new(), offset: None, phantom_data: PhantomData, } @@ -163,6 +167,32 @@ 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(User::age, Order::Asc); // or Order::Desc + /// ``` + 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 + } + /// Set the offset for the query. /// /// # Example @@ -249,6 +279,17 @@ impl Query { } } + 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, sql_builder)?; + } + Ok(()) + } + pub(super) fn add_offset_to_statement(&self, statement: &mut sea_query::SelectStatement) { if let Some(offset) = self.offset { statement.offset(offset); diff --git a/cot/src/db/query/expr.rs b/cot/src/db/query/expr.rs index 8946ccd9..5d585649 100644 --- a/cot/src/db/query/expr.rs +++ b/cot/src/db/query/expr.rs @@ -1,14 +1,20 @@ //! Database expressions. 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, @@ -1197,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 @@ -1316,6 +1354,21 @@ impl FieldRef { pub fn as_expr(&self) -> Expr { Expr::Field(self.identifier) } + + pub(crate) fn identifier(&self) -> Identifier { + self.identifier + } +} + +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. @@ -1645,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 new file mode 100644 index 00000000..19562ab5 --- /dev/null +++ b/cot/src/db/query/expr/order_by.rs @@ -0,0 +1,254 @@ +use cot::db::{DbFieldValue, ToDbFieldValue}; + +use crate::db::Identifier; +use crate::db::query::expr::{FieldRef, SqlQueryBuilder}; +use crate::db::query::{Expr, IntoField, QueryBuildingError}; + +/// Ordering Options +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SortOrder { + /// Sort in Ascending order. + Asc, + /// Sort in Descending Order. + Desc, +} + +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, + } + } +} + +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<&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 { + NullsOrder::First => sea_query::NullOrdering::First, + NullsOrder::Last => sea_query::NullOrdering::Last, + } + } +} + +/// 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 +/// +/// ``` +/// 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 { + target: OrderTarget, + mode: OrderMode, +} + +impl OrderByExpr { + 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 { + 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); + self + } + + #[track_caller] + fn set_nulls(&mut self, nulls: NullsOrder) { + 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, + 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: IntoField; +} + +impl ExprSort for FieldRef { + fn asc(&self) -> OrderByExpr { + OrderByExpr::directional(OrderTarget::Column(self.identifier()), SortOrder::Asc) + } + + fn desc(&self) -> OrderByExpr { + OrderByExpr::directional(OrderTarget::Column(self.identifier()), SortOrder::Desc) + } + + fn custom(&self, values: I) -> OrderByExpr + where + I: IntoIterator, + I::Item: IntoField, + { + let values = values + .into_iter() + .map(|v| match v.into_field().to_db_field_value() { + DbFieldValue::Value(value) => value, + DbFieldValue::Auto => { + panic!("cannot use an auto-generated value as a custom ordering key") + } + }) + .collect(); + OrderByExpr::custom( + OrderTarget::Column(self.identifier()), + sea_query::Values(values), + ) + } +}