Skip to main content

umbral_core/orm/
column.rs

1//! Column types used in QuerySet predicates and ordering.
2//!
3//! Each column type carries inherent methods like `.eq`, `.ne`, `.lt`,
4//! `.like`, `.is_null`, etc. that build `Predicate<T>` values, plus
5//! `.asc()` and `.desc()` that build `OrderExpr<T>` values. The model
6//! type parameter `T` ties the column to its parent model so a column
7//! from `Post` can't be passed to a `QuerySet<Comment>`.
8//!
9//! M1 covers four column kinds (`IntCol`, `StrCol`, `DateTimeCol`,
10//! `NullableDateTimeCol`). More land at M2 when the `Model` trait
11//! abstraction goes in.
12//!
13//! The struct shapes and `::new` constructors are fixed (the sibling
14//! `post` module references them). Inherent method implementations
15//! were filled in by the M1 ORM fan-out subagent.
16
17use std::marker::PhantomData;
18
19use sea_query::{Alias, Expr, ExprTrait, Func};
20
21use super::{OrderExpr, Predicate};
22
23/// An i64-typed column.
24pub struct IntCol<T> {
25    pub(crate) name: &'static str,
26    _phantom: PhantomData<T>,
27}
28
29impl<T> IntCol<T> {
30    pub const fn new(name: &'static str) -> Self {
31        Self {
32            name,
33            _phantom: PhantomData,
34        }
35    }
36
37    /// SQL `=`.
38    ///
39    /// # Examples
40    ///
41    /// ```
42    /// use umbral_core::orm::Post;
43    /// use umbral_core::orm::post::post;
44    ///
45    /// // Build a predicate for `id = 2` and pass it to filter.
46    /// let _ = Post::objects().filter(post::ID.eq(2));
47    /// ```
48    pub fn eq(&self, val: i64) -> Predicate<T> {
49        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
50    }
51
52    /// SQL `<>`.
53    pub fn ne(&self, val: i64) -> Predicate<T> {
54        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
55    }
56
57    /// SQL `<`.
58    pub fn lt(&self, val: i64) -> Predicate<T> {
59        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
60    }
61
62    /// SQL `<=`.
63    pub fn le(&self, val: i64) -> Predicate<T> {
64        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
65    }
66
67    /// Lookup-style alias for [`Self::le`]. Matches the REST filter
68    /// parser's `__lte` lookup name so handler-side code and URL
69    /// filters spell the same operation the same way.
70    pub fn lte(&self, val: i64) -> Predicate<T> {
71        self.le(val)
72    }
73
74    /// SQL `>`.
75    pub fn gt(&self, val: i64) -> Predicate<T> {
76        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
77    }
78
79    /// SQL `>=`.
80    pub fn ge(&self, val: i64) -> Predicate<T> {
81        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
82    }
83
84    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
85    /// filter strings.
86    pub fn gte(&self, val: i64) -> Predicate<T> {
87        self.ge(val)
88    }
89
90    /// SQL `IN (...)`.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use umbral_core::orm::Post;
96    /// use umbral_core::orm::post::post;
97    ///
98    /// let _ = Post::objects().filter(post::ID.in_(&[1, 2, 3]));
99    /// ```
100    pub fn in_(&self, vals: &[i64]) -> Predicate<T> {
101        Predicate::new(Expr::col(Alias::new(self.name)).is_in(vals.iter().copied()))
102    }
103
104    /// SQL `<col> IN (SELECT ...)` against a [`super::Subquery`]
105    /// built from another QuerySet via `.into_subquery("col")`
106    /// (gap #26).
107    pub fn in_subquery(&self, sub: super::Subquery) -> Predicate<T> {
108        Predicate::new(Expr::col(Alias::new(self.name)).in_subquery(sub.into_statement()))
109    }
110
111    /// SQL `ORDER BY ... ASC`.
112    pub fn asc(&self) -> OrderExpr<T> {
113        OrderExpr::new(self.name, false)
114    }
115
116    /// SQL `ORDER BY ... DESC`.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use umbral_core::orm::Post;
122    /// use umbral_core::orm::post::post;
123    ///
124    /// // Newest posts first, capped at 20 rows.
125    /// let _ = Post::objects().order_by(post::ID.desc()).limit(20);
126    /// ```
127    pub fn desc(&self) -> OrderExpr<T> {
128        OrderExpr::new(self.name, true)
129    }
130}
131
132/// A String-typed column.
133pub struct StrCol<T> {
134    pub(crate) name: &'static str,
135    _phantom: PhantomData<T>,
136}
137
138impl<T> StrCol<T> {
139    pub const fn new(name: &'static str) -> Self {
140        Self {
141            name,
142            _phantom: PhantomData,
143        }
144    }
145
146    /// SQL `=`.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use umbral_core::orm::Post;
152    /// use umbral_core::orm::post::post;
153    ///
154    /// let _ = Post::objects().filter(post::TITLE.eq("Hello world"));
155    /// ```
156    pub fn eq<S: Into<String>>(&self, val: S) -> Predicate<T> {
157        Predicate::new(Expr::col(Alias::new(self.name)).eq(val.into()))
158    }
159
160    /// SQL `<>`.
161    pub fn ne<S: Into<String>>(&self, val: S) -> Predicate<T> {
162        Predicate::new(Expr::col(Alias::new(self.name)).ne(val.into()))
163    }
164
165    /// SQL `LIKE` (case-sensitive).
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// use umbral_core::orm::Post;
171    /// use umbral_core::orm::post::post;
172    ///
173    /// let _ = Post::objects().filter(post::TITLE.like("Hello%"));
174    /// ```
175    pub fn like<S: Into<String>>(&self, pattern: S) -> Predicate<T> {
176        Predicate::new(Expr::col(Alias::new(self.name)).like(pattern.into()))
177    }
178
179    /// Case-insensitive `LIKE` via `UPPER(col) LIKE UPPER(pattern)` for
180    /// portability across backends without a native `ILIKE`.
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use umbral_core::orm::Post;
186    /// use umbral_core::orm::post::post;
187    ///
188    /// let _ = Post::objects().filter(post::TITLE.ilike("hello%"));
189    /// ```
190    pub fn ilike<S: Into<String>>(&self, pattern: S) -> Predicate<T> {
191        let pattern = pattern.into().to_uppercase();
192        Predicate::new(Expr::expr(Func::upper(Expr::col(Alias::new(self.name)))).like(pattern))
193    }
194
195    /// SQL `LIKE '%val%'` substring containment.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use umbral_core::orm::Post;
201    /// use umbral_core::orm::post::post;
202    ///
203    /// let _ = Post::objects().filter(post::TITLE.contains("rust"));
204    /// ```
205    pub fn contains<S: Into<String>>(&self, substring: S) -> Predicate<T> {
206        let pattern = format!("%{}%", super::escape_like_literal(&substring.into()));
207        Predicate::new(
208            Expr::col(Alias::new(self.name)).like(sea_query::LikeExpr::new(pattern).escape('\\')),
209        )
210    }
211
212    /// Case-insensitive substring containment via `UPPER(col) LIKE
213    /// UPPER('%val%')`.
214    ///
215    /// SQLite's `LIKE` is already ASCII-case-insensitive, so `contains`
216    /// and `icontains` may return the same rows there. The contract is
217    /// "emit `LIKE`"; backend case-sensitivity differs.
218    ///
219    /// # Examples
220    ///
221    /// ```
222    /// use umbral_core::orm::Post;
223    /// use umbral_core::orm::post::post;
224    ///
225    /// let _ = Post::objects().filter(post::TITLE.icontains("rust"));
226    /// ```
227    pub fn icontains<S: Into<String>>(&self, substring: S) -> Predicate<T> {
228        let pattern = format!("%{}%", super::escape_like_literal(&substring.into())).to_uppercase();
229        Predicate::new(
230            Expr::expr(Func::upper(Expr::col(Alias::new(self.name))))
231                .like(sea_query::LikeExpr::new(pattern).escape('\\')),
232        )
233    }
234
235    /// SQL `LIKE 'val%'` — prefix match. Mirrors the REST filter
236    /// parser's `__startswith` lookup.
237    ///
238    /// # Examples
239    ///
240    /// ```
241    /// use umbral_core::orm::Post;
242    /// use umbral_core::orm::post::post;
243    ///
244    /// let _ = Post::objects().filter(post::TITLE.startswith("intro"));
245    /// ```
246    pub fn startswith<S: Into<String>>(&self, prefix: S) -> Predicate<T> {
247        let pattern = format!("{}%", super::escape_like_literal(&prefix.into()));
248        Predicate::new(
249            Expr::col(Alias::new(self.name)).like(sea_query::LikeExpr::new(pattern).escape('\\')),
250        )
251    }
252
253    /// Case-insensitive prefix match via `UPPER(col) LIKE UPPER('val%')`.
254    pub fn istartswith<S: Into<String>>(&self, prefix: S) -> Predicate<T> {
255        let pattern = format!("{}%", super::escape_like_literal(&prefix.into())).to_uppercase();
256        Predicate::new(
257            Expr::expr(Func::upper(Expr::col(Alias::new(self.name))))
258                .like(sea_query::LikeExpr::new(pattern).escape('\\')),
259        )
260    }
261
262    /// SQL `ORDER BY ... ASC`.
263    pub fn asc(&self) -> OrderExpr<T> {
264        OrderExpr::new(self.name, false)
265    }
266
267    /// SQL `ORDER BY ... DESC`.
268    pub fn desc(&self) -> OrderExpr<T> {
269        OrderExpr::new(self.name, true)
270    }
271}
272
273/// A `chrono::DateTime<Utc>`-typed column.
274pub struct DateTimeCol<T> {
275    pub(crate) name: &'static str,
276    _phantom: PhantomData<T>,
277}
278
279impl<T> DateTimeCol<T> {
280    pub const fn new(name: &'static str) -> Self {
281        Self {
282            name,
283            _phantom: PhantomData,
284        }
285    }
286
287    /// SQL `=`.
288    pub fn eq(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
289        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
290    }
291
292    /// SQL `<>`.
293    pub fn ne(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
294        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
295    }
296
297    /// SQL `<`.
298    pub fn lt(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
299        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
300    }
301
302    /// SQL `<=`.
303    pub fn le(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
304        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
305    }
306
307    /// Lookup-style alias for [`Self::le`]. Same as `__lte` in URL
308    /// filter strings.
309    pub fn lte(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
310        self.le(val)
311    }
312
313    /// SQL `>`.
314    pub fn gt(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
315        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
316    }
317
318    /// SQL `>=`.
319    pub fn ge(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
320        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
321    }
322
323    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
324    /// filter strings.
325    pub fn gte(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
326        self.ge(val)
327    }
328
329    /// Alias for `.lt`, reading naturally for time.
330    pub fn before(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
331        self.lt(val)
332    }
333
334    /// Alias for `.gt`, reading naturally for time.
335    pub fn after(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
336        self.gt(val)
337    }
338
339    /// SQL `ORDER BY ... ASC`.
340    pub fn asc(&self) -> OrderExpr<T> {
341        OrderExpr::new(self.name, false)
342    }
343
344    /// SQL `ORDER BY ... DESC`.
345    pub fn desc(&self) -> OrderExpr<T> {
346        OrderExpr::new(self.name, true)
347    }
348}
349
350/// A nullable `chrono::DateTime<Utc>`-typed column.
351pub struct NullableDateTimeCol<T> {
352    pub(crate) name: &'static str,
353    _phantom: PhantomData<T>,
354}
355
356impl<T> NullableDateTimeCol<T> {
357    pub const fn new(name: &'static str) -> Self {
358        Self {
359            name,
360            _phantom: PhantomData,
361        }
362    }
363
364    /// SQL `=`. NULL rows are excluded by SQL's NULL semantics.
365    pub fn eq(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
366        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
367    }
368
369    /// SQL `<>`. NULL rows are excluded by SQL's NULL semantics.
370    pub fn ne(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
371        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
372    }
373
374    /// SQL `<`.
375    pub fn lt(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
376        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
377    }
378
379    /// SQL `<=`.
380    pub fn le(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
381        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
382    }
383
384    /// Lookup-style alias for [`Self::le`]. Same as `__lte` in URL
385    /// filter strings.
386    pub fn lte(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
387        self.le(val)
388    }
389
390    /// SQL `>`.
391    pub fn gt(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
392        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
393    }
394
395    /// SQL `>=`.
396    pub fn ge(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
397        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
398    }
399
400    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
401    /// filter strings.
402    pub fn gte(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
403        self.ge(val)
404    }
405
406    /// Alias for `.lt`, reading naturally for time.
407    ///
408    /// # Examples
409    ///
410    /// ```
411    /// use chrono::Utc;
412    /// use umbral_core::orm::Post;
413    /// use umbral_core::orm::post::post;
414    ///
415    /// let now = Utc::now();
416    /// let _ = Post::objects().filter(post::PUBLISHED_AT.before(now));
417    /// ```
418    pub fn before(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
419        self.lt(val)
420    }
421
422    /// Alias for `.gt`, reading naturally for time.
423    pub fn after(&self, val: chrono::DateTime<chrono::Utc>) -> Predicate<T> {
424        self.gt(val)
425    }
426
427    /// SQL `IS NULL`.
428    ///
429    /// # Examples
430    ///
431    /// ```
432    /// use umbral_core::orm::Post;
433    /// use umbral_core::orm::post::post;
434    ///
435    /// // Drafts: rows where `published_at` has not been set.
436    /// let _ = Post::objects().filter(post::PUBLISHED_AT.is_null());
437    /// ```
438    pub fn is_null(&self) -> Predicate<T> {
439        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
440    }
441
442    /// SQL `IS NOT NULL`.
443    ///
444    /// # Examples
445    ///
446    /// ```
447    /// use umbral_core::orm::Post;
448    /// use umbral_core::orm::post::post;
449    ///
450    /// // Published posts only.
451    /// let _ = Post::objects().filter(post::PUBLISHED_AT.is_not_null());
452    ///
453    /// // Compose with `&` for AND: published posts mentioning "rust".
454    /// let _ = Post::objects()
455    ///     .filter(post::PUBLISHED_AT.is_not_null() & post::TITLE.icontains("rust"));
456    /// ```
457    pub fn is_not_null(&self) -> Predicate<T> {
458        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
459    }
460
461    /// SQL `ORDER BY ... ASC`.
462    pub fn asc(&self) -> OrderExpr<T> {
463        OrderExpr::new(self.name, false)
464    }
465
466    /// SQL `ORDER BY ... DESC`.
467    pub fn desc(&self) -> OrderExpr<T> {
468        OrderExpr::new(self.name, true)
469    }
470}
471
472// =========================================================================
473//
474// M3 type-catalogue refresh: stubs added by the scaffold commit; methods
475// filled in by the M3 type-catalogue fan-out subagent A.
476//
477// Convention for the new types: a struct with `name: &'static str` plus
478// `PhantomData<T>`, and a const `::new(&'static str)` constructor.
479// Methods (.eq / .ne / .lt / .gt / .le / .ge / .is_null / .is_not_null /
480// .asc / .desc / .before / .after / etc.) get added by subagent A so the
481// stubs compile cleanly during the parallel phase.
482//
483// =========================================================================
484
485/// A 64-bit float column (`f64`). Also serves `f32` field declarations
486/// because `f32 -> f64` is lossless; the SqlType variant on FieldSpec
487/// keeps the original precision distinction (`Real` vs `Double`) so
488/// the migration engine renders the right SQL column type.
489pub struct F64Col<T> {
490    pub(crate) name: &'static str,
491    _phantom: PhantomData<T>,
492}
493
494impl<T> F64Col<T> {
495    pub const fn new(name: &'static str) -> Self {
496        Self {
497            name,
498            _phantom: PhantomData,
499        }
500    }
501
502    /// SQL `=`.
503    pub fn eq(&self, val: f64) -> Predicate<T> {
504        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
505    }
506
507    /// SQL `<>`.
508    pub fn ne(&self, val: f64) -> Predicate<T> {
509        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
510    }
511
512    /// SQL `<`.
513    pub fn lt(&self, val: f64) -> Predicate<T> {
514        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
515    }
516
517    /// SQL `<=`.
518    pub fn le(&self, val: f64) -> Predicate<T> {
519        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
520    }
521
522    /// Lookup-style alias for [`Self::le`]. Same as `__lte` in URL
523    /// filter strings.
524    pub fn lte(&self, val: f64) -> Predicate<T> {
525        self.le(val)
526    }
527
528    /// SQL `>`.
529    pub fn gt(&self, val: f64) -> Predicate<T> {
530        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
531    }
532
533    /// SQL `>=`.
534    pub fn ge(&self, val: f64) -> Predicate<T> {
535        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
536    }
537
538    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
539    /// filter strings.
540    pub fn gte(&self, val: f64) -> Predicate<T> {
541        self.ge(val)
542    }
543
544    /// SQL `ORDER BY ... ASC`.
545    pub fn asc(&self) -> OrderExpr<T> {
546        OrderExpr::new(self.name, false)
547    }
548
549    /// SQL `ORDER BY ... DESC`.
550    pub fn desc(&self) -> OrderExpr<T> {
551        OrderExpr::new(self.name, true)
552    }
553}
554
555/// A boolean column.
556pub struct BoolCol<T> {
557    pub(crate) name: &'static str,
558    _phantom: PhantomData<T>,
559}
560
561impl<T> BoolCol<T> {
562    pub const fn new(name: &'static str) -> Self {
563        Self {
564            name,
565            _phantom: PhantomData,
566        }
567    }
568
569    /// SQL `=`.
570    pub fn eq(&self, val: bool) -> Predicate<T> {
571        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
572    }
573
574    /// SQL `<>`.
575    pub fn ne(&self, val: bool) -> Predicate<T> {
576        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
577    }
578
579    /// Sugar for `.eq(true)`.
580    pub fn is_true(&self) -> Predicate<T> {
581        self.eq(true)
582    }
583
584    /// Sugar for `.eq(false)`.
585    pub fn is_false(&self) -> Predicate<T> {
586        self.eq(false)
587    }
588
589    /// SQL `ORDER BY ... ASC`.
590    pub fn asc(&self) -> OrderExpr<T> {
591        OrderExpr::new(self.name, false)
592    }
593
594    /// SQL `ORDER BY ... DESC`.
595    pub fn desc(&self) -> OrderExpr<T> {
596        OrderExpr::new(self.name, true)
597    }
598}
599
600/// A `uuid::Uuid`-typed column.
601pub struct UuidCol<T> {
602    pub(crate) name: &'static str,
603    _phantom: PhantomData<T>,
604}
605
606impl<T> UuidCol<T> {
607    pub const fn new(name: &'static str) -> Self {
608        Self {
609            name,
610            _phantom: PhantomData,
611        }
612    }
613
614    /// SQL `=`.
615    pub fn eq(&self, val: uuid::Uuid) -> Predicate<T> {
616        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
617    }
618
619    /// SQL `<>`.
620    pub fn ne(&self, val: uuid::Uuid) -> Predicate<T> {
621        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
622    }
623
624    /// SQL `IN (...)`.
625    pub fn in_(&self, vals: &[uuid::Uuid]) -> Predicate<T> {
626        Predicate::new(Expr::col(Alias::new(self.name)).is_in(vals.iter().copied()))
627    }
628
629    /// SQL `ORDER BY ... ASC`.
630    pub fn asc(&self) -> OrderExpr<T> {
631        OrderExpr::new(self.name, false)
632    }
633
634    /// SQL `ORDER BY ... DESC`.
635    pub fn desc(&self) -> OrderExpr<T> {
636        OrderExpr::new(self.name, true)
637    }
638}
639
640/// A `chrono::NaiveDate`-typed column (no time, no timezone).
641pub struct DateCol<T> {
642    pub(crate) name: &'static str,
643    _phantom: PhantomData<T>,
644}
645
646impl<T> DateCol<T> {
647    pub const fn new(name: &'static str) -> Self {
648        Self {
649            name,
650            _phantom: PhantomData,
651        }
652    }
653
654    /// SQL `=`.
655    pub fn eq(&self, val: chrono::NaiveDate) -> Predicate<T> {
656        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
657    }
658
659    /// SQL `<>`.
660    pub fn ne(&self, val: chrono::NaiveDate) -> Predicate<T> {
661        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
662    }
663
664    /// SQL `<`.
665    pub fn lt(&self, val: chrono::NaiveDate) -> Predicate<T> {
666        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
667    }
668
669    /// SQL `<=`.
670    pub fn le(&self, val: chrono::NaiveDate) -> Predicate<T> {
671        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
672    }
673
674    /// Lookup-style alias for [`Self::le`]. Same as `__lte` in URL
675    /// filter strings.
676    pub fn lte(&self, val: chrono::NaiveDate) -> Predicate<T> {
677        self.le(val)
678    }
679
680    /// SQL `>`.
681    pub fn gt(&self, val: chrono::NaiveDate) -> Predicate<T> {
682        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
683    }
684
685    /// SQL `>=`.
686    pub fn ge(&self, val: chrono::NaiveDate) -> Predicate<T> {
687        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
688    }
689
690    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
691    /// filter strings.
692    pub fn gte(&self, val: chrono::NaiveDate) -> Predicate<T> {
693        self.ge(val)
694    }
695
696    /// Alias for `.lt`, reading naturally for dates.
697    pub fn before(&self, val: chrono::NaiveDate) -> Predicate<T> {
698        self.lt(val)
699    }
700
701    /// Alias for `.gt`, reading naturally for dates.
702    pub fn after(&self, val: chrono::NaiveDate) -> Predicate<T> {
703        self.gt(val)
704    }
705
706    /// SQL `ORDER BY ... ASC`.
707    pub fn asc(&self) -> OrderExpr<T> {
708        OrderExpr::new(self.name, false)
709    }
710
711    /// SQL `ORDER BY ... DESC`.
712    pub fn desc(&self) -> OrderExpr<T> {
713        OrderExpr::new(self.name, true)
714    }
715}
716
717/// A `chrono::NaiveTime`-typed column (no date, no timezone).
718pub struct TimeCol<T> {
719    pub(crate) name: &'static str,
720    _phantom: PhantomData<T>,
721}
722
723impl<T> TimeCol<T> {
724    pub const fn new(name: &'static str) -> Self {
725        Self {
726            name,
727            _phantom: PhantomData,
728        }
729    }
730
731    /// SQL `=`.
732    pub fn eq(&self, val: chrono::NaiveTime) -> Predicate<T> {
733        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
734    }
735
736    /// SQL `<>`.
737    pub fn ne(&self, val: chrono::NaiveTime) -> Predicate<T> {
738        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
739    }
740
741    /// SQL `<`.
742    pub fn lt(&self, val: chrono::NaiveTime) -> Predicate<T> {
743        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
744    }
745
746    /// SQL `<=`.
747    pub fn le(&self, val: chrono::NaiveTime) -> Predicate<T> {
748        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
749    }
750
751    /// Lookup-style alias for [`Self::le`]. Same as `__lte` in URL
752    /// filter strings.
753    pub fn lte(&self, val: chrono::NaiveTime) -> Predicate<T> {
754        self.le(val)
755    }
756
757    /// SQL `>`.
758    pub fn gt(&self, val: chrono::NaiveTime) -> Predicate<T> {
759        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
760    }
761
762    /// SQL `>=`.
763    pub fn ge(&self, val: chrono::NaiveTime) -> Predicate<T> {
764        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
765    }
766
767    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
768    /// filter strings.
769    pub fn gte(&self, val: chrono::NaiveTime) -> Predicate<T> {
770        self.ge(val)
771    }
772
773    /// Alias for `.lt`, reading naturally for times.
774    pub fn before(&self, val: chrono::NaiveTime) -> Predicate<T> {
775        self.lt(val)
776    }
777
778    /// Alias for `.gt`, reading naturally for times.
779    pub fn after(&self, val: chrono::NaiveTime) -> Predicate<T> {
780        self.gt(val)
781    }
782
783    /// SQL `ORDER BY ... ASC`.
784    pub fn asc(&self) -> OrderExpr<T> {
785        OrderExpr::new(self.name, false)
786    }
787
788    /// SQL `ORDER BY ... DESC`.
789    pub fn desc(&self) -> OrderExpr<T> {
790        OrderExpr::new(self.name, true)
791    }
792}
793
794// -------------------------------------------------------------------------
795// Nullable variants. Each wraps a base type and adds `.is_null` /
796// `.is_not_null`; otherwise the same predicates apply with the same
797// signatures. The derive emits these for `Option<T>` fields across the
798// catalogue.
799// -------------------------------------------------------------------------
800
801/// A nullable `i64`-typed column.
802pub struct NullableIntCol<T> {
803    pub(crate) name: &'static str,
804    _phantom: PhantomData<T>,
805}
806
807impl<T> NullableIntCol<T> {
808    pub const fn new(name: &'static str) -> Self {
809        Self {
810            name,
811            _phantom: PhantomData,
812        }
813    }
814
815    /// SQL `=`. NULL rows are excluded by SQL's NULL semantics.
816    pub fn eq(&self, val: i64) -> Predicate<T> {
817        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
818    }
819
820    /// SQL `<>`. NULL rows are excluded by SQL's NULL semantics.
821    pub fn ne(&self, val: i64) -> Predicate<T> {
822        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
823    }
824
825    /// SQL `<`.
826    pub fn lt(&self, val: i64) -> Predicate<T> {
827        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
828    }
829
830    /// SQL `<=`.
831    pub fn le(&self, val: i64) -> Predicate<T> {
832        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
833    }
834
835    /// Lookup-style alias for [`Self::le`]. Same as `__lte` in URL
836    /// filter strings.
837    pub fn lte(&self, val: i64) -> Predicate<T> {
838        self.le(val)
839    }
840
841    /// SQL `>`.
842    pub fn gt(&self, val: i64) -> Predicate<T> {
843        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
844    }
845
846    /// SQL `>=`.
847    pub fn ge(&self, val: i64) -> Predicate<T> {
848        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
849    }
850
851    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
852    /// filter strings.
853    pub fn gte(&self, val: i64) -> Predicate<T> {
854        self.ge(val)
855    }
856
857    /// SQL `IN (...)`.
858    pub fn in_(&self, vals: &[i64]) -> Predicate<T> {
859        Predicate::new(Expr::col(Alias::new(self.name)).is_in(vals.iter().copied()))
860    }
861
862    /// SQL `IS NULL`.
863    pub fn is_null(&self) -> Predicate<T> {
864        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
865    }
866
867    /// SQL `IS NOT NULL`.
868    pub fn is_not_null(&self) -> Predicate<T> {
869        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
870    }
871
872    /// SQL `ORDER BY ... ASC`.
873    pub fn asc(&self) -> OrderExpr<T> {
874        OrderExpr::new(self.name, false)
875    }
876
877    /// SQL `ORDER BY ... DESC`.
878    pub fn desc(&self) -> OrderExpr<T> {
879        OrderExpr::new(self.name, true)
880    }
881}
882
883/// A nullable `String`-typed column.
884pub struct NullableStrCol<T> {
885    pub(crate) name: &'static str,
886    _phantom: PhantomData<T>,
887}
888
889impl<T> NullableStrCol<T> {
890    pub const fn new(name: &'static str) -> Self {
891        Self {
892            name,
893            _phantom: PhantomData,
894        }
895    }
896
897    /// SQL `=`. NULL rows are excluded by SQL's NULL semantics.
898    pub fn eq<S: Into<String>>(&self, val: S) -> Predicate<T> {
899        Predicate::new(Expr::col(Alias::new(self.name)).eq(val.into()))
900    }
901
902    /// SQL `<>`. NULL rows are excluded by SQL's NULL semantics.
903    pub fn ne<S: Into<String>>(&self, val: S) -> Predicate<T> {
904        Predicate::new(Expr::col(Alias::new(self.name)).ne(val.into()))
905    }
906
907    /// SQL `LIKE` (case-sensitive).
908    pub fn like<S: Into<String>>(&self, pattern: S) -> Predicate<T> {
909        Predicate::new(Expr::col(Alias::new(self.name)).like(pattern.into()))
910    }
911
912    /// Case-insensitive `LIKE` via `UPPER(col) LIKE UPPER(pattern)`.
913    pub fn ilike<S: Into<String>>(&self, pattern: S) -> Predicate<T> {
914        let pattern = pattern.into().to_uppercase();
915        Predicate::new(Expr::expr(Func::upper(Expr::col(Alias::new(self.name)))).like(pattern))
916    }
917
918    /// SQL `LIKE '%val%'` substring containment.
919    pub fn contains<S: Into<String>>(&self, substring: S) -> Predicate<T> {
920        let pattern = format!("%{}%", super::escape_like_literal(&substring.into()));
921        Predicate::new(
922            Expr::col(Alias::new(self.name)).like(sea_query::LikeExpr::new(pattern).escape('\\')),
923        )
924    }
925
926    /// Case-insensitive substring containment via `UPPER(col) LIKE
927    /// UPPER('%val%')`.
928    pub fn icontains<S: Into<String>>(&self, substring: S) -> Predicate<T> {
929        let pattern = format!("%{}%", super::escape_like_literal(&substring.into())).to_uppercase();
930        Predicate::new(
931            Expr::expr(Func::upper(Expr::col(Alias::new(self.name))))
932                .like(sea_query::LikeExpr::new(pattern).escape('\\')),
933        )
934    }
935
936    /// SQL `LIKE 'val%'` — prefix match. Mirrors the REST filter
937    /// parser's `__startswith` lookup.
938    pub fn startswith<S: Into<String>>(&self, prefix: S) -> Predicate<T> {
939        let pattern = format!("{}%", super::escape_like_literal(&prefix.into()));
940        Predicate::new(
941            Expr::col(Alias::new(self.name)).like(sea_query::LikeExpr::new(pattern).escape('\\')),
942        )
943    }
944
945    /// Case-insensitive prefix match via `UPPER(col) LIKE UPPER('val%')`.
946    pub fn istartswith<S: Into<String>>(&self, prefix: S) -> Predicate<T> {
947        let pattern = format!("{}%", super::escape_like_literal(&prefix.into())).to_uppercase();
948        Predicate::new(
949            Expr::expr(Func::upper(Expr::col(Alias::new(self.name))))
950                .like(sea_query::LikeExpr::new(pattern).escape('\\')),
951        )
952    }
953
954    /// SQL `IS NULL`.
955    pub fn is_null(&self) -> Predicate<T> {
956        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
957    }
958
959    /// SQL `IS NOT NULL`.
960    pub fn is_not_null(&self) -> Predicate<T> {
961        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
962    }
963
964    /// SQL `ORDER BY ... ASC`.
965    pub fn asc(&self) -> OrderExpr<T> {
966        OrderExpr::new(self.name, false)
967    }
968
969    /// SQL `ORDER BY ... DESC`.
970    pub fn desc(&self) -> OrderExpr<T> {
971        OrderExpr::new(self.name, true)
972    }
973}
974
975/// A nullable `f64`-typed column.
976pub struct NullableF64Col<T> {
977    pub(crate) name: &'static str,
978    _phantom: PhantomData<T>,
979}
980
981impl<T> NullableF64Col<T> {
982    pub const fn new(name: &'static str) -> Self {
983        Self {
984            name,
985            _phantom: PhantomData,
986        }
987    }
988
989    /// SQL `=`. NULL rows are excluded by SQL's NULL semantics.
990    pub fn eq(&self, val: f64) -> Predicate<T> {
991        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
992    }
993
994    /// SQL `<>`. NULL rows are excluded by SQL's NULL semantics.
995    pub fn ne(&self, val: f64) -> Predicate<T> {
996        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
997    }
998
999    /// SQL `<`.
1000    pub fn lt(&self, val: f64) -> Predicate<T> {
1001        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
1002    }
1003
1004    /// SQL `<=`.
1005    pub fn le(&self, val: f64) -> Predicate<T> {
1006        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
1007    }
1008
1009    /// Lookup-style alias for [`Self::le`]. Same as `__lte` in URL
1010    /// filter strings.
1011    pub fn lte(&self, val: f64) -> Predicate<T> {
1012        self.le(val)
1013    }
1014
1015    /// SQL `>`.
1016    pub fn gt(&self, val: f64) -> Predicate<T> {
1017        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
1018    }
1019
1020    /// SQL `>=`.
1021    pub fn ge(&self, val: f64) -> Predicate<T> {
1022        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
1023    }
1024
1025    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
1026    /// filter strings.
1027    pub fn gte(&self, val: f64) -> Predicate<T> {
1028        self.ge(val)
1029    }
1030
1031    /// SQL `IS NULL`.
1032    pub fn is_null(&self) -> Predicate<T> {
1033        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
1034    }
1035
1036    /// SQL `IS NOT NULL`.
1037    pub fn is_not_null(&self) -> Predicate<T> {
1038        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
1039    }
1040
1041    /// SQL `ORDER BY ... ASC`.
1042    pub fn asc(&self) -> OrderExpr<T> {
1043        OrderExpr::new(self.name, false)
1044    }
1045
1046    /// SQL `ORDER BY ... DESC`.
1047    pub fn desc(&self) -> OrderExpr<T> {
1048        OrderExpr::new(self.name, true)
1049    }
1050}
1051
1052/// A nullable `bool`-typed column.
1053pub struct NullableBoolCol<T> {
1054    pub(crate) name: &'static str,
1055    _phantom: PhantomData<T>,
1056}
1057
1058impl<T> NullableBoolCol<T> {
1059    pub const fn new(name: &'static str) -> Self {
1060        Self {
1061            name,
1062            _phantom: PhantomData,
1063        }
1064    }
1065
1066    /// SQL `=`. NULL rows are excluded by SQL's NULL semantics.
1067    pub fn eq(&self, val: bool) -> Predicate<T> {
1068        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
1069    }
1070
1071    /// SQL `<>`. NULL rows are excluded by SQL's NULL semantics.
1072    pub fn ne(&self, val: bool) -> Predicate<T> {
1073        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
1074    }
1075
1076    /// Sugar for `.eq(true)`.
1077    pub fn is_true(&self) -> Predicate<T> {
1078        self.eq(true)
1079    }
1080
1081    /// Sugar for `.eq(false)`.
1082    pub fn is_false(&self) -> Predicate<T> {
1083        self.eq(false)
1084    }
1085
1086    /// SQL `IS NULL`.
1087    pub fn is_null(&self) -> Predicate<T> {
1088        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
1089    }
1090
1091    /// SQL `IS NOT NULL`.
1092    pub fn is_not_null(&self) -> Predicate<T> {
1093        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
1094    }
1095
1096    /// SQL `ORDER BY ... ASC`.
1097    pub fn asc(&self) -> OrderExpr<T> {
1098        OrderExpr::new(self.name, false)
1099    }
1100
1101    /// SQL `ORDER BY ... DESC`.
1102    pub fn desc(&self) -> OrderExpr<T> {
1103        OrderExpr::new(self.name, true)
1104    }
1105}
1106
1107/// A nullable `uuid::Uuid`-typed column.
1108pub struct NullableUuidCol<T> {
1109    pub(crate) name: &'static str,
1110    _phantom: PhantomData<T>,
1111}
1112
1113impl<T> NullableUuidCol<T> {
1114    pub const fn new(name: &'static str) -> Self {
1115        Self {
1116            name,
1117            _phantom: PhantomData,
1118        }
1119    }
1120
1121    /// SQL `=`. NULL rows are excluded by SQL's NULL semantics.
1122    pub fn eq(&self, val: uuid::Uuid) -> Predicate<T> {
1123        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
1124    }
1125
1126    /// SQL `<>`. NULL rows are excluded by SQL's NULL semantics.
1127    pub fn ne(&self, val: uuid::Uuid) -> Predicate<T> {
1128        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
1129    }
1130
1131    /// SQL `IN (...)`.
1132    pub fn in_(&self, vals: &[uuid::Uuid]) -> Predicate<T> {
1133        Predicate::new(Expr::col(Alias::new(self.name)).is_in(vals.iter().copied()))
1134    }
1135
1136    /// SQL `IS NULL`.
1137    pub fn is_null(&self) -> Predicate<T> {
1138        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
1139    }
1140
1141    /// SQL `IS NOT NULL`.
1142    pub fn is_not_null(&self) -> Predicate<T> {
1143        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
1144    }
1145
1146    /// SQL `ORDER BY ... ASC`.
1147    pub fn asc(&self) -> OrderExpr<T> {
1148        OrderExpr::new(self.name, false)
1149    }
1150
1151    /// SQL `ORDER BY ... DESC`.
1152    pub fn desc(&self) -> OrderExpr<T> {
1153        OrderExpr::new(self.name, true)
1154    }
1155}
1156
1157/// A nullable `chrono::NaiveDate`-typed column.
1158pub struct NullableDateCol<T> {
1159    pub(crate) name: &'static str,
1160    _phantom: PhantomData<T>,
1161}
1162
1163impl<T> NullableDateCol<T> {
1164    pub const fn new(name: &'static str) -> Self {
1165        Self {
1166            name,
1167            _phantom: PhantomData,
1168        }
1169    }
1170
1171    /// SQL `=`. NULL rows are excluded by SQL's NULL semantics.
1172    pub fn eq(&self, val: chrono::NaiveDate) -> Predicate<T> {
1173        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
1174    }
1175
1176    /// SQL `<>`. NULL rows are excluded by SQL's NULL semantics.
1177    pub fn ne(&self, val: chrono::NaiveDate) -> Predicate<T> {
1178        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
1179    }
1180
1181    /// SQL `<`.
1182    pub fn lt(&self, val: chrono::NaiveDate) -> Predicate<T> {
1183        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
1184    }
1185
1186    /// SQL `<=`.
1187    pub fn le(&self, val: chrono::NaiveDate) -> Predicate<T> {
1188        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
1189    }
1190
1191    /// Lookup-style alias for [`Self::le`]. Same as `__lte` in URL
1192    /// filter strings.
1193    pub fn lte(&self, val: chrono::NaiveDate) -> Predicate<T> {
1194        self.le(val)
1195    }
1196
1197    /// SQL `>`.
1198    pub fn gt(&self, val: chrono::NaiveDate) -> Predicate<T> {
1199        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
1200    }
1201
1202    /// SQL `>=`.
1203    pub fn ge(&self, val: chrono::NaiveDate) -> Predicate<T> {
1204        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
1205    }
1206
1207    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
1208    /// filter strings.
1209    pub fn gte(&self, val: chrono::NaiveDate) -> Predicate<T> {
1210        self.ge(val)
1211    }
1212
1213    /// Alias for `.lt`, reading naturally for dates.
1214    pub fn before(&self, val: chrono::NaiveDate) -> Predicate<T> {
1215        self.lt(val)
1216    }
1217
1218    /// Alias for `.gt`, reading naturally for dates.
1219    pub fn after(&self, val: chrono::NaiveDate) -> Predicate<T> {
1220        self.gt(val)
1221    }
1222
1223    /// SQL `IS NULL`.
1224    pub fn is_null(&self) -> Predicate<T> {
1225        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
1226    }
1227
1228    /// SQL `IS NOT NULL`.
1229    pub fn is_not_null(&self) -> Predicate<T> {
1230        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
1231    }
1232
1233    /// SQL `ORDER BY ... ASC`.
1234    pub fn asc(&self) -> OrderExpr<T> {
1235        OrderExpr::new(self.name, false)
1236    }
1237
1238    /// SQL `ORDER BY ... DESC`.
1239    pub fn desc(&self) -> OrderExpr<T> {
1240        OrderExpr::new(self.name, true)
1241    }
1242}
1243
1244/// A nullable `chrono::NaiveTime`-typed column.
1245pub struct NullableTimeCol<T> {
1246    pub(crate) name: &'static str,
1247    _phantom: PhantomData<T>,
1248}
1249
1250impl<T> NullableTimeCol<T> {
1251    pub const fn new(name: &'static str) -> Self {
1252        Self {
1253            name,
1254            _phantom: PhantomData,
1255        }
1256    }
1257
1258    /// SQL `=`. NULL rows are excluded by SQL's NULL semantics.
1259    pub fn eq(&self, val: chrono::NaiveTime) -> Predicate<T> {
1260        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
1261    }
1262
1263    /// SQL `<>`. NULL rows are excluded by SQL's NULL semantics.
1264    pub fn ne(&self, val: chrono::NaiveTime) -> Predicate<T> {
1265        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
1266    }
1267
1268    /// SQL `<`.
1269    pub fn lt(&self, val: chrono::NaiveTime) -> Predicate<T> {
1270        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
1271    }
1272
1273    /// SQL `<=`.
1274    pub fn le(&self, val: chrono::NaiveTime) -> Predicate<T> {
1275        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
1276    }
1277
1278    /// Lookup-style alias for [`Self::le`]. Same as `__lte` in URL
1279    /// filter strings.
1280    pub fn lte(&self, val: chrono::NaiveTime) -> Predicate<T> {
1281        self.le(val)
1282    }
1283
1284    /// SQL `>`.
1285    pub fn gt(&self, val: chrono::NaiveTime) -> Predicate<T> {
1286        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
1287    }
1288
1289    /// SQL `>=`.
1290    pub fn ge(&self, val: chrono::NaiveTime) -> Predicate<T> {
1291        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
1292    }
1293
1294    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
1295    /// filter strings.
1296    pub fn gte(&self, val: chrono::NaiveTime) -> Predicate<T> {
1297        self.ge(val)
1298    }
1299
1300    /// Alias for `.lt`, reading naturally for times.
1301    pub fn before(&self, val: chrono::NaiveTime) -> Predicate<T> {
1302        self.lt(val)
1303    }
1304
1305    /// Alias for `.gt`, reading naturally for times.
1306    pub fn after(&self, val: chrono::NaiveTime) -> Predicate<T> {
1307        self.gt(val)
1308    }
1309
1310    /// SQL `IS NULL`.
1311    pub fn is_null(&self) -> Predicate<T> {
1312        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
1313    }
1314
1315    /// SQL `IS NOT NULL`.
1316    pub fn is_not_null(&self) -> Predicate<T> {
1317        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
1318    }
1319
1320    /// SQL `ORDER BY ... ASC`.
1321    pub fn asc(&self) -> OrderExpr<T> {
1322        OrderExpr::new(self.name, false)
1323    }
1324
1325    /// SQL `ORDER BY ... DESC`.
1326    pub fn desc(&self) -> OrderExpr<T> {
1327        OrderExpr::new(self.name, true)
1328    }
1329}
1330
1331// =========================================================================
1332// Json columns (`serde_json::Value`).
1333//
1334// The first iteration of Phase 4. JSON value comparison is semantically
1335// non-trivial across backends — Postgres has `=` for jsonb (deep
1336// equality with key-order normalization), SQLite as TEXT compares
1337// strings literally and so depends on how the value was serialized.
1338// To avoid shipping a half-thought comparison story, the first
1339// iteration covers only `IS NULL` / `IS NOT NULL` predicates plus the
1340// usual ordering ops. Equality / containment / path-access operators
1341// land as a follow-on once the cross-backend semantics are pinned.
1342// =========================================================================
1343
1344/// A `serde_json::Value`-typed column.
1345pub struct JsonCol<T> {
1346    pub(crate) name: &'static str,
1347    _phantom: PhantomData<T>,
1348}
1349
1350impl<T> JsonCol<T> {
1351    pub const fn new(name: &'static str) -> Self {
1352        Self {
1353            name,
1354            _phantom: PhantomData,
1355        }
1356    }
1357
1358    /// SQL `ORDER BY ... ASC`. Ordering on JSON values is well-defined
1359    /// per-backend (Postgres has a total order on jsonb; SQLite orders
1360    /// the underlying TEXT). Use sparingly — JSON ordering is rarely
1361    /// what the user means.
1362    pub fn asc(&self) -> OrderExpr<T> {
1363        OrderExpr::new(self.name, false)
1364    }
1365
1366    /// SQL `ORDER BY ... DESC`.
1367    pub fn desc(&self) -> OrderExpr<T> {
1368        OrderExpr::new(self.name, true)
1369    }
1370
1371    /// Extract a JSON path as text. Postgres-only.
1372    ///
1373    /// ```ignore
1374    /// post::METADATA.path_text(&["author", "name"]).eq("alice")
1375    /// ```
1376    ///
1377    /// Renders as `"metadata" -> 'author' ->> 'name' = 'alice'` when
1378    /// the QuerySet is bound to a `PgPool`. The path must have at
1379    /// least one segment; an empty path panics at construction.
1380    ///
1381    /// See [`JsonPathText`] for the chainable surface.
1382    pub fn path_text(&self, keys: &[&str]) -> JsonPathText<T> {
1383        JsonPathText::new(self.name, keys)
1384    }
1385
1386    /// Postgres `"col" ? key` — true when the JSON object has the
1387    /// given top-level key. Returns `Predicate<T>` directly (no
1388    /// chainable form yet — `has_key` is a complete boolean op).
1389    /// The key is single-quoted into the SQL fragment; standard SQL
1390    /// apostrophe escaping is applied.
1391    pub fn has_key(&self, key: &str) -> Predicate<T> {
1392        json_has_key_predicate(self.name, key)
1393    }
1394}
1395
1396/// A nullable `serde_json::Value`-typed column.
1397pub struct NullableJsonCol<T> {
1398    pub(crate) name: &'static str,
1399    _phantom: PhantomData<T>,
1400}
1401
1402impl<T> NullableJsonCol<T> {
1403    pub const fn new(name: &'static str) -> Self {
1404        Self {
1405            name,
1406            _phantom: PhantomData,
1407        }
1408    }
1409
1410    /// SQL `IS NULL`.
1411    pub fn is_null(&self) -> Predicate<T> {
1412        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
1413    }
1414
1415    /// SQL `IS NOT NULL`.
1416    pub fn is_not_null(&self) -> Predicate<T> {
1417        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
1418    }
1419
1420    /// SQL `ORDER BY ... ASC`.
1421    pub fn asc(&self) -> OrderExpr<T> {
1422        OrderExpr::new(self.name, false)
1423    }
1424
1425    /// SQL `ORDER BY ... DESC`.
1426    pub fn desc(&self) -> OrderExpr<T> {
1427        OrderExpr::new(self.name, true)
1428    }
1429
1430    /// See [`JsonCol::path_text`]. NULL columns extract NULL through
1431    /// the operator — SQL's three-valued logic excludes them from
1432    /// equality predicates naturally.
1433    pub fn path_text(&self, keys: &[&str]) -> JsonPathText<T> {
1434        JsonPathText::new(self.name, keys)
1435    }
1436
1437    /// See [`JsonCol::has_key`].
1438    pub fn has_key(&self, key: &str) -> Predicate<T> {
1439        json_has_key_predicate(self.name, key)
1440    }
1441}
1442
1443// =========================================================================
1444// JSON operators — Phase 4.2, Postgres-only.
1445//
1446// `path_text(&["a", "b"])` returns a `JsonPathText<T>` builder that
1447// chains into a predicate via `.eq` / `.ne` / `.is_null` / `.is_not_null`.
1448// `has_key("k")` returns a Predicate<T> directly.
1449//
1450// The SQL templates use `$N` placeholders and resolve correctly only
1451// under PostgresQueryBuilder. `to_sql_pg()` is the right debug entry
1452// for these predicates; `to_sql()` (SQLite builder) leaves `$N` tokens
1453// literal. The user-facing docs and the Phase 4.0 Json field rustdoc
1454// both call out that operators are deferred for SQLite; Phase 4.2.1
1455// is the slot where the SQLite JSON1 fallback lands.
1456// =========================================================================
1457
1458/// An expression that extracts a deeply-nested JSON value as text.
1459/// Produced by [`JsonCol::path_text`] / [`NullableJsonCol::path_text`]
1460/// and consumed by `.eq` / `.ne` / `.is_null` / `.is_not_null` to
1461/// produce a `Predicate<T>`.
1462///
1463/// The extraction renders to Postgres' chained `->` / `->>` operator
1464/// form: a path of length `n` produces `n-1` `->` steps and one final
1465/// `->>` step that returns text. Single-key paths use a single `->>`.
1466/// Empty paths would have nothing to extract — `path_text(&[])` panics
1467/// (constructor-level invariant; an empty path is a programmer bug,
1468/// not a runtime user input).
1469pub struct JsonPathText<T> {
1470    column: &'static str,
1471    /// Path segments, ordered root-to-leaf. Owned strings so the
1472    /// builder can be passed around without lifetime contortions.
1473    path: Vec<String>,
1474    _phantom: PhantomData<T>,
1475}
1476
1477impl<T> JsonPathText<T> {
1478    fn new(column: &'static str, keys: &[&str]) -> Self {
1479        assert!(
1480            !keys.is_empty(),
1481            "umbral::orm::JsonPathText: path must have at least one segment"
1482        );
1483        Self {
1484            column,
1485            path: keys.iter().map(|s| s.to_string()).collect(),
1486            _phantom: PhantomData,
1487        }
1488    }
1489
1490    /// Render the Postgres `"col" -> $1 -> $2 ->> $N` template for a
1491    /// path of length `n`. Returns the SQL string and the path-segment
1492    /// Values (in order). The caller appends comparison fragments and
1493    /// binds additional values.
1494    fn extract_template_pg(&self, base_placeholder: usize) -> (String, Vec<sea_query::Value>) {
1495        let col = self.column.replace('"', "\"\"");
1496        let n = self.path.len();
1497        let mut sql = format!("\"{col}\"");
1498        for i in 1..n {
1499            sql.push_str(&format!(" -> ${}", base_placeholder + i - 1));
1500        }
1501        sql.push_str(&format!(" ->> ${}", base_placeholder + n - 1));
1502        let values: Vec<sea_query::Value> = self
1503            .path
1504            .iter()
1505            .map(|k| sea_query::Value::String(Some(Box::new(k.clone()))))
1506            .collect();
1507        (sql, values)
1508    }
1509
1510    /// Build the SQLite JSON1 path string `$.a.b.c` for the stored
1511    /// path. v1 uses dot-notation; users with quoted keys or array
1512    /// indexes hand-roll the path as the SQLite JSON1 bracket form.
1513    fn sqlite_json_path(&self) -> String {
1514        let mut s = String::from("$");
1515        for seg in &self.path {
1516            s.push('.');
1517            s.push_str(seg);
1518        }
1519        s
1520    }
1521
1522    /// SQL `<extracted> = $val`. Backend-aware:
1523    /// - **Postgres**: `"col" -> 'a' ->> 'b' = $val`
1524    /// - **SQLite**: `json_extract("col", '$.a.b') = ?`
1525    pub fn eq(&self, val: &str) -> Predicate<T> {
1526        let (extract_pg, mut pg_values) = self.extract_template_pg(1);
1527        let pg_placeholder = pg_values.len() + 1;
1528        let pg_sql = format!("{extract_pg} = ${pg_placeholder}");
1529        pg_values.push(sea_query::Value::String(Some(Box::new(val.to_string()))));
1530        let pg_cond = Expr::cust_with_values(&pg_sql, pg_values);
1531
1532        let col = self.column.replace('"', "\"\"");
1533        let sqlite_sql = format!("json_extract(\"{col}\", ?) = ?");
1534        let sqlite_values = vec![
1535            sea_query::Value::String(Some(Box::new(self.sqlite_json_path()))),
1536            sea_query::Value::String(Some(Box::new(val.to_string()))),
1537        ];
1538        let sqlite_cond = Expr::cust_with_values(&sqlite_sql, sqlite_values);
1539
1540        Predicate::new_with_sqlite(pg_cond, sqlite_cond)
1541    }
1542
1543    /// SQL `<extracted> <> $val`. Backend-aware (see [`Self::eq`]).
1544    pub fn ne(&self, val: &str) -> Predicate<T> {
1545        let (extract_pg, mut pg_values) = self.extract_template_pg(1);
1546        let pg_placeholder = pg_values.len() + 1;
1547        let pg_sql = format!("{extract_pg} <> ${pg_placeholder}");
1548        pg_values.push(sea_query::Value::String(Some(Box::new(val.to_string()))));
1549        let pg_cond = Expr::cust_with_values(&pg_sql, pg_values);
1550
1551        let col = self.column.replace('"', "\"\"");
1552        let sqlite_sql = format!("json_extract(\"{col}\", ?) <> ?");
1553        let sqlite_values = vec![
1554            sea_query::Value::String(Some(Box::new(self.sqlite_json_path()))),
1555            sea_query::Value::String(Some(Box::new(val.to_string()))),
1556        ];
1557        let sqlite_cond = Expr::cust_with_values(&sqlite_sql, sqlite_values);
1558
1559        Predicate::new_with_sqlite(pg_cond, sqlite_cond)
1560    }
1561
1562    /// SQL `<extracted> IS NULL`. Backend-aware. Both renderings
1563    /// produce NULL when the column itself is NULL OR the path
1564    /// misses a key.
1565    pub fn is_null(&self) -> Predicate<T> {
1566        let (extract_pg, pg_values) = self.extract_template_pg(1);
1567        let pg_cond = Expr::cust_with_values(format!("{extract_pg} IS NULL"), pg_values);
1568
1569        let col = self.column.replace('"', "\"\"");
1570        let sqlite_sql = format!("json_extract(\"{col}\", ?) IS NULL");
1571        let sqlite_values = vec![sea_query::Value::String(Some(Box::new(
1572            self.sqlite_json_path(),
1573        )))];
1574        let sqlite_cond = Expr::cust_with_values(&sqlite_sql, sqlite_values);
1575
1576        Predicate::new_with_sqlite(pg_cond, sqlite_cond)
1577    }
1578
1579    /// SQL `<extracted> IS NOT NULL`. Backend-aware (see
1580    /// [`Self::is_null`]).
1581    pub fn is_not_null(&self) -> Predicate<T> {
1582        let (extract_pg, pg_values) = self.extract_template_pg(1);
1583        let pg_cond = Expr::cust_with_values(format!("{extract_pg} IS NOT NULL"), pg_values);
1584
1585        let col = self.column.replace('"', "\"\"");
1586        let sqlite_sql = format!("json_extract(\"{col}\", ?) IS NOT NULL");
1587        let sqlite_values = vec![sea_query::Value::String(Some(Box::new(
1588            self.sqlite_json_path(),
1589        )))];
1590        let sqlite_cond = Expr::cust_with_values(&sqlite_sql, sqlite_values);
1591
1592        Predicate::new_with_sqlite(pg_cond, sqlite_cond)
1593    }
1594}
1595
1596/// Build a `"col" ? $1` predicate — Postgres's "has top-level key"
1597/// operator. Shared between JsonCol and NullableJsonCol so both
1598/// expose the same surface. Postgres-only; the `?` token is sea-
1599/// query's positional placeholder for SQLite, so the template uses
1600/// the explicit `?` (which Postgres builder will leave alone, but
1601/// sea-query's `cust_with_values` interprets — that means we can't
1602/// use literal `?` here. We use the `\?` escape or build the SQL
1603/// directly).
1604fn json_has_key_predicate<T>(col: &'static str, key: &str) -> Predicate<T> {
1605    let col_escaped = col.replace('"', "\"\"");
1606    let key_escaped = key.replace('\'', "''");
1607
1608    // Postgres: native `?` has-key operator. sea-query's
1609    // `cust_with_values` uses `?` and `$` as placeholder tokens, so
1610    // we double the `?` to emit a literal one. The key is inline
1611    // single-quoted (no binding).
1612    let pg_sql = format!("\"{col_escaped}\" ?? '{key_escaped}'");
1613    let pg_cond = Expr::cust(&pg_sql);
1614
1615    // SQLite JSON1: there's no native has-key operator. The closest
1616    // semantic match is `json_extract(col, '$.key') IS NOT NULL` —
1617    // true when the key exists with a non-null value, false when
1618    // missing OR explicitly null. The Postgres `?` operator returns
1619    // true on `{"k": null}`; SQLite's fallback returns false. The
1620    // diverging-on-explicit-null case is documented; users with
1621    // strict "key present even if value is null" needs hand-roll the
1622    // SQLite SQL.
1623    let sqlite_sql = format!("json_extract(\"{col_escaped}\", ?) IS NOT NULL");
1624    let sqlite_values = vec![sea_query::Value::String(Some(Box::new(format!("$.{key}"))))];
1625    let sqlite_cond = Expr::cust_with_values(&sqlite_sql, sqlite_values);
1626
1627    Predicate::new_with_sqlite(pg_cond, sqlite_cond)
1628}
1629
1630// =========================================================================
1631// Array columns — Phase 4.1, Postgres-only.
1632//
1633// v1 surface: ordering ops (asc/desc) and IS NULL / IS NOT NULL for
1634// the nullable variant. Array-specific operators (`@>` contains,
1635// `<@` contained-by, `&&` overlaps, `array_length`, `unnest`) land
1636// as a follow-on. The element type is *not* a generic parameter on
1637// the column struct itself — the predicate methods we ship today
1638// don't need to know it, and adding a type parameter would force the
1639// derive macro to plumb it through every column-const declaration
1640// (each user struct's sibling module would gain an extra type arg).
1641// When the per-element operators land, the element type comes via a
1642// const associated value on the column or via the element ops as
1643// generics on a single method.
1644// =========================================================================
1645
1646/// A `Vec<T>`-typed column (Postgres array).
1647pub struct ArrayCol<T> {
1648    pub(crate) name: &'static str,
1649    _phantom: PhantomData<T>,
1650}
1651
1652impl<T> ArrayCol<T> {
1653    pub const fn new(name: &'static str) -> Self {
1654        Self {
1655            name,
1656            _phantom: PhantomData,
1657        }
1658    }
1659
1660    /// SQL `ORDER BY ... ASC`. Postgres array ordering is element-wise
1661    /// lexicographic — rarely what the user wants, but well-defined.
1662    pub fn asc(&self) -> OrderExpr<T> {
1663        OrderExpr::new(self.name, false)
1664    }
1665
1666    /// SQL `ORDER BY ... DESC`.
1667    pub fn desc(&self) -> OrderExpr<T> {
1668        OrderExpr::new(self.name, true)
1669    }
1670
1671    /// SQL `col @> ARRAY[elem]` (Postgres contains).
1672    ///
1673    /// Returns `true` if every element of `ARRAY[elem]` is present in
1674    /// the column's array — i.e. `elem` appears in the array. Use
1675    /// [`Self::contains_all`] when checking multiple elements at once.
1676    ///
1677    /// Postgres-only. ArrayCol is system-check-gated against SQLite, so
1678    /// the SQL fragment this emits only ever renders against a
1679    /// PostgresQueryBuilder.
1680    pub fn contains<V: Into<sea_query::Value>>(&self, elem: V) -> Predicate<T> {
1681        array_contains_predicate(self.name, std::iter::once(elem.into()))
1682    }
1683
1684    /// SQL `col @> ARRAY[elems...]` (Postgres contains-all).
1685    ///
1686    /// Returns `true` if every element of `elems` is present in the
1687    /// column's array. An empty `elems` returns vacuously `true` (the
1688    /// empty set is contained by every set), which Postgres also
1689    /// reports — but the renderer requires at least one element to
1690    /// produce a typed `ARRAY[...]` literal; passing an empty iterator
1691    /// returns a tautology predicate (`1 = 1`).
1692    pub fn contains_all<I, V>(&self, elems: I) -> Predicate<T>
1693    where
1694        I: IntoIterator<Item = V>,
1695        V: Into<sea_query::Value>,
1696    {
1697        array_contains_predicate(self.name, elems.into_iter().map(Into::into))
1698    }
1699
1700    /// SQL `col <@ ARRAY[elems...]` (Postgres contained-by).
1701    ///
1702    /// Returns `true` if every element of the column's array is in
1703    /// `elems` — i.e. the column is a subset of the supplied set.
1704    pub fn contained_by<I, V>(&self, elems: I) -> Predicate<T>
1705    where
1706        I: IntoIterator<Item = V>,
1707        V: Into<sea_query::Value>,
1708    {
1709        array_contained_by_predicate(self.name, elems.into_iter().map(Into::into))
1710    }
1711
1712    /// SQL `col && ARRAY[elems...]` (Postgres overlaps).
1713    ///
1714    /// Returns `true` if the column's array and `elems` share at least
1715    /// one element.
1716    pub fn overlaps<I, V>(&self, elems: I) -> Predicate<T>
1717    where
1718        I: IntoIterator<Item = V>,
1719        V: Into<sea_query::Value>,
1720    {
1721        array_overlaps_predicate(self.name, elems.into_iter().map(Into::into))
1722    }
1723}
1724
1725/// A nullable `Vec<T>`-typed column.
1726pub struct NullableArrayCol<T> {
1727    pub(crate) name: &'static str,
1728    _phantom: PhantomData<T>,
1729}
1730
1731impl<T> NullableArrayCol<T> {
1732    pub const fn new(name: &'static str) -> Self {
1733        Self {
1734            name,
1735            _phantom: PhantomData,
1736        }
1737    }
1738
1739    /// SQL `IS NULL`. Note this is "the column is NULL", not "the
1740    /// array is empty" — Postgres distinguishes them. The empty-array
1741    /// predicate lands with the `array_length` op in a follow-on.
1742    pub fn is_null(&self) -> Predicate<T> {
1743        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
1744    }
1745
1746    /// SQL `IS NOT NULL`.
1747    pub fn is_not_null(&self) -> Predicate<T> {
1748        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
1749    }
1750
1751    /// SQL `ORDER BY ... ASC`.
1752    pub fn asc(&self) -> OrderExpr<T> {
1753        OrderExpr::new(self.name, false)
1754    }
1755
1756    /// SQL `ORDER BY ... DESC`.
1757    pub fn desc(&self) -> OrderExpr<T> {
1758        OrderExpr::new(self.name, true)
1759    }
1760
1761    /// See [`ArrayCol::contains`]. NULL columns are excluded by SQL's
1762    /// three-valued logic — same as every other column predicate.
1763    pub fn contains<V: Into<sea_query::Value>>(&self, elem: V) -> Predicate<T> {
1764        array_contains_predicate(self.name, std::iter::once(elem.into()))
1765    }
1766
1767    /// See [`ArrayCol::contains_all`].
1768    pub fn contains_all<I, V>(&self, elems: I) -> Predicate<T>
1769    where
1770        I: IntoIterator<Item = V>,
1771        V: Into<sea_query::Value>,
1772    {
1773        array_contains_predicate(self.name, elems.into_iter().map(Into::into))
1774    }
1775
1776    /// See [`ArrayCol::contained_by`].
1777    pub fn contained_by<I, V>(&self, elems: I) -> Predicate<T>
1778    where
1779        I: IntoIterator<Item = V>,
1780        V: Into<sea_query::Value>,
1781    {
1782        array_contained_by_predicate(self.name, elems.into_iter().map(Into::into))
1783    }
1784
1785    /// See [`ArrayCol::overlaps`].
1786    pub fn overlaps<I, V>(&self, elems: I) -> Predicate<T>
1787    where
1788        I: IntoIterator<Item = V>,
1789        V: Into<sea_query::Value>,
1790    {
1791        array_overlaps_predicate(self.name, elems.into_iter().map(Into::into))
1792    }
1793}
1794
1795// =========================================================================
1796// Internal helpers: array operator predicates.
1797//
1798// The three operators share the same shape — `"col" OP ARRAY[$1, $2,
1799// ...]` — and differ only by the operator string. Factored so the
1800// ArrayCol and NullableArrayCol impls stay short.
1801//
1802// Each helper builds a `sea_query::Expr::cust_with_values` SimpleExpr.
1803// The column identifier is quoted into the SQL template (Postgres
1804// double-quote escaping); the elements bind through sea-query's value
1805// list. Empty element lists return a tautology (`1 = 1`) or a
1806// guaranteed-false predicate as appropriate, so the caller doesn't
1807// have to special-case empty input.
1808//
1809// **Postgres-only.** ArrayCol is system-check-gated against SQLite, so
1810// these fragments only ever render against PostgresQueryBuilder.
1811// =========================================================================
1812
1813fn array_op_predicate<T>(
1814    col: &'static str,
1815    op: &str,
1816    values: Vec<sea_query::Value>,
1817) -> Predicate<T> {
1818    if values.is_empty() {
1819        // Render as a constant boolean. `1 = 1` is true; `1 = 0` false.
1820        // Each operator picks the right tautology in the caller.
1821        return Predicate::new(Expr::cust("1 = 1"));
1822    }
1823    let placeholders: Vec<String> = (1..=values.len()).map(|i| format!("${i}")).collect();
1824    let sql = format!(
1825        "\"{}\" {op} ARRAY[{}]",
1826        col.replace('"', "\"\""),
1827        placeholders.join(", ")
1828    );
1829    Predicate::new(Expr::cust_with_values(&sql, values))
1830}
1831
1832fn array_contains_predicate<T, I>(col: &'static str, elems: I) -> Predicate<T>
1833where
1834    I: IntoIterator<Item = sea_query::Value>,
1835{
1836    // `col @> ARRAY[]` is vacuously true on Postgres (empty set is
1837    // contained by every set). Render as 1 = 1 to keep the QuerySet
1838    // simple and predictable.
1839    array_op_predicate::<T>(col, "@>", elems.into_iter().collect())
1840}
1841
1842fn array_contained_by_predicate<T, I>(col: &'static str, elems: I) -> Predicate<T>
1843where
1844    I: IntoIterator<Item = sea_query::Value>,
1845{
1846    let values: Vec<sea_query::Value> = elems.into_iter().collect();
1847    if values.is_empty() {
1848        // `col <@ ARRAY[]` is true only when `col` is empty or NULL;
1849        // 1 = 1 isn't right here. Use a guaranteed-false predicate
1850        // so the caller sees zero rows for "subset of nothing" — the
1851        // honest answer when the column has any rows at all. The
1852        // empty-array-equality check belongs in a future `len()`
1853        // op.
1854        return Predicate::new(Expr::cust("1 = 0"));
1855    }
1856    array_op_predicate::<T>(col, "<@", values)
1857}
1858
1859fn array_overlaps_predicate<T, I>(col: &'static str, elems: I) -> Predicate<T>
1860where
1861    I: IntoIterator<Item = sea_query::Value>,
1862{
1863    let values: Vec<sea_query::Value> = elems.into_iter().collect();
1864    if values.is_empty() {
1865        // Empty set overlaps nothing; predicate is always false.
1866        return Predicate::new(Expr::cust("1 = 0"));
1867    }
1868    array_op_predicate::<T>(col, "&&", values)
1869}
1870
1871// =========================================================================
1872// Network address columns — Phase 4.4, Postgres-only.
1873//
1874// Three pairs: `InetCol` / `NullableInetCol` for INET (`ipnetwork::
1875// IpNetwork`); `CidrCol` / `NullableCidrCol` for CIDR (same Rust type
1876// as Inet, just constrained to a network address); `MacAddrCol` /
1877// `NullableMacAddrCol` for MACADDR (`mac_address::MacAddress`).
1878//
1879// v1 surface: equality / inequality, `IS NULL` / `IS NOT NULL` on the
1880// nullable variants, plus the standard `asc()` / `desc()`. Network-
1881// specific operators (`<<`, `>>`, `&`, `|` on inet types; `<<=` /
1882// `>>=` for containment; `~` for MAC ranges) are deferred until a
1883// real consumer surfaces them.
1884//
1885// Each `Col::eq(val)` takes the Rust binding type by value. sea-query
1886// has built-in `Value::IpNetwork` and `Value::MacAddress` variants
1887// (gated behind sqlx feature flags we've enabled on sea-query-binder
1888// via the `with-ipnetwork` / `with-mac_address` route — sqlx pulls
1889// the same types through and they implement `Into<sea_query::Value>`).
1890// =========================================================================
1891
1892/// An `ipnetwork::IpNetwork`-typed column (Postgres INET).
1893pub struct InetCol<T> {
1894    pub(crate) name: &'static str,
1895    _phantom: PhantomData<T>,
1896}
1897
1898impl<T> InetCol<T> {
1899    pub const fn new(name: &'static str) -> Self {
1900        Self {
1901            name,
1902            _phantom: PhantomData,
1903        }
1904    }
1905
1906    /// SQL `=`.
1907    pub fn eq(&self, val: ipnetwork::IpNetwork) -> Predicate<T> {
1908        // sea-query doesn't expose `Into<Value>` for `IpNetwork` from
1909        // the `ipnetwork` crate directly; render the comparison via
1910        // `cust_with_values` with the value bound positionally.
1911        let sql = format!("\"{}\" = $1", self.name.replace('"', "\"\""));
1912        // sea_query::Value carries an IpNetwork variant when its
1913        // `with-ipnetwork` feature is enabled; cast through the
1914        // `Into` impl.
1915        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
1916    }
1917
1918    /// SQL `<>`.
1919    pub fn ne(&self, val: ipnetwork::IpNetwork) -> Predicate<T> {
1920        let sql = format!("\"{}\" <> $1", self.name.replace('"', "\"\""));
1921        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
1922    }
1923
1924    /// SQL `ORDER BY ... ASC`.
1925    pub fn asc(&self) -> OrderExpr<T> {
1926        OrderExpr::new(self.name, false)
1927    }
1928
1929    /// SQL `ORDER BY ... DESC`.
1930    pub fn desc(&self) -> OrderExpr<T> {
1931        OrderExpr::new(self.name, true)
1932    }
1933}
1934
1935/// A nullable INET column.
1936pub struct NullableInetCol<T> {
1937    pub(crate) name: &'static str,
1938    _phantom: PhantomData<T>,
1939}
1940
1941impl<T> NullableInetCol<T> {
1942    pub const fn new(name: &'static str) -> Self {
1943        Self {
1944            name,
1945            _phantom: PhantomData,
1946        }
1947    }
1948
1949    /// SQL `=`. NULL rows are excluded by SQL's three-valued logic.
1950    pub fn eq(&self, val: ipnetwork::IpNetwork) -> Predicate<T> {
1951        let sql = format!("\"{}\" = $1", self.name.replace('"', "\"\""));
1952        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
1953    }
1954
1955    /// SQL `<>`.
1956    pub fn ne(&self, val: ipnetwork::IpNetwork) -> Predicate<T> {
1957        let sql = format!("\"{}\" <> $1", self.name.replace('"', "\"\""));
1958        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
1959    }
1960
1961    /// SQL `IS NULL`.
1962    pub fn is_null(&self) -> Predicate<T> {
1963        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
1964    }
1965
1966    /// SQL `IS NOT NULL`.
1967    pub fn is_not_null(&self) -> Predicate<T> {
1968        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
1969    }
1970
1971    /// SQL `ORDER BY ... ASC`.
1972    pub fn asc(&self) -> OrderExpr<T> {
1973        OrderExpr::new(self.name, false)
1974    }
1975
1976    /// SQL `ORDER BY ... DESC`.
1977    pub fn desc(&self) -> OrderExpr<T> {
1978        OrderExpr::new(self.name, true)
1979    }
1980}
1981
1982/// An `ipnetwork::IpNetwork`-typed column declared as a Postgres CIDR.
1983///
1984/// Same Rust binding type as [`InetCol`]; the DDL renders as `cidr`
1985/// (with the host-bits-zero constraint Postgres enforces). For
1986/// general host-address storage, use `InetCol`.
1987pub struct CidrCol<T> {
1988    pub(crate) name: &'static str,
1989    _phantom: PhantomData<T>,
1990}
1991
1992impl<T> CidrCol<T> {
1993    pub const fn new(name: &'static str) -> Self {
1994        Self {
1995            name,
1996            _phantom: PhantomData,
1997        }
1998    }
1999
2000    pub fn eq(&self, val: ipnetwork::IpNetwork) -> Predicate<T> {
2001        let sql = format!("\"{}\" = $1", self.name.replace('"', "\"\""));
2002        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
2003    }
2004
2005    pub fn ne(&self, val: ipnetwork::IpNetwork) -> Predicate<T> {
2006        let sql = format!("\"{}\" <> $1", self.name.replace('"', "\"\""));
2007        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
2008    }
2009
2010    pub fn asc(&self) -> OrderExpr<T> {
2011        OrderExpr::new(self.name, false)
2012    }
2013
2014    pub fn desc(&self) -> OrderExpr<T> {
2015        OrderExpr::new(self.name, true)
2016    }
2017}
2018
2019/// A nullable CIDR column.
2020pub struct NullableCidrCol<T> {
2021    pub(crate) name: &'static str,
2022    _phantom: PhantomData<T>,
2023}
2024
2025impl<T> NullableCidrCol<T> {
2026    pub const fn new(name: &'static str) -> Self {
2027        Self {
2028            name,
2029            _phantom: PhantomData,
2030        }
2031    }
2032
2033    pub fn eq(&self, val: ipnetwork::IpNetwork) -> Predicate<T> {
2034        let sql = format!("\"{}\" = $1", self.name.replace('"', "\"\""));
2035        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
2036    }
2037
2038    pub fn ne(&self, val: ipnetwork::IpNetwork) -> Predicate<T> {
2039        let sql = format!("\"{}\" <> $1", self.name.replace('"', "\"\""));
2040        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
2041    }
2042
2043    pub fn is_null(&self) -> Predicate<T> {
2044        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
2045    }
2046
2047    pub fn is_not_null(&self) -> Predicate<T> {
2048        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
2049    }
2050
2051    pub fn asc(&self) -> OrderExpr<T> {
2052        OrderExpr::new(self.name, false)
2053    }
2054
2055    pub fn desc(&self) -> OrderExpr<T> {
2056        OrderExpr::new(self.name, true)
2057    }
2058}
2059
2060/// A `mac_address::MacAddress`-typed column (Postgres MACADDR).
2061pub struct MacAddrCol<T> {
2062    pub(crate) name: &'static str,
2063    _phantom: PhantomData<T>,
2064}
2065
2066impl<T> MacAddrCol<T> {
2067    pub const fn new(name: &'static str) -> Self {
2068        Self {
2069            name,
2070            _phantom: PhantomData,
2071        }
2072    }
2073
2074    pub fn eq(&self, val: mac_address::MacAddress) -> Predicate<T> {
2075        let sql = format!("\"{}\" = $1", self.name.replace('"', "\"\""));
2076        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
2077    }
2078
2079    pub fn ne(&self, val: mac_address::MacAddress) -> Predicate<T> {
2080        let sql = format!("\"{}\" <> $1", self.name.replace('"', "\"\""));
2081        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
2082    }
2083
2084    pub fn asc(&self) -> OrderExpr<T> {
2085        OrderExpr::new(self.name, false)
2086    }
2087
2088    pub fn desc(&self) -> OrderExpr<T> {
2089        OrderExpr::new(self.name, true)
2090    }
2091}
2092
2093// =========================================================================
2094// Full-text search columns — Phase 4.3, Postgres-only.
2095//
2096// `FullTextCol<T>` / `NullableFullTextCol<T>` wrap a Postgres
2097// `tsvector` column. v1 surface: `matches(query)` for plain
2098// `to_tsquery` matching, `matches_websearch(query)` for the more
2099// permissive `websearch_to_tsquery` form (handles user-typed search
2100// strings with quoted phrases, OR, etc.). Storage is a text vector;
2101// the column is typically populated via Postgres trigger or
2102// GENERATED ALWAYS clause — umbral's migration engine emits the bare
2103// `tsvector` declaration and leaves the population to the user.
2104// =========================================================================
2105
2106/// A `umbral::orm::TsVector`-typed column (Postgres tsvector).
2107pub struct FullTextCol<T> {
2108    pub(crate) name: &'static str,
2109    _phantom: PhantomData<T>,
2110}
2111
2112impl<T> FullTextCol<T> {
2113    pub const fn new(name: &'static str) -> Self {
2114        Self {
2115            name,
2116            _phantom: PhantomData,
2117        }
2118    }
2119
2120    /// SQL `col @@ to_tsquery($1)`. The query string follows
2121    /// Postgres's `to_tsquery` syntax: `&` AND, `|` OR, `!` NOT,
2122    /// `:*` prefix match. Strict — malformed queries error at the
2123    /// server.
2124    pub fn matches(&self, query: &str) -> Predicate<T> {
2125        let col = self.name.replace('"', "\"\"");
2126        let sql = format!("\"{col}\" @@ to_tsquery($1)");
2127        let values = vec![sea_query::Value::String(Some(Box::new(query.to_string())))];
2128        Predicate::new(Expr::cust_with_values(&sql, values))
2129    }
2130
2131    /// SQL `col @@ websearch_to_tsquery($1)`. The query string follows
2132    /// web-search conventions: space-separated terms (AND), `OR`,
2133    /// `-term` for negation, `"quoted phrase"` for adjacency. More
2134    /// forgiving than [`Self::matches`].
2135    pub fn matches_websearch(&self, query: &str) -> Predicate<T> {
2136        let col = self.name.replace('"', "\"\"");
2137        let sql = format!("\"{col}\" @@ websearch_to_tsquery($1)");
2138        let values = vec![sea_query::Value::String(Some(Box::new(query.to_string())))];
2139        Predicate::new(Expr::cust_with_values(&sql, values))
2140    }
2141
2142    pub fn asc(&self) -> OrderExpr<T> {
2143        OrderExpr::new(self.name, false)
2144    }
2145
2146    pub fn desc(&self) -> OrderExpr<T> {
2147        OrderExpr::new(self.name, true)
2148    }
2149}
2150
2151/// A nullable tsvector column.
2152pub struct NullableFullTextCol<T> {
2153    pub(crate) name: &'static str,
2154    _phantom: PhantomData<T>,
2155}
2156
2157impl<T> NullableFullTextCol<T> {
2158    pub const fn new(name: &'static str) -> Self {
2159        Self {
2160            name,
2161            _phantom: PhantomData,
2162        }
2163    }
2164
2165    pub fn matches(&self, query: &str) -> Predicate<T> {
2166        let col = self.name.replace('"', "\"\"");
2167        let sql = format!("\"{col}\" @@ to_tsquery($1)");
2168        let values = vec![sea_query::Value::String(Some(Box::new(query.to_string())))];
2169        Predicate::new(Expr::cust_with_values(&sql, values))
2170    }
2171
2172    pub fn matches_websearch(&self, query: &str) -> Predicate<T> {
2173        let col = self.name.replace('"', "\"\"");
2174        let sql = format!("\"{col}\" @@ websearch_to_tsquery($1)");
2175        let values = vec![sea_query::Value::String(Some(Box::new(query.to_string())))];
2176        Predicate::new(Expr::cust_with_values(&sql, values))
2177    }
2178
2179    pub fn is_null(&self) -> Predicate<T> {
2180        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
2181    }
2182
2183    pub fn is_not_null(&self) -> Predicate<T> {
2184        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
2185    }
2186
2187    pub fn asc(&self) -> OrderExpr<T> {
2188        OrderExpr::new(self.name, false)
2189    }
2190
2191    pub fn desc(&self) -> OrderExpr<T> {
2192        OrderExpr::new(self.name, true)
2193    }
2194}
2195
2196/// A nullable MACADDR column.
2197pub struct NullableMacAddrCol<T> {
2198    pub(crate) name: &'static str,
2199    _phantom: PhantomData<T>,
2200}
2201
2202impl<T> NullableMacAddrCol<T> {
2203    pub const fn new(name: &'static str) -> Self {
2204        Self {
2205            name,
2206            _phantom: PhantomData,
2207        }
2208    }
2209
2210    pub fn eq(&self, val: mac_address::MacAddress) -> Predicate<T> {
2211        let sql = format!("\"{}\" = $1", self.name.replace('"', "\"\""));
2212        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
2213    }
2214
2215    pub fn ne(&self, val: mac_address::MacAddress) -> Predicate<T> {
2216        let sql = format!("\"{}\" <> $1", self.name.replace('"', "\"\""));
2217        Predicate::new(Expr::cust_with_values(&sql, vec![val]))
2218    }
2219
2220    pub fn is_null(&self) -> Predicate<T> {
2221        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
2222    }
2223
2224    pub fn is_not_null(&self) -> Predicate<T> {
2225        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
2226    }
2227
2228    pub fn asc(&self) -> OrderExpr<T> {
2229        OrderExpr::new(self.name, false)
2230    }
2231
2232    pub fn desc(&self) -> OrderExpr<T> {
2233        OrderExpr::new(self.name, true)
2234    }
2235}
2236
2237// =========================================================================
2238// Text-backed Postgres-only columns — gaps2 #70.
2239//
2240// Three pairs of String-valued columns for Postgres types that have a
2241// faithful textual representation but their own native column type:
2242// `XmlCol` (XML), `LtreeCol` (LTREE), `BitCol` (BIT VARYING). Unlike the
2243// network columns above, the Rust binding is a plain `String` — umbral
2244// stores and round-trips the serialized form and lets Postgres enforce
2245// the type's invariants on insert. **Postgres-only**; the field.backend
2246// system check rejects them on SQLite the same way it rejects Inet /
2247// Cidr / MacAddr / Decimal / Array.
2248//
2249// v1 surface mirrors the network columns: equality / inequality, plus
2250// `IS NULL` / `IS NOT NULL` on the nullable variants and `asc()` /
2251// `desc()`. Type-specific operators (`@>` ancestor on ltree, `xpath` on
2252// xml, bitwise ops on bit) are deferred until a real consumer surfaces
2253// them. Each `eq` / `ne` binds the value as a text parameter; Postgres
2254// applies the column's own cast on the way in.
2255// =========================================================================
2256
2257/// A `String`-valued Postgres `XML` column.
2258pub struct XmlCol<T> {
2259    pub(crate) name: &'static str,
2260    _phantom: PhantomData<T>,
2261}
2262
2263impl<T> XmlCol<T> {
2264    pub const fn new(name: &'static str) -> Self {
2265        Self {
2266            name,
2267            _phantom: PhantomData,
2268        }
2269    }
2270
2271    pub fn eq(&self, val: &str) -> Predicate<T> {
2272        let sql = format!("\"{}\"::text = $1", self.name.replace('"', "\"\""));
2273        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2274        Predicate::new(Expr::cust_with_values(&sql, values))
2275    }
2276
2277    pub fn ne(&self, val: &str) -> Predicate<T> {
2278        let sql = format!("\"{}\"::text <> $1", self.name.replace('"', "\"\""));
2279        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2280        Predicate::new(Expr::cust_with_values(&sql, values))
2281    }
2282
2283    pub fn asc(&self) -> OrderExpr<T> {
2284        OrderExpr::new(self.name, false)
2285    }
2286
2287    pub fn desc(&self) -> OrderExpr<T> {
2288        OrderExpr::new(self.name, true)
2289    }
2290}
2291
2292/// A nullable XML column.
2293pub struct NullableXmlCol<T> {
2294    pub(crate) name: &'static str,
2295    _phantom: PhantomData<T>,
2296}
2297
2298impl<T> NullableXmlCol<T> {
2299    pub const fn new(name: &'static str) -> Self {
2300        Self {
2301            name,
2302            _phantom: PhantomData,
2303        }
2304    }
2305
2306    pub fn eq(&self, val: &str) -> Predicate<T> {
2307        let sql = format!("\"{}\"::text = $1", self.name.replace('"', "\"\""));
2308        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2309        Predicate::new(Expr::cust_with_values(&sql, values))
2310    }
2311
2312    pub fn ne(&self, val: &str) -> Predicate<T> {
2313        let sql = format!("\"{}\"::text <> $1", self.name.replace('"', "\"\""));
2314        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2315        Predicate::new(Expr::cust_with_values(&sql, values))
2316    }
2317
2318    pub fn is_null(&self) -> Predicate<T> {
2319        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
2320    }
2321
2322    pub fn is_not_null(&self) -> Predicate<T> {
2323        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
2324    }
2325
2326    pub fn asc(&self) -> OrderExpr<T> {
2327        OrderExpr::new(self.name, false)
2328    }
2329
2330    pub fn desc(&self) -> OrderExpr<T> {
2331        OrderExpr::new(self.name, true)
2332    }
2333}
2334
2335/// A `String`-valued Postgres `LTREE` column (dotted label path).
2336pub struct LtreeCol<T> {
2337    pub(crate) name: &'static str,
2338    _phantom: PhantomData<T>,
2339}
2340
2341impl<T> LtreeCol<T> {
2342    pub const fn new(name: &'static str) -> Self {
2343        Self {
2344            name,
2345            _phantom: PhantomData,
2346        }
2347    }
2348
2349    pub fn eq(&self, val: &str) -> Predicate<T> {
2350        let sql = format!("\"{}\" = $1::ltree", self.name.replace('"', "\"\""));
2351        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2352        Predicate::new(Expr::cust_with_values(&sql, values))
2353    }
2354
2355    pub fn ne(&self, val: &str) -> Predicate<T> {
2356        let sql = format!("\"{}\" <> $1::ltree", self.name.replace('"', "\"\""));
2357        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2358        Predicate::new(Expr::cust_with_values(&sql, values))
2359    }
2360
2361    pub fn asc(&self) -> OrderExpr<T> {
2362        OrderExpr::new(self.name, false)
2363    }
2364
2365    pub fn desc(&self) -> OrderExpr<T> {
2366        OrderExpr::new(self.name, true)
2367    }
2368}
2369
2370/// A nullable LTREE column.
2371pub struct NullableLtreeCol<T> {
2372    pub(crate) name: &'static str,
2373    _phantom: PhantomData<T>,
2374}
2375
2376impl<T> NullableLtreeCol<T> {
2377    pub const fn new(name: &'static str) -> Self {
2378        Self {
2379            name,
2380            _phantom: PhantomData,
2381        }
2382    }
2383
2384    pub fn eq(&self, val: &str) -> Predicate<T> {
2385        let sql = format!("\"{}\" = $1::ltree", self.name.replace('"', "\"\""));
2386        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2387        Predicate::new(Expr::cust_with_values(&sql, values))
2388    }
2389
2390    pub fn ne(&self, val: &str) -> Predicate<T> {
2391        let sql = format!("\"{}\" <> $1::ltree", self.name.replace('"', "\"\""));
2392        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2393        Predicate::new(Expr::cust_with_values(&sql, values))
2394    }
2395
2396    pub fn is_null(&self) -> Predicate<T> {
2397        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
2398    }
2399
2400    pub fn is_not_null(&self) -> Predicate<T> {
2401        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
2402    }
2403
2404    pub fn asc(&self) -> OrderExpr<T> {
2405        OrderExpr::new(self.name, false)
2406    }
2407
2408    pub fn desc(&self) -> OrderExpr<T> {
2409        OrderExpr::new(self.name, true)
2410    }
2411}
2412
2413/// A `String`-valued Postgres `BIT VARYING` column.
2414pub struct BitCol<T> {
2415    pub(crate) name: &'static str,
2416    _phantom: PhantomData<T>,
2417}
2418
2419impl<T> BitCol<T> {
2420    pub const fn new(name: &'static str) -> Self {
2421        Self {
2422            name,
2423            _phantom: PhantomData,
2424        }
2425    }
2426
2427    pub fn eq(&self, val: &str) -> Predicate<T> {
2428        let sql = format!("\"{}\" = $1::bit varying", self.name.replace('"', "\"\""));
2429        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2430        Predicate::new(Expr::cust_with_values(&sql, values))
2431    }
2432
2433    pub fn ne(&self, val: &str) -> Predicate<T> {
2434        let sql = format!("\"{}\" <> $1::bit varying", self.name.replace('"', "\"\""));
2435        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2436        Predicate::new(Expr::cust_with_values(&sql, values))
2437    }
2438
2439    pub fn asc(&self) -> OrderExpr<T> {
2440        OrderExpr::new(self.name, false)
2441    }
2442
2443    pub fn desc(&self) -> OrderExpr<T> {
2444        OrderExpr::new(self.name, true)
2445    }
2446}
2447
2448/// A nullable BIT VARYING column.
2449pub struct NullableBitCol<T> {
2450    pub(crate) name: &'static str,
2451    _phantom: PhantomData<T>,
2452}
2453
2454impl<T> NullableBitCol<T> {
2455    pub const fn new(name: &'static str) -> Self {
2456        Self {
2457            name,
2458            _phantom: PhantomData,
2459        }
2460    }
2461
2462    pub fn eq(&self, val: &str) -> Predicate<T> {
2463        let sql = format!("\"{}\" = $1::bit varying", self.name.replace('"', "\"\""));
2464        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2465        Predicate::new(Expr::cust_with_values(&sql, values))
2466    }
2467
2468    pub fn ne(&self, val: &str) -> Predicate<T> {
2469        let sql = format!("\"{}\" <> $1::bit varying", self.name.replace('"', "\"\""));
2470        let values = vec![sea_query::Value::String(Some(Box::new(val.to_string())))];
2471        Predicate::new(Expr::cust_with_values(&sql, values))
2472    }
2473
2474    pub fn is_null(&self) -> Predicate<T> {
2475        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
2476    }
2477
2478    pub fn is_not_null(&self) -> Predicate<T> {
2479        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
2480    }
2481
2482    pub fn asc(&self) -> OrderExpr<T> {
2483        OrderExpr::new(self.name, false)
2484    }
2485
2486    pub fn desc(&self) -> OrderExpr<T> {
2487        OrderExpr::new(self.name, true)
2488    }
2489}
2490
2491// =========================================================================
2492// Foreign-key columns — gap 14.
2493//
2494// `ForeignKeyCol<T>` is the column type emitted by `#[derive(Model)]` for
2495// fields of type `ForeignKey<U>`. Because `ForeignKey<U>` is stored as
2496// `i64` in SQL, the predicate surface is identical to `IntCol<T>`: equality,
2497// inequality, range comparisons, and `IN`. Ordering and `ASC` / `DESC` are
2498// also present.
2499//
2500// The `T` phantom parameter ties the column to its *owning* model (as every
2501// column type does); the referenced model type lives only in the Rust field
2502// declaration and is erased at the column-constant level.
2503// =========================================================================
2504
2505/// A foreign-key column — stored as `i64`, referencing the primary key of
2506/// another model's table.
2507pub struct ForeignKeyCol<T> {
2508    pub(crate) name: &'static str,
2509    _phantom: PhantomData<T>,
2510}
2511
2512impl<T> ForeignKeyCol<T> {
2513    pub const fn new(name: &'static str) -> Self {
2514        Self {
2515            name,
2516            _phantom: PhantomData,
2517        }
2518    }
2519
2520    /// SQL `=`.
2521    ///
2522    /// Accepts any value convertible to `sea_query::Value` — i64 for
2523    /// the common autoincrement-PK case, String for slug-keyed
2524    /// parents (`umbral-permissions::Permission.codename`), Uuid for
2525    /// UUID-keyed models. The type bound is permissive so reverse-FK
2526    /// accessors (gap #30) emitted by the derive macro can pass the
2527    /// parent's `Model::PrimaryKey` directly regardless of width.
2528    ///
2529    /// # Examples
2530    ///
2531    /// ```ignore
2532    /// Post::objects().filter(post::AUTHOR.eq(1));
2533    /// UserGroup::objects().filter(usergroup::GROUP_ID.eq(group.id));
2534    /// ```
2535    pub fn eq<V: Into<sea_query::Value>>(&self, val: V) -> Predicate<T> {
2536        Predicate::new(Expr::col(Alias::new(self.name)).eq(val.into()))
2537    }
2538
2539    /// SQL `<>`. See [`Self::eq`] for the type bound rationale.
2540    pub fn ne<V: Into<sea_query::Value>>(&self, val: V) -> Predicate<T> {
2541        Predicate::new(Expr::col(Alias::new(self.name)).ne(val.into()))
2542    }
2543
2544    /// SQL `<`.
2545    pub fn lt(&self, val: i64) -> Predicate<T> {
2546        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
2547    }
2548
2549    /// SQL `<=`.
2550    pub fn le(&self, val: i64) -> Predicate<T> {
2551        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
2552    }
2553
2554    /// Lookup-style alias for [`Self::le`]. Same as `__lte` in URL
2555    /// filter strings.
2556    pub fn lte(&self, val: i64) -> Predicate<T> {
2557        self.le(val)
2558    }
2559
2560    /// SQL `>`.
2561    pub fn gt(&self, val: i64) -> Predicate<T> {
2562        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
2563    }
2564
2565    /// SQL `>=`.
2566    pub fn ge(&self, val: i64) -> Predicate<T> {
2567        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
2568    }
2569
2570    /// Lookup-style alias for [`Self::ge`]. Same as `__gte` in URL
2571    /// filter strings.
2572    pub fn gte(&self, val: i64) -> Predicate<T> {
2573        self.ge(val)
2574    }
2575
2576    /// SQL `IN (...)`.
2577    pub fn in_(&self, vals: &[i64]) -> Predicate<T> {
2578        Predicate::new(Expr::col(Alias::new(self.name)).is_in(vals.iter().copied()))
2579    }
2580
2581    /// SQL `<col> IN (SELECT ...)` against a [`super::Subquery`]
2582    /// (gap #26). See [`IntCol::in_subquery`].
2583    pub fn in_subquery(&self, sub: super::Subquery) -> Predicate<T> {
2584        Predicate::new(Expr::col(Alias::new(self.name)).in_subquery(sub.into_statement()))
2585    }
2586
2587    /// SQL `ORDER BY ... ASC`.
2588    pub fn asc(&self) -> OrderExpr<T> {
2589        OrderExpr::new(self.name, false)
2590    }
2591
2592    /// SQL `ORDER BY ... DESC`.
2593    pub fn desc(&self) -> OrderExpr<T> {
2594        OrderExpr::new(self.name, true)
2595    }
2596}
2597
2598/// A nullable foreign-key column — the `Option<ForeignKey<U>>` shape.
2599pub struct NullableForeignKeyCol<T> {
2600    pub(crate) name: &'static str,
2601    _phantom: PhantomData<T>,
2602}
2603
2604impl<T> NullableForeignKeyCol<T> {
2605    pub const fn new(name: &'static str) -> Self {
2606        Self {
2607            name,
2608            _phantom: PhantomData,
2609        }
2610    }
2611
2612    /// SQL `=`. NULL rows are excluded by SQL's NULL semantics.
2613    pub fn eq(&self, val: i64) -> Predicate<T> {
2614        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
2615    }
2616
2617    /// SQL `<>`.
2618    pub fn ne(&self, val: i64) -> Predicate<T> {
2619        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
2620    }
2621
2622    /// SQL `IN (...)`.
2623    pub fn in_(&self, vals: &[i64]) -> Predicate<T> {
2624        Predicate::new(Expr::col(Alias::new(self.name)).is_in(vals.iter().copied()))
2625    }
2626
2627    /// SQL `IS NULL`.
2628    pub fn is_null(&self) -> Predicate<T> {
2629        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
2630    }
2631
2632    /// SQL `IS NOT NULL`.
2633    pub fn is_not_null(&self) -> Predicate<T> {
2634        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
2635    }
2636
2637    /// SQL `ORDER BY ... ASC`.
2638    pub fn asc(&self) -> OrderExpr<T> {
2639        OrderExpr::new(self.name, false)
2640    }
2641
2642    /// SQL `ORDER BY ... DESC`.
2643    pub fn desc(&self) -> OrderExpr<T> {
2644        OrderExpr::new(self.name, true)
2645    }
2646}
2647
2648// =============================================================================
2649// BytesCol — Vec<u8> / BLOB / BYTEA columns.
2650// =============================================================================
2651
2652/// A `BLOB` (SQLite) / `BYTEA` (Postgres) column carrying arbitrary bytes.
2653/// The Rust field type is `Vec<u8>`. v1 ships equality + null-checks + ordering;
2654/// the operator surface is intentionally small because byte columns rarely
2655/// appear in WHERE clauses (think file payloads, cache values, encrypted
2656/// envelopes).
2657pub struct BytesCol<T> {
2658    pub(crate) name: &'static str,
2659    _phantom: PhantomData<T>,
2660}
2661
2662impl<T> BytesCol<T> {
2663    pub const fn new(name: &'static str) -> Self {
2664        Self {
2665            name,
2666            _phantom: PhantomData,
2667        }
2668    }
2669
2670    /// SQL `=`. Borrows the byte slice into a sea_query Value.
2671    pub fn eq(&self, val: &[u8]) -> Predicate<T> {
2672        Predicate::new(Expr::col(Alias::new(self.name)).eq(val.to_vec()))
2673    }
2674
2675    /// SQL `<>`.
2676    pub fn ne(&self, val: &[u8]) -> Predicate<T> {
2677        Predicate::new(Expr::col(Alias::new(self.name)).ne(val.to_vec()))
2678    }
2679
2680    /// SQL `ORDER BY ... ASC`.
2681    pub fn asc(&self) -> OrderExpr<T> {
2682        OrderExpr::new(self.name, false)
2683    }
2684
2685    /// SQL `ORDER BY ... DESC`.
2686    pub fn desc(&self) -> OrderExpr<T> {
2687        OrderExpr::new(self.name, true)
2688    }
2689}
2690
2691/// `Option<Vec<u8>>` column. Same surface plus `is_null` / `is_not_null`.
2692pub struct NullableBytesCol<T> {
2693    pub(crate) name: &'static str,
2694    _phantom: PhantomData<T>,
2695}
2696
2697impl<T> NullableBytesCol<T> {
2698    pub const fn new(name: &'static str) -> Self {
2699        Self {
2700            name,
2701            _phantom: PhantomData,
2702        }
2703    }
2704
2705    pub fn eq(&self, val: &[u8]) -> Predicate<T> {
2706        Predicate::new(Expr::col(Alias::new(self.name)).eq(val.to_vec()))
2707    }
2708
2709    pub fn ne(&self, val: &[u8]) -> Predicate<T> {
2710        Predicate::new(Expr::col(Alias::new(self.name)).ne(val.to_vec()))
2711    }
2712
2713    /// SQL `IS NULL`.
2714    pub fn is_null(&self) -> Predicate<T> {
2715        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
2716    }
2717
2718    /// SQL `IS NOT NULL`.
2719    pub fn is_not_null(&self) -> Predicate<T> {
2720        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
2721    }
2722
2723    pub fn asc(&self) -> OrderExpr<T> {
2724        OrderExpr::new(self.name, false)
2725    }
2726
2727    pub fn desc(&self) -> OrderExpr<T> {
2728        OrderExpr::new(self.name, true)
2729    }
2730}
2731
2732// =============================================================================
2733// DecimalCol — rust_decimal::Decimal / NUMERIC(19, 4) columns.
2734// =============================================================================
2735
2736/// A fixed-point `NUMERIC(19, 4)` column carrying `rust_decimal::Decimal`.
2737/// Decimal is Postgres-only at v1, but the predicate surface follows the
2738/// numeric columns: comparisons, equality, and ordering.
2739pub struct DecimalCol<T> {
2740    pub(crate) name: &'static str,
2741    _phantom: PhantomData<T>,
2742}
2743
2744impl<T> DecimalCol<T> {
2745    pub const fn new(name: &'static str) -> Self {
2746        Self {
2747            name,
2748            _phantom: PhantomData,
2749        }
2750    }
2751
2752    /// SQL `=`.
2753    pub fn eq(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2754        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
2755    }
2756
2757    /// SQL `<>`.
2758    pub fn ne(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2759        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
2760    }
2761
2762    /// SQL `<`.
2763    pub fn lt(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2764        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
2765    }
2766
2767    /// SQL `<=`.
2768    pub fn le(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2769        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
2770    }
2771
2772    /// Lookup-style alias for [`Self::le`].
2773    pub fn lte(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2774        self.le(val)
2775    }
2776
2777    /// SQL `>`.
2778    pub fn gt(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2779        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
2780    }
2781
2782    /// SQL `>=`.
2783    pub fn ge(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2784        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
2785    }
2786
2787    /// Lookup-style alias for [`Self::ge`].
2788    pub fn gte(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2789        self.ge(val)
2790    }
2791
2792    /// SQL `ORDER BY ... ASC`.
2793    pub fn asc(&self) -> OrderExpr<T> {
2794        OrderExpr::new(self.name, false)
2795    }
2796
2797    /// SQL `ORDER BY ... DESC`.
2798    pub fn desc(&self) -> OrderExpr<T> {
2799        OrderExpr::new(self.name, true)
2800    }
2801}
2802
2803// =============================================================================
2804// NullableDecimalCol — Option<rust_decimal::Decimal> / nullable NUMERIC(19,4).
2805// =============================================================================
2806
2807/// A nullable fixed-point `NUMERIC(19, 4)` column. Mirrors [`DecimalCol`] with
2808/// `is_null` / `is_not_null` and `Option<Decimal>`-flavoured eq/ne predicates.
2809/// Decimal is Postgres-only at v1 (rust_decimal only implements the sqlx encode
2810/// / decode traits for Postgres). Closes the nullable half of gaps2 #70.
2811pub struct NullableDecimalCol<T> {
2812    pub(crate) name: &'static str,
2813    _phantom: PhantomData<T>,
2814}
2815
2816impl<T> NullableDecimalCol<T> {
2817    pub const fn new(name: &'static str) -> Self {
2818        Self {
2819            name,
2820            _phantom: PhantomData,
2821        }
2822    }
2823
2824    /// SQL `=`.
2825    pub fn eq(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2826        Predicate::new(Expr::col(Alias::new(self.name)).eq(val))
2827    }
2828
2829    /// SQL `<>`.
2830    pub fn ne(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2831        Predicate::new(Expr::col(Alias::new(self.name)).ne(val))
2832    }
2833
2834    /// SQL `<`.
2835    pub fn lt(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2836        Predicate::new(Expr::col(Alias::new(self.name)).lt(val))
2837    }
2838
2839    /// SQL `<=`.
2840    pub fn le(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2841        Predicate::new(Expr::col(Alias::new(self.name)).lte(val))
2842    }
2843
2844    /// Lookup-style alias for [`Self::le`].
2845    pub fn lte(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2846        self.le(val)
2847    }
2848
2849    /// SQL `>`.
2850    pub fn gt(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2851        Predicate::new(Expr::col(Alias::new(self.name)).gt(val))
2852    }
2853
2854    /// SQL `>=`.
2855    pub fn ge(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2856        Predicate::new(Expr::col(Alias::new(self.name)).gte(val))
2857    }
2858
2859    /// Lookup-style alias for [`Self::ge`].
2860    pub fn gte(&self, val: rust_decimal::Decimal) -> Predicate<T> {
2861        self.ge(val)
2862    }
2863
2864    /// SQL `IS NULL`.
2865    pub fn is_null(&self) -> Predicate<T> {
2866        Predicate::new(Expr::col(Alias::new(self.name)).is_null())
2867    }
2868
2869    /// SQL `IS NOT NULL`.
2870    pub fn is_not_null(&self) -> Predicate<T> {
2871        Predicate::new(Expr::col(Alias::new(self.name)).is_not_null())
2872    }
2873
2874    /// SQL `ORDER BY ... ASC`.
2875    pub fn asc(&self) -> OrderExpr<T> {
2876        OrderExpr::new(self.name, false)
2877    }
2878
2879    /// SQL `ORDER BY ... DESC`.
2880    pub fn desc(&self) -> OrderExpr<T> {
2881        OrderExpr::new(self.name, true)
2882    }
2883}
2884
2885// =========================================================================
2886// Gap #24 + #36 — DB-function helpers (`ColExpr<T>`)
2887//
2888// Column extension methods (`StrCol::lower`, `DateTimeCol::year`, ...)
2889// return a `ColExpr<T>` so the caller can pick the comparison
2890// operator: `post::TITLE.lower().eq(...)`,
2891// `post::CREATED_AT.year().lt(2026)`. `ColExpr<T>` carries a primary
2892// `SimpleExpr` plus an optional SQLite-specific override (same
2893// dual-rendering pattern `Predicate<T>` uses); date-extract needs
2894// this so it can emit `EXTRACT(YEAR FROM …)` on Postgres and
2895// `CAST(strftime('%Y', …) AS INTEGER)` on SQLite from a single
2896// `ColExpr`.
2897// =========================================================================
2898
2899/// A backend-aware expression that hasn't been compared yet. Built by
2900/// the column extension methods (`.lower()`, `.year()`, etc.) and
2901/// finalised by calling a comparison operator (`.eq`, `.lt`, etc.) to
2902/// produce a `Predicate<T>`.
2903pub struct ColExpr<T> {
2904    expr: sea_query::SimpleExpr,
2905    expr_sqlite: Option<sea_query::SimpleExpr>,
2906    _phantom: PhantomData<T>,
2907}
2908
2909impl<T> ColExpr<T> {
2910    /// Construct a single-form expression (same SQL on every backend).
2911    pub(crate) fn new(expr: sea_query::SimpleExpr) -> Self {
2912        Self {
2913            expr,
2914            expr_sqlite: None,
2915            _phantom: PhantomData,
2916        }
2917    }
2918
2919    /// Construct an expression that renders differently on SQLite vs
2920    /// Postgres. The default `expr` is the Postgres form; `sqlite` is
2921    /// substituted at terminal time when the resolved pool is SQLite.
2922    pub(crate) fn new_with_sqlite(
2923        expr: sea_query::SimpleExpr,
2924        sqlite: sea_query::SimpleExpr,
2925    ) -> Self {
2926        Self {
2927            expr,
2928            expr_sqlite: Some(sqlite),
2929            _phantom: PhantomData,
2930        }
2931    }
2932
2933    /// Internal: build a `Predicate` by applying the supplied
2934    /// operator to both expression forms in parallel.
2935    fn into_predicate<F>(self, op: F) -> Predicate<T>
2936    where
2937        F: Fn(sea_query::SimpleExpr) -> sea_query::SimpleExpr,
2938    {
2939        let cond = op(self.expr);
2940        let cond_sqlite = self.expr_sqlite.map(&op);
2941        match cond_sqlite {
2942            Some(sql) => Predicate::new_with_sqlite(cond, sql),
2943            None => Predicate::new(cond),
2944        }
2945    }
2946
2947    /// `<expr> = value`.
2948    pub fn eq<V: Into<sea_query::Value>>(self, val: V) -> Predicate<T> {
2949        let val = val.into();
2950        self.into_predicate(move |e| e.eq(val.clone()))
2951    }
2952
2953    /// `<expr> <> value`.
2954    pub fn ne<V: Into<sea_query::Value>>(self, val: V) -> Predicate<T> {
2955        let val = val.into();
2956        self.into_predicate(move |e| e.ne(val.clone()))
2957    }
2958
2959    /// `<expr> < value`.
2960    pub fn lt<V: Into<sea_query::Value>>(self, val: V) -> Predicate<T> {
2961        let val = val.into();
2962        self.into_predicate(move |e| e.lt(val.clone()))
2963    }
2964
2965    /// `<expr> <= value`.
2966    pub fn le<V: Into<sea_query::Value>>(self, val: V) -> Predicate<T> {
2967        let val = val.into();
2968        self.into_predicate(move |e| e.lte(val.clone()))
2969    }
2970
2971    /// `<expr> > value`.
2972    pub fn gt<V: Into<sea_query::Value>>(self, val: V) -> Predicate<T> {
2973        let val = val.into();
2974        self.into_predicate(move |e| e.gt(val.clone()))
2975    }
2976
2977    /// `<expr> >= value`.
2978    pub fn ge<V: Into<sea_query::Value>>(self, val: V) -> Predicate<T> {
2979        let val = val.into();
2980        self.into_predicate(move |e| e.gte(val.clone()))
2981    }
2982}
2983
2984/// String-function helpers — `lower()`, `upper()`, `length()`, `trim()`,
2985/// `coalesce()`, `concat()`. Implemented for both `StrCol<T>` and
2986/// `NullableStrCol<T>` so the extension methods work whether the column is
2987/// `String` or `Option<String>`.
2988///
2989/// Each returns a [`ColExpr`]; chain a comparison (`.eq` / `.ne` / `.lt`
2990/// …) to produce a `Predicate<T>` for `filter` / `exclude`. All six render
2991/// identically on SQLite and Postgres (`TRIM`, `COALESCE` are standard
2992/// SQL; `||` is the standard concatenation operator both backends accept).
2993pub trait StrColExt<T> {
2994    /// `LOWER(col)` — case-insensitive comparison primitive.
2995    fn lower(&self) -> ColExpr<T>;
2996    /// `UPPER(col)`.
2997    fn upper(&self) -> ColExpr<T>;
2998    /// `LENGTH(col)` — character count of the stored value.
2999    fn length(&self) -> ColExpr<T>;
3000    /// `TRIM(col)` — strip leading/trailing whitespace before comparing,
3001    /// so `name.trim().eq("ada")` matches a stored `" ada "`.
3002    fn trim(&self) -> ColExpr<T>;
3003    /// `COALESCE(col, default)` — substitute `default` when the column is
3004    /// NULL, so a nullable column compares as the fallback. Mostly paired
3005    /// with `NullableStrCol`.
3006    fn coalesce<V: Into<sea_query::Value>>(&self, default: V) -> ColExpr<T>;
3007    /// `col || suffix` — append `suffix` (the standard SQL concatenation
3008    /// operator, which both backends accept) before comparing.
3009    fn concat<V: Into<sea_query::Value>>(&self, suffix: V) -> ColExpr<T>;
3010}
3011
3012/// `TRIM("col")`. No bound values; same SQL on both backends.
3013fn str_trim_expr(name: &'static str) -> sea_query::SimpleExpr {
3014    Expr::cust(format!("TRIM(\"{}\")", name.replace('"', "\"\"")))
3015}
3016
3017/// `COALESCE("col", default)` built as a native sea-query function so the
3018/// bound `default` is ordered alongside any later comparison value by
3019/// sea-query itself (mixing `cust_with_values`' embedded params with a
3020/// builder-added `.eq` value swaps their bind order).
3021fn str_coalesce_expr(name: &'static str, default: sea_query::Value) -> sea_query::SimpleExpr {
3022    let col: sea_query::SimpleExpr = Expr::col(Alias::new(name)).into();
3023    let def: sea_query::SimpleExpr = Expr::val(default).into();
3024    Func::coalesce([col, def]).into()
3025}
3026
3027/// `"col" || suffix` via the standard concatenation operator `||` (which
3028/// both backends accept) as a native binary expr, so the bound `suffix`
3029/// orders correctly with a later comparison value.
3030fn str_concat_expr(name: &'static str, suffix: sea_query::Value) -> sea_query::SimpleExpr {
3031    Expr::col(Alias::new(name)).binary(sea_query::BinOper::Custom("||"), Expr::val(suffix))
3032}
3033
3034impl<T> StrColExt<T> for StrCol<T> {
3035    fn lower(&self) -> ColExpr<T> {
3036        ColExpr::new(Func::lower(Expr::col(Alias::new(self.name))).into())
3037    }
3038    fn upper(&self) -> ColExpr<T> {
3039        ColExpr::new(Func::upper(Expr::col(Alias::new(self.name))).into())
3040    }
3041    fn length(&self) -> ColExpr<T> {
3042        ColExpr::new(Func::char_length(Expr::col(Alias::new(self.name))).into())
3043    }
3044    fn trim(&self) -> ColExpr<T> {
3045        ColExpr::new(str_trim_expr(self.name))
3046    }
3047    fn coalesce<V: Into<sea_query::Value>>(&self, default: V) -> ColExpr<T> {
3048        ColExpr::new(str_coalesce_expr(self.name, default.into()))
3049    }
3050    fn concat<V: Into<sea_query::Value>>(&self, suffix: V) -> ColExpr<T> {
3051        ColExpr::new(str_concat_expr(self.name, suffix.into()))
3052    }
3053}
3054
3055impl<T> StrColExt<T> for NullableStrCol<T> {
3056    fn lower(&self) -> ColExpr<T> {
3057        ColExpr::new(Func::lower(Expr::col(Alias::new(self.name))).into())
3058    }
3059    fn upper(&self) -> ColExpr<T> {
3060        ColExpr::new(Func::upper(Expr::col(Alias::new(self.name))).into())
3061    }
3062    fn length(&self) -> ColExpr<T> {
3063        ColExpr::new(Func::char_length(Expr::col(Alias::new(self.name))).into())
3064    }
3065    fn trim(&self) -> ColExpr<T> {
3066        ColExpr::new(str_trim_expr(self.name))
3067    }
3068    fn coalesce<V: Into<sea_query::Value>>(&self, default: V) -> ColExpr<T> {
3069        ColExpr::new(str_coalesce_expr(self.name, default.into()))
3070    }
3071    fn concat<V: Into<sea_query::Value>>(&self, suffix: V) -> ColExpr<T> {
3072        ColExpr::new(str_concat_expr(self.name, suffix.into()))
3073    }
3074}
3075
3076/// Date-extract helpers — `year()`, `month()`, `day()`.
3077///
3078/// Backend dispatch is hidden inside the returned [`ColExpr`]: the
3079/// Postgres form uses `CAST(EXTRACT(<part> FROM col) AS INTEGER)`;
3080/// the SQLite form uses `CAST(strftime('<fmt>', col) AS INTEGER)`.
3081/// Both forms land in the same `ColExpr`; `Predicate` picks the
3082/// right one at terminal time based on the resolved pool.
3083pub trait DateTimeColExt<T> {
3084    /// Year as an integer (e.g. 2026).
3085    fn year(&self) -> ColExpr<T>;
3086    /// Month of year, 1..=12.
3087    fn month(&self) -> ColExpr<T>;
3088    /// Day of month, 1..=31.
3089    fn day(&self) -> ColExpr<T>;
3090    /// Hour of day, 0..=23.
3091    fn hour(&self) -> ColExpr<T>;
3092    /// Minute of hour, 0..=59.
3093    fn minute(&self) -> ColExpr<T>;
3094    /// Second of minute, 0..=59 (whole seconds; subsecond fragments
3095    /// are truncated by the cast).
3096    fn second(&self) -> ColExpr<T>;
3097    /// Day of week. **Numbering differs by backend** to keep each
3098    /// dialect's native form: Postgres `EXTRACT(DOW ...)` returns
3099    /// 0=Sunday..6=Saturday; SQLite `strftime('%w', ...)` matches
3100    /// that numbering too, so both backends agree. Use this for
3101    /// "rows posted on weekends" / "rows posted on a Friday" style
3102    /// queries — compare against the integer (`week_day().eq(5)`
3103    /// for Friday).
3104    fn week_day(&self) -> ColExpr<T>;
3105}
3106
3107fn date_part_exprs(
3108    col_name: &str,
3109    part_pg: &'static str,
3110    fmt_sqlite: &'static str,
3111) -> (sea_query::SimpleExpr, sea_query::SimpleExpr) {
3112    let pg = sea_query::SimpleExpr::Custom(format!(
3113        "CAST(EXTRACT({part_pg} FROM \"{col_name}\") AS INTEGER)"
3114    ));
3115    let sqlite = sea_query::SimpleExpr::Custom(format!(
3116        "CAST(strftime('{fmt_sqlite}', \"{col_name}\") AS INTEGER)"
3117    ));
3118    (pg, sqlite)
3119}
3120
3121impl<T> DateTimeColExt<T> for DateTimeCol<T> {
3122    fn year(&self) -> ColExpr<T> {
3123        let (pg, sqlite) = date_part_exprs(self.name, "YEAR", "%Y");
3124        ColExpr::new_with_sqlite(pg, sqlite)
3125    }
3126    fn month(&self) -> ColExpr<T> {
3127        let (pg, sqlite) = date_part_exprs(self.name, "MONTH", "%m");
3128        ColExpr::new_with_sqlite(pg, sqlite)
3129    }
3130    fn day(&self) -> ColExpr<T> {
3131        let (pg, sqlite) = date_part_exprs(self.name, "DAY", "%d");
3132        ColExpr::new_with_sqlite(pg, sqlite)
3133    }
3134    fn hour(&self) -> ColExpr<T> {
3135        let (pg, sqlite) = date_part_exprs(self.name, "HOUR", "%H");
3136        ColExpr::new_with_sqlite(pg, sqlite)
3137    }
3138    fn minute(&self) -> ColExpr<T> {
3139        let (pg, sqlite) = date_part_exprs(self.name, "MINUTE", "%M");
3140        ColExpr::new_with_sqlite(pg, sqlite)
3141    }
3142    fn second(&self) -> ColExpr<T> {
3143        let (pg, sqlite) = date_part_exprs(self.name, "SECOND", "%S");
3144        ColExpr::new_with_sqlite(pg, sqlite)
3145    }
3146    fn week_day(&self) -> ColExpr<T> {
3147        let (pg, sqlite) = date_part_exprs(self.name, "DOW", "%w");
3148        ColExpr::new_with_sqlite(pg, sqlite)
3149    }
3150}
3151
3152impl<T> DateTimeColExt<T> for NullableDateTimeCol<T> {
3153    fn year(&self) -> ColExpr<T> {
3154        let (pg, sqlite) = date_part_exprs(self.name, "YEAR", "%Y");
3155        ColExpr::new_with_sqlite(pg, sqlite)
3156    }
3157    fn month(&self) -> ColExpr<T> {
3158        let (pg, sqlite) = date_part_exprs(self.name, "MONTH", "%m");
3159        ColExpr::new_with_sqlite(pg, sqlite)
3160    }
3161    fn day(&self) -> ColExpr<T> {
3162        let (pg, sqlite) = date_part_exprs(self.name, "DAY", "%d");
3163        ColExpr::new_with_sqlite(pg, sqlite)
3164    }
3165    fn hour(&self) -> ColExpr<T> {
3166        let (pg, sqlite) = date_part_exprs(self.name, "HOUR", "%H");
3167        ColExpr::new_with_sqlite(pg, sqlite)
3168    }
3169    fn minute(&self) -> ColExpr<T> {
3170        let (pg, sqlite) = date_part_exprs(self.name, "MINUTE", "%M");
3171        ColExpr::new_with_sqlite(pg, sqlite)
3172    }
3173    fn second(&self) -> ColExpr<T> {
3174        let (pg, sqlite) = date_part_exprs(self.name, "SECOND", "%S");
3175        ColExpr::new_with_sqlite(pg, sqlite)
3176    }
3177    fn week_day(&self) -> ColExpr<T> {
3178        let (pg, sqlite) = date_part_exprs(self.name, "DOW", "%w");
3179        ColExpr::new_with_sqlite(pg, sqlite)
3180    }
3181}