Skip to main content

cratestack_sql/filter/
coalesce.rs

1use crate::{IntoSqlValue, values::FilterValue};
2
3use super::expr::FilterExpr;
4use super::field_ref::FieldRef;
5use super::op::FilterOp;
6
7/// `COALESCE(col_a, col_b, ...) <op> <value>` — left-hand expression
8/// is the first non-null among the listed columns; right-hand side is
9/// a bound value via the usual `FilterValue` envelope. Lets schemas
10/// express the "ranked-fallback compare" pattern that shows up in
11/// outbox / scheduler tables, where a single row carries several
12/// time columns and the dispatcher wants the earliest non-null one.
13///
14/// `IsNull` and `IsNotNull` are valid `op` choices too: a row where
15/// every coalesced column is null collapses to `COALESCE(...) IS
16/// NULL`, which the engine can index-elide when at least one of the
17/// inputs has a `NOT NULL` constraint.
18#[derive(Debug, Clone, PartialEq)]
19pub struct CoalesceFilter {
20    pub columns: Vec<&'static str>,
21    pub op: FilterOp,
22    pub value: FilterValue,
23}
24
25/// Anything that can name a single SQL column. Lets [`coalesce`]
26/// accept both bare `&'static str` column names and typed
27/// [`FieldRef`] handles, so callers don't have to choose between
28/// schema-rooted typing and ad-hoc strings at the call site.
29pub trait IntoColumnName {
30    fn into_column_name(self) -> &'static str;
31}
32
33impl IntoColumnName for &'static str {
34    fn into_column_name(self) -> &'static str {
35        self
36    }
37}
38
39impl<M, T> IntoColumnName for FieldRef<M, T> {
40    fn into_column_name(self) -> &'static str {
41        self.column_name()
42    }
43}
44
45/// Build a `COALESCE(...)` left-hand operand. The returned
46/// [`CoalesceExpr`] carries the column list; chain a comparator
47/// method (`.lte`, `.eq`, `.is_null`, ...) to produce a [`FilterExpr`]
48/// the query builders can consume.
49///
50/// ```ignore
51/// .where_(coalesce([
52///     task::next_attempt_at(),
53///     task::scheduled_at(),
54///     task::created_at(),
55/// ]).lte(now))
56/// ```
57pub fn coalesce<I, C>(columns: I) -> CoalesceExpr
58where
59    I: IntoIterator<Item = C>,
60    C: IntoColumnName,
61{
62    CoalesceExpr {
63        columns: columns
64            .into_iter()
65            .map(IntoColumnName::into_column_name)
66            .collect(),
67    }
68}
69
70/// Left-hand operand of a coalesce-based filter — chain a comparator
71/// method to turn it into a [`FilterExpr`].
72#[derive(Debug, Clone)]
73pub struct CoalesceExpr {
74    columns: Vec<&'static str>,
75}
76
77impl CoalesceExpr {
78    fn into_filter<V: IntoSqlValue>(self, op: FilterOp, value: V) -> FilterExpr {
79        FilterExpr::Coalesce(CoalesceFilter {
80            columns: self.columns,
81            op,
82            value: FilterValue::Single(value.into_sql_value()),
83        })
84    }
85
86    pub fn eq<V: IntoSqlValue>(self, value: V) -> FilterExpr {
87        self.into_filter(FilterOp::Eq, value)
88    }
89    pub fn ne<V: IntoSqlValue>(self, value: V) -> FilterExpr {
90        self.into_filter(FilterOp::Ne, value)
91    }
92    pub fn lt<V: IntoSqlValue>(self, value: V) -> FilterExpr {
93        self.into_filter(FilterOp::Lt, value)
94    }
95    pub fn lte<V: IntoSqlValue>(self, value: V) -> FilterExpr {
96        self.into_filter(FilterOp::Lte, value)
97    }
98    pub fn gt<V: IntoSqlValue>(self, value: V) -> FilterExpr {
99        self.into_filter(FilterOp::Gt, value)
100    }
101    pub fn gte<V: IntoSqlValue>(self, value: V) -> FilterExpr {
102        self.into_filter(FilterOp::Gte, value)
103    }
104
105    /// `COALESCE(...) IS NULL` — every input column was null. No
106    /// bind: this side never carries a value.
107    pub fn is_null(self) -> FilterExpr {
108        FilterExpr::Coalesce(CoalesceFilter {
109            columns: self.columns,
110            op: FilterOp::IsNull,
111            value: FilterValue::None,
112        })
113    }
114
115    /// `COALESCE(...) IS NOT NULL` — at least one input column has a
116    /// value.
117    pub fn is_not_null(self) -> FilterExpr {
118        FilterExpr::Coalesce(CoalesceFilter {
119            columns: self.columns,
120            op: FilterOp::IsNotNull,
121            value: FilterValue::None,
122        })
123    }
124}