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