Skip to main content

cratestack_sql/filter/
field_ref.rs

1use std::marker::PhantomData;
2
3use crate::{IntoSqlValue, OrderClause, SortDirection, order::OrderTarget, values::FilterValue};
4
5use super::filter::Filter;
6use super::op::FilterOp;
7
8#[derive(Debug, Clone, Copy)]
9pub struct FieldRef<M, T> {
10    pub(super) column: &'static str,
11    _marker: PhantomData<fn() -> (M, T)>,
12}
13
14impl<M, T> FieldRef<M, T> {
15    pub const fn new(column: &'static str) -> Self {
16        Self {
17            column,
18            _marker: PhantomData,
19        }
20    }
21
22    /// The underlying SQL column name. Exposed so AST-builder helpers
23    /// like [`super::coalesce::coalesce`] can interop with the typed
24    /// `FieldRef` API without giving up generic-column flexibility.
25    pub const fn column_name(self) -> &'static str {
26        self.column
27    }
28
29    pub fn asc(self) -> OrderClause {
30        OrderClause {
31            target: OrderTarget::Column(self.column),
32            direction: SortDirection::Asc,
33            null_order: crate::NullOrder::Last,
34        }
35    }
36
37    pub fn desc(self) -> OrderClause {
38        OrderClause {
39            target: OrderTarget::Column(self.column),
40            direction: SortDirection::Desc,
41            null_order: crate::NullOrder::Last,
42        }
43    }
44}
45
46impl<M, T> FieldRef<M, T> {
47    pub fn eq<V>(self, value: V) -> Filter
48    where
49        V: IntoSqlValue,
50    {
51        Filter::single(self.column, FilterOp::Eq, value)
52    }
53
54    pub fn ne<V>(self, value: V) -> Filter
55    where
56        V: IntoSqlValue,
57    {
58        Filter::single(self.column, FilterOp::Ne, value)
59    }
60
61    pub fn in_<I, V>(self, values: I) -> Filter
62    where
63        I: IntoIterator<Item = V>,
64        V: IntoSqlValue,
65    {
66        Filter {
67            column: self.column,
68            op: FilterOp::In,
69            value: FilterValue::Many(
70                values
71                    .into_iter()
72                    .map(IntoSqlValue::into_sql_value)
73                    .collect(),
74            ),
75        }
76    }
77
78    pub fn lt<V>(self, value: V) -> Filter
79    where
80        V: IntoSqlValue,
81    {
82        Filter::single(self.column, FilterOp::Lt, value)
83    }
84
85    pub fn lte<V>(self, value: V) -> Filter
86    where
87        V: IntoSqlValue,
88    {
89        Filter::single(self.column, FilterOp::Lte, value)
90    }
91
92    pub fn gt<V>(self, value: V) -> Filter
93    where
94        V: IntoSqlValue,
95    {
96        Filter::single(self.column, FilterOp::Gt, value)
97    }
98
99    pub fn gte<V>(self, value: V) -> Filter
100    where
101        V: IntoSqlValue,
102    {
103        Filter::single(self.column, FilterOp::Gte, value)
104    }
105
106    /// Match rows where the column is null OR equals `value`. The
107    /// canonical inline-SQL workaround for "filter only if the caller
108    /// provided this value, otherwise let nulls through" — schemas
109    /// with sparse, optional foreign-key-style columns hit this
110    /// constantly. Renders as `(col IS NULL OR col = $1)`.
111    ///
112    /// Use [`Self::match_optional`] when the *caller's* value is
113    /// itself an `Option` — that variant skips the filter entirely on
114    /// `None` instead of binding a null.
115    pub fn eq_or_null<V>(self, value: V) -> Filter
116    where
117        V: IntoSqlValue,
118    {
119        Filter::single(self.column, FilterOp::EqOrNull, value)
120    }
121
122    /// Filter on equality when the caller has a value, skip the
123    /// filter entirely when they don't. Returns `None` for the no-op
124    /// case so callers can plumb it through
125    /// [`crate::FilterExpr::any_of_optional`]-style helpers, or feed
126    /// it directly into a `where_optional(...)` builder method on the
127    /// query builders.
128    ///
129    /// The emitted filter is the same `(col IS NULL OR col = $1)` as
130    /// [`Self::eq_or_null`] — when the caller *did* supply a value,
131    /// we still let nulls through, matching the canonical
132    /// "optional-equality with null-as-wildcard" semantics from the
133    /// inline-SQL pattern.
134    pub fn match_optional<V>(self, value: Option<V>) -> Option<Filter>
135    where
136        V: IntoSqlValue,
137    {
138        value.map(|v| self.eq_or_null(v))
139    }
140}
141
142impl<M> FieldRef<M, bool> {
143    pub fn is_true(self) -> Filter {
144        self.eq(true)
145    }
146
147    pub fn is_false(self) -> Filter {
148        self.eq(false)
149    }
150}
151
152impl<M> FieldRef<M, String> {
153    pub fn contains(self, value: impl Into<String>) -> Filter {
154        Filter::string_pattern(self.column, FilterOp::Contains, "%{}%", value)
155    }
156
157    pub fn starts_with(self, value: impl Into<String>) -> Filter {
158        Filter::string_pattern(self.column, FilterOp::StartsWith, "{}%", value)
159    }
160}
161
162impl<M, T> FieldRef<M, Option<T>> {
163    pub fn is_null(self) -> Filter {
164        Filter {
165            column: self.column,
166            op: FilterOp::IsNull,
167            value: FilterValue::None,
168        }
169    }
170
171    pub fn is_not_null(self) -> Filter {
172        Filter {
173            column: self.column,
174            op: FilterOp::IsNotNull,
175            value: FilterValue::None,
176        }
177    }
178}
179
180impl<M> FieldRef<M, Option<String>> {
181    pub fn contains(self, value: impl Into<String>) -> Filter {
182        Filter::string_pattern(self.column, FilterOp::Contains, "%{}%", value)
183    }
184
185    pub fn starts_with(self, value: impl Into<String>) -> Filter {
186        Filter::string_pattern(self.column, FilterOp::StartsWith, "{}%", value)
187    }
188}