Skip to main content

renox_core/db/
column.rs

1//! Typed columns: a [`Col`] names one column of a model and carries its Rust type, and its
2//! comparison methods build a [`Condition`] for [`Query::where_`](super::Query::where_).
3
4use std::fmt;
5use std::marker::PhantomData;
6
7use super::value::{DbValue, ToDbValue};
8
9/// One column of the model `M`, whose Rust type is `T` (for example `Option<String>`).
10///
11/// A misspelled column or a value of the wrong type is a compile error:
12///
13/// ```
14/// use renox::db::Col;
15///
16/// struct Product;
17/// const PRICE: Col<Product, i64> = Col::new("price");
18///
19/// let cheap = PRICE.lt(20_000);
20/// # let _ = cheap;
21/// ```
22pub struct Col<M, T> {
23    name: &'static str,
24    _p: PhantomData<fn() -> (M, T)>,
25}
26
27impl<M, T> Clone for Col<M, T> {
28    fn clone(&self) -> Self {
29        *self
30    }
31}
32
33impl<M, T> Copy for Col<M, T> {}
34
35impl<M, T> fmt::Debug for Col<M, T> {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.debug_tuple("Col").field(&self.name).finish()
38    }
39}
40
41impl<M, T> Col<M, T> {
42    /// A column named `name`.
43    pub const fn new(name: &'static str) -> Self {
44        Self {
45            name,
46            _p: PhantomData,
47        }
48    }
49
50    /// The column's name.
51    pub const fn name(&self) -> &'static str {
52        self.name
53    }
54}
55
56/// Anything that names a column of the model `M`: a string, or a typed [`Col`] of `M`.
57///
58/// Taken by `order_by`, `order_by_desc` and `pluck`. Implemented for `&str`, `&&str`,
59/// `String`, `&String` and `Col<M, T>`.
60pub trait IntoColumn<M> {
61    /// The column's name.
62    fn column_name(&self) -> &str;
63}
64
65impl<M> IntoColumn<M> for &str {
66    fn column_name(&self) -> &str {
67        self
68    }
69}
70
71impl<M> IntoColumn<M> for &&str {
72    fn column_name(&self) -> &str {
73        self
74    }
75}
76
77impl<M> IntoColumn<M> for String {
78    fn column_name(&self) -> &str {
79        self
80    }
81}
82
83impl<M> IntoColumn<M> for &String {
84    fn column_name(&self) -> &str {
85        self
86    }
87}
88
89impl<M, T> IntoColumn<M> for Col<M, T> {
90    fn column_name(&self) -> &str {
91        self.name
92    }
93}
94
95/// Says that a column of type `Self` can be compared with a value of type `V`.
96///
97/// Implemented by Renox only, for a type and itself, an `Option` and its inner type, and
98/// `String` / `Option<String>` with `&str`.
99pub trait Comparable<V> {}
100
101impl<T: ToDbValue> Comparable<T> for T {}
102impl<T: ToDbValue> Comparable<T> for Option<T> {}
103impl Comparable<&str> for String {}
104impl Comparable<&str> for Option<String> {}
105
106/// A filter on the model `M`, made by a [`Col`] and given to
107/// [`Query::where_`](super::Query::where_).
108#[non_exhaustive]
109pub struct Condition<M> {
110    pub(super) kind: CondKind,
111    _p: PhantomData<fn() -> M>,
112}
113
114pub(super) enum CondKind {
115    Op(&'static str, &'static str, DbValue),
116    Null(&'static str, bool),
117    In(&'static str, Vec<DbValue>),
118}
119
120impl<M> fmt::Debug for Condition<M> {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        f.write_str("Condition")
123    }
124}
125
126impl<M> Condition<M> {
127    fn new(kind: CondKind) -> Self {
128        Self {
129            kind,
130            _p: PhantomData,
131        }
132    }
133}
134
135macro_rules! comparison {
136    ($($(#[$doc:meta])* $method:ident => $op:literal),* $(,)?) => {
137        $(
138            $(#[$doc])*
139            pub fn $method<V: ToDbValue>(self, value: V) -> Condition<M>
140            where
141                T: Comparable<V>,
142            {
143                Condition::new(CondKind::Op(self.name, $op, value.to_db_value()))
144            }
145        )*
146    };
147}
148
149impl<M, T> Col<M, T> {
150    comparison! {
151        /// `column = value`.
152        eq => "=",
153        /// `column != value`.
154        ne => "!=",
155        /// `column < value`.
156        lt => "<",
157        /// `column <= value`.
158        lte => "<=",
159        /// `column > value`.
160        gt => ">",
161        /// `column >= value`.
162        gte => ">=",
163    }
164
165    /// `column IS NULL`.
166    pub fn is_null(self) -> Condition<M> {
167        Condition::new(CondKind::Null(self.name, true))
168    }
169
170    /// `column IS NOT NULL`.
171    pub fn is_not_null(self) -> Condition<M> {
172        Condition::new(CondKind::Null(self.name, false))
173    }
174
175    /// `column IN (…)`; an empty list matches no rows.
176    pub fn is_in<V: ToDbValue>(self, values: impl IntoIterator<Item = V>) -> Condition<M>
177    where
178        T: Comparable<V>,
179    {
180        let values = values.into_iter().map(|v| v.to_db_value()).collect();
181        Condition::new(CondKind::In(self.name, values))
182    }
183}
184
185impl<M> Col<M, String> {
186    /// `column LIKE pattern`, ignoring ASCII case on both databases.
187    pub fn like(self, pattern: &str) -> Condition<M> {
188        Condition::new(CondKind::Op(self.name, "like", pattern.to_db_value()))
189    }
190}
191
192impl<M> Col<M, Option<String>> {
193    /// `column LIKE pattern`, ignoring ASCII case on both databases.
194    pub fn like(self, pattern: &str) -> Condition<M> {
195        Condition::new(CondKind::Op(self.name, "like", pattern.to_db_value()))
196    }
197}