Skip to main content

siderite_orm/expr/
functions.rs

1//! Scalar functions, `CASE` and date-part extraction.
2//!
3//! ```ignore
4//! use siderite_orm::functions::*;
5//! User::objects(&db)
6//!     .filter(lower(User::name.expr()).eq("ann"))
7//!     .annotate("label", coalesce([User::nick.expr(), User::name.expr()]))
8//! ```
9
10use super::Expr;
11use crate::types::SqlType;
12
13/// Scalar functions with the same meaning on every relational backend.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Function {
16    /// `LOWER(text)`.
17    Lower,
18    /// `UPPER(text)`.
19    Upper,
20    /// Length in characters.
21    Length,
22    /// First non-`NULL` argument.
23    Coalesce,
24    /// Concatenation; `NULL` arguments count as empty text.
25    Concat,
26    /// `SUBSTR(text, start [, length])` with a 1-based start.
27    Substr,
28    /// `REPLACE(text, from, to)`.
29    Replace,
30    /// Remove leading and trailing spaces.
31    Trim,
32}
33
34/// Component extracted by [`Expr::date_part`].
35///
36/// `Week` is the ISO 8601 week number on every backend. On SQLite the source
37/// must use the canonical text forms documented in [`crate::types`]; on
38/// PostgreSQL timestamps are read in UTC.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum DatePart {
41    /// Calendar year.
42    Year,
43    /// Month `1..=12`.
44    Month,
45    /// Day of month.
46    Day,
47    /// ISO 8601 week `1..=53`.
48    Week,
49    /// Quarter `1..=4`.
50    Quarter,
51    /// Hour `0..=23`.
52    Hour,
53    /// Minute `0..=59`.
54    Minute,
55    /// Second `0..=59` (fractions are dropped).
56    Second,
57    /// The date, truncating a timestamp.
58    Date,
59}
60
61fn call(func: Function, args: impl IntoIterator<Item = Expr>) -> Expr {
62    Expr::Func {
63        func,
64        args: args.into_iter().collect(),
65    }
66}
67
68/// `LOWER(expr)`.
69pub fn lower(expr: impl Into<Expr>) -> Expr {
70    call(Function::Lower, [expr.into()])
71}
72
73/// `UPPER(expr)`.
74pub fn upper(expr: impl Into<Expr>) -> Expr {
75    call(Function::Upper, [expr.into()])
76}
77
78/// Length of `expr` in characters.
79pub fn length(expr: impl Into<Expr>) -> Expr {
80    call(Function::Length, [expr.into()])
81}
82
83/// First non-`NULL` of `exprs`.
84pub fn coalesce<E: Into<Expr>>(exprs: impl IntoIterator<Item = E>) -> Expr {
85    call(Function::Coalesce, exprs.into_iter().map(Into::into))
86}
87
88/// Concatenate text expressions (`NULL` counts as empty text).
89pub fn concat<E: Into<Expr>>(exprs: impl IntoIterator<Item = E>) -> Expr {
90    call(Function::Concat, exprs.into_iter().map(Into::into))
91}
92
93/// `SUBSTR(expr, start, length)` with a 1-based `start`.
94pub fn substr(expr: impl Into<Expr>, start: i64, length: Option<i64>) -> Expr {
95    let mut args = vec![expr.into(), Expr::val(start)];
96    args.extend(length.map(Expr::val));
97    call(Function::Substr, args)
98}
99
100/// Replace every `from` in `expr` by `to`.
101pub fn replace(expr: impl Into<Expr>, from: impl Into<String>, to: impl Into<String>) -> Expr {
102    call(
103        Function::Replace,
104        [expr.into(), Expr::val(from.into()), Expr::val(to.into())],
105    )
106}
107
108/// Strip leading and trailing spaces.
109pub fn trim(expr: impl Into<Expr>) -> Expr {
110    call(Function::Trim, [expr.into()])
111}
112
113/// `CAST(expr AS ty)`.
114pub fn cast(expr: impl Into<Expr>, ty: SqlType) -> Expr {
115    expr.into().cast(ty)
116}
117
118/// Start a searched `CASE`: `case().when(cond, then).otherwise(fallback)`.
119pub fn case() -> CaseBuilder {
120    CaseBuilder::default()
121}
122
123/// Builder for [`Expr::Case`].
124#[derive(Debug, Clone, Default)]
125pub struct CaseBuilder {
126    branches: Vec<(Expr, Expr)>,
127}
128
129impl CaseBuilder {
130    /// Add a `WHEN condition THEN result` branch.
131    #[must_use]
132    pub fn when(mut self, condition: Expr, result: impl Into<Expr>) -> Self {
133        self.branches.push((condition, result.into()));
134        self
135    }
136
137    /// Finish with an `ELSE` result.
138    pub fn otherwise(self, fallback: impl Into<Expr>) -> Expr {
139        Expr::Case {
140            branches: self.branches,
141            otherwise: Some(Box::new(fallback.into())),
142        }
143    }
144
145    /// Finish without `ELSE`: unmatched rows yield `NULL`.
146    pub fn end(self) -> Expr {
147        Expr::Case {
148            branches: self.branches,
149            otherwise: None,
150        }
151    }
152}
153
154macro_rules! date_parts {
155    ($($method:ident => $part:ident),* $(,)?) => {
156        impl Expr {$(
157            #[doc = concat!("The `", stringify!($part), "` component of a temporal expression.")]
158            pub fn $method(self) -> Expr {
159                self.date_part(DatePart::$part)
160            }
161        )*}
162    };
163}
164date_parts!(
165    year => Year, month => Month, day => Day, week => Week, quarter => Quarter,
166    hour => Hour, minute => Minute, second => Second, date => Date,
167);