Skip to main content

floz_orm/
column.rs

1//! Typed column references for building type-safe queries.
2//!
3//! `Column<T>` carries the Rust type `T` at compile time, enabling the proc macro
4//! to generate type-safe `.eq()`, `.gt()` etc. methods. At runtime, the column
5//! name and table are used for SQL rendering.
6//!
7//! ```ignore
8//! // Generated by the proc macro:
9//! pub struct UserTable;
10//! impl UserTable {
11//!     pub const id: Column<i32> = Column::new("id", "users");
12//!     pub const name: Column<String> = Column::new("name", "users");
13//! }
14//!
15//! // Used in queries:
16//! UserTable::age.gt(25)              // Expr::Gt
17//! UserTable::name.eq("Alice")        // Expr::Eq
18//! UserTable::name.asc()              // OrderExpr
19//! ```
20
21use std::marker::PhantomData;
22
23use crate::expr::{Expr, OrderDirection, OrderExpr};
24use crate::value::Value;
25
26/// A typed reference to a database column.
27///
28/// `T` represents the Rust type this column maps to (e.g., `Column<i32>` for INTEGER).
29/// The type parameter enables compile-time type checking when the proc macro generates
30/// operator methods.
31#[derive(Debug, Clone)]
32pub struct Column<T> {
33    name: &'static str,
34    table: &'static str,
35    _phantom: PhantomData<T>,
36}
37
38impl<T> Column<T> {
39    /// Create a new column reference.
40    pub const fn new(name: &'static str, table: &'static str) -> Self {
41        Self {
42            name,
43            table,
44            _phantom: PhantomData,
45        }
46    }
47
48    /// The database column name.
49    pub const fn name(&self) -> &'static str {
50        self.name
51    }
52
53    /// The table this column belongs to.
54    pub const fn table(&self) -> &'static str {
55        self.table
56    }
57
58    /// Erase the type information for dynamic use.
59    pub const fn into_any(&self) -> AnyColumn {
60        AnyColumn {
61            name: self.name,
62            table: self.table,
63        }
64    }
65
66    fn any(&self) -> AnyColumn {
67        self.into_any()
68    }
69}
70
71// ── Comparison operators (available on all column types) ──
72
73impl<T> Column<T>
74where
75    T: Into<Value> + Clone,
76{
77    /// `column = value`
78    pub fn eq(&self, val: impl Into<Value>) -> Expr {
79        Expr::Eq(self.any(), val.into())
80    }
81
82    /// `column != value`
83    pub fn ne(&self, val: impl Into<Value>) -> Expr {
84        Expr::Ne(self.any(), val.into())
85    }
86
87    /// `column > value`
88    pub fn gt(&self, val: impl Into<Value>) -> Expr {
89        Expr::Gt(self.any(), val.into())
90    }
91
92    /// `column >= value`
93    pub fn gte(&self, val: impl Into<Value>) -> Expr {
94        Expr::Gte(self.any(), val.into())
95    }
96
97    /// `column < value`
98    pub fn lt(&self, val: impl Into<Value>) -> Expr {
99        Expr::Lt(self.any(), val.into())
100    }
101
102    /// `column <= value`
103    pub fn lte(&self, val: impl Into<Value>) -> Expr {
104        Expr::Lte(self.any(), val.into())
105    }
106
107    /// `column BETWEEN low AND high`
108    pub fn between(&self, low: impl Into<Value>, high: impl Into<Value>) -> Expr {
109        Expr::Between(self.any(), low.into(), high.into())
110    }
111
112    /// `column IN (values...)` — empty list renders as `1=0` (always false)
113    pub fn in_list(&self, vals: Vec<impl Into<Value>>) -> Expr {
114        let values: Vec<Value> = vals.into_iter().map(|v| v.into()).collect();
115        Expr::InList(self.any(), values)
116    }
117
118    /// `column NOT IN (values...)` — empty list renders as `1=1` (always true)
119    pub fn not_in(&self, vals: Vec<impl Into<Value>>) -> Expr {
120        let values: Vec<Value> = vals.into_iter().map(|v| v.into()).collect();
121        Expr::NotIn(self.any(), values)
122    }
123}
124
125// ── String operators (available on all columns, typically used with String columns) ──
126
127impl<T> Column<T> {
128    /// `column LIKE pattern` — e.g., `.like("A%")`
129    pub fn like(&self, pattern: impl Into<Value>) -> Expr {
130        Expr::Like(self.any(), pattern.into())
131    }
132
133    /// `column ILIKE pattern` — case-insensitive (PostgreSQL)
134    pub fn ilike(&self, pattern: impl Into<Value>) -> Expr {
135        Expr::ILike(self.any(), pattern.into())
136    }
137
138    /// `column LIKE '%value%'` — shorthand for contains
139    pub fn contains(&self, val: &str) -> Expr {
140        Expr::Like(self.any(), Value::String(format!("%{}%", val)))
141    }
142
143    /// `column LIKE 'value%'` — shorthand for starts_with
144    pub fn starts_with(&self, val: &str) -> Expr {
145        Expr::Like(self.any(), Value::String(format!("{}%", val)))
146    }
147
148    /// `column LIKE '%value'` — shorthand for ends_with
149    pub fn ends_with(&self, val: &str) -> Expr {
150        Expr::Like(self.any(), Value::String(format!("%{}", val)))
151    }
152}
153
154// ── Null operators ──
155
156impl<T> Column<T> {
157    /// `column IS NULL`
158    pub fn is_null(&self) -> Expr {
159        Expr::IsNull(self.any())
160    }
161
162    /// `column IS NOT NULL`
163    pub fn is_not_null(&self) -> Expr {
164        Expr::IsNotNull(self.any())
165    }
166}
167
168// ── Ordering ──
169
170impl<T> Column<T> {
171    /// `column ASC`
172    pub fn asc(&self) -> OrderExpr {
173        OrderExpr::new(self.any(), OrderDirection::Asc)
174    }
175
176    /// `column DESC`
177    pub fn desc(&self) -> OrderExpr {
178        OrderExpr::new(self.any(), OrderDirection::Desc)
179    }
180
181    /// `column ASC NULLS LAST`
182    pub fn asc_nulls_last(&self) -> OrderExpr {
183        OrderExpr::new(self.any(), OrderDirection::AscNullsLast)
184    }
185
186    /// `column DESC NULLS FIRST`
187    pub fn desc_nulls_first(&self) -> OrderExpr {
188        OrderExpr::new(self.any(), OrderDirection::DescNullsFirst)
189    }
190}
191
192// ── AnyColumn (type-erased) ──
193
194/// A type-erased column reference for use in dynamic contexts (e.g., `Value` bindings, `user_row!`).
195#[derive(Debug, Clone)]
196pub struct AnyColumn {
197    name: &'static str,
198    table: &'static str,
199}
200
201impl AnyColumn {
202    pub const fn new(name: &'static str, table: &'static str) -> Self {
203        Self { name, table }
204    }
205
206    pub const fn name(&self) -> &'static str {
207        self.name
208    }
209
210    pub const fn table(&self) -> &'static str {
211        self.table
212    }
213}
214
215// ── Tests ──
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    fn col_id() -> Column<i32> {
222        Column::new("id", "users")
223    }
224
225    fn col_age() -> Column<i32> {
226        Column::new("age", "users")
227    }
228
229    fn col_name() -> Column<String> {
230        Column::new("name", "users")
231    }
232
233    fn col_email() -> Column<String> {
234        Column::new("email", "users")
235    }
236
237    // ── Column basics ──
238
239    #[test]
240    fn column_name_and_table() {
241        let col = col_age();
242        assert_eq!(col.name(), "age");
243        assert_eq!(col.table(), "users");
244    }
245
246    #[test]
247    fn column_into_any() {
248        let any = col_age().into_any();
249        assert_eq!(any.name(), "age");
250        assert_eq!(any.table(), "users");
251    }
252
253    // ── Comparison operators ──
254
255    #[test]
256    fn column_eq() {
257        let expr = col_age().eq(25);
258        match expr {
259            Expr::Eq(col, Value::Int(25)) => {
260                assert_eq!(col.name(), "age");
261            }
262            _ => panic!("Expected Eq with Int(25)"),
263        }
264    }
265
266    #[test]
267    fn column_ne() {
268        let expr = col_age().ne(0);
269        assert!(matches!(expr, Expr::Ne(_, Value::Int(0))));
270    }
271
272    #[test]
273    fn column_gt() {
274        let expr = col_age().gt(25);
275        assert!(matches!(expr, Expr::Gt(_, Value::Int(25))));
276    }
277
278    #[test]
279    fn column_gte() {
280        let expr = col_age().gte(18);
281        assert!(matches!(expr, Expr::Gte(_, Value::Int(18))));
282    }
283
284    #[test]
285    fn column_lt() {
286        let expr = col_age().lt(65);
287        assert!(matches!(expr, Expr::Lt(_, Value::Int(65))));
288    }
289
290    #[test]
291    fn column_lte() {
292        let expr = col_age().lte(100);
293        assert!(matches!(expr, Expr::Lte(_, Value::Int(100))));
294    }
295
296    // ── Range ──
297
298    #[test]
299    fn column_between() {
300        let expr = col_age().between(18, 65);
301        match expr {
302            Expr::Between(col, Value::Int(18), Value::Int(65)) => {
303                assert_eq!(col.name(), "age");
304            }
305            _ => panic!("Expected Between(18, 65)"),
306        }
307    }
308
309    #[test]
310    fn column_in_list() {
311        let expr = col_id().in_list(vec![1, 2, 3]);
312        match expr {
313            Expr::InList(col, vals) => {
314                assert_eq!(col.name(), "id");
315                assert_eq!(vals.len(), 3);
316                assert_eq!(vals[0], Value::Int(1));
317                assert_eq!(vals[1], Value::Int(2));
318                assert_eq!(vals[2], Value::Int(3));
319            }
320            _ => panic!("Expected InList"),
321        }
322    }
323
324    #[test]
325    fn column_in_list_empty() {
326        let expr = col_id().in_list(Vec::<i32>::new());
327        match expr {
328            Expr::InList(_, vals) => assert!(vals.is_empty()),
329            _ => panic!("Expected InList"),
330        }
331    }
332
333    #[test]
334    fn column_not_in() {
335        let expr = col_id().not_in(vec![4, 5]);
336        match expr {
337            Expr::NotIn(col, vals) => {
338                assert_eq!(col.name(), "id");
339                assert_eq!(vals.len(), 2);
340            }
341            _ => panic!("Expected NotIn"),
342        }
343    }
344
345    // ── String operators ──
346
347    #[test]
348    fn column_like() {
349        let expr = col_name().like("A%");
350        match expr {
351            Expr::Like(col, Value::String(ref s)) => {
352                assert_eq!(col.name(), "name");
353                assert_eq!(s, "A%");
354            }
355            _ => panic!("Expected Like"),
356        }
357    }
358
359    #[test]
360    fn column_ilike() {
361        let expr = col_name().ilike("alice%");
362        assert!(matches!(expr, Expr::ILike(_, _)));
363    }
364
365    #[test]
366    fn column_contains() {
367        let expr = col_name().contains("foo");
368        match expr {
369            Expr::Like(_, Value::String(ref s)) => {
370                assert_eq!(s, "%foo%");
371            }
372            _ => panic!("Expected Like with %foo%"),
373        }
374    }
375
376    #[test]
377    fn column_starts_with() {
378        let expr = col_name().starts_with("A");
379        match expr {
380            Expr::Like(_, Value::String(ref s)) => {
381                assert_eq!(s, "A%");
382            }
383            _ => panic!("Expected Like with A%"),
384        }
385    }
386
387    #[test]
388    fn column_ends_with() {
389        let expr = col_name().ends_with("z");
390        match expr {
391            Expr::Like(_, Value::String(ref s)) => {
392                assert_eq!(s, "%z");
393            }
394            _ => panic!("Expected Like with %z"),
395        }
396    }
397
398    // ── Null operators ──
399
400    #[test]
401    fn column_is_null() {
402        let expr = col_email().is_null();
403        match expr {
404            Expr::IsNull(col) => assert_eq!(col.name(), "email"),
405            _ => panic!("Expected IsNull"),
406        }
407    }
408
409    #[test]
410    fn column_is_not_null() {
411        let expr = col_email().is_not_null();
412        match expr {
413            Expr::IsNotNull(col) => assert_eq!(col.name(), "email"),
414            _ => panic!("Expected IsNotNull"),
415        }
416    }
417
418    // ── Ordering ──
419
420    #[test]
421    fn column_asc() {
422        let o = col_name().asc();
423        assert_eq!(o.column.name(), "name");
424        assert_eq!(o.direction, OrderDirection::Asc);
425    }
426
427    #[test]
428    fn column_desc() {
429        let o = col_age().desc();
430        assert_eq!(o.direction, OrderDirection::Desc);
431    }
432
433    #[test]
434    fn column_asc_nulls_last() {
435        let o = col_age().asc_nulls_last();
436        assert_eq!(o.direction, OrderDirection::AscNullsLast);
437    }
438
439    #[test]
440    fn column_desc_nulls_first() {
441        let o = col_age().desc_nulls_first();
442        assert_eq!(o.direction, OrderDirection::DescNullsFirst);
443    }
444
445    // ── Chaining ──
446
447    #[test]
448    fn column_chain_and() {
449        let expr = col_age().gt(25).and(col_name().eq("Alice"));
450        assert!(matches!(expr, Expr::And(_, _)));
451    }
452
453    #[test]
454    fn column_chain_or() {
455        let expr = col_age().lt(18).or(col_age().gt(65));
456        assert!(matches!(expr, Expr::Or(_, _)));
457    }
458
459    #[test]
460    fn column_chain_not() {
461        let expr = col_email().is_null().not();
462        assert!(matches!(expr, Expr::Not(_)));
463    }
464
465    #[test]
466    fn column_complex_filter() {
467        // (age BETWEEN 18 AND 65) AND (email IS NOT NULL) OR (name = 'admin')
468        let expr = col_age()
469            .between(18, 65)
470            .and(col_email().is_not_null())
471            .or(col_name().eq("admin"));
472
473        // Should be: Or(And(Between, IsNotNull), Eq)
474        match expr {
475            Expr::Or(left, right) => {
476                assert!(matches!(*left, Expr::And(_, _)));
477                assert!(matches!(*right, Expr::Eq(_, _)));
478            }
479            _ => panic!("Expected Or(And(...), Eq(...))"),
480        }
481    }
482
483    // ── Type safety: different column types ──
484
485    #[test]
486    fn column_string_eq() {
487        let expr = col_name().eq("Alice");
488        match expr {
489            Expr::Eq(_, Value::String(ref s)) => assert_eq!(s, "Alice"),
490            _ => panic!("Expected Eq with String"),
491        }
492    }
493
494    #[test]
495    fn column_i16() {
496        let col: Column<i16> = Column::new("age", "users");
497        let expr = col.eq(25i16);
498        assert!(matches!(expr, Expr::Eq(_, Value::Short(25))));
499    }
500
501    #[test]
502    fn column_i64() {
503        let col: Column<i64> = Column::new("big_id", "users");
504        let expr = col.eq(999i64);
505        assert!(matches!(expr, Expr::Eq(_, Value::BigInt(999))));
506    }
507
508    #[test]
509    fn column_bool() {
510        let col: Column<bool> = Column::new("is_active", "users");
511        let expr = col.eq(true);
512        assert!(matches!(expr, Expr::Eq(_, Value::Bool(true))));
513    }
514}