Skip to main content

drizzle_core/expr/
cmp.rs

1//! Type-safe comparison functions.
2//!
3//! This module provides both function-based and method-based comparisons:
4//!
5//! ```rust
6//! # let _ = r####"
7//! // Function style
8//! eq(users.id, 42)
9//! gt(users.age, 18)
10//!
11//! // Method style (on SQLExpr)
12//! users.id.eq(42)
13//! users.age.gt(18)
14//! # "####;
15//! ```
16//!
17//! # Type Safety
18//!
19//! - `eq`, `neq`, `gt`, `gte`, `lt`, `lte`: Require compatible types
20//! - `like`, `not_like`: Require textual types on both sides
21//! - `between`: Requires expr compatible with both bounds
22//! - `is_null`, `is_not_null`: No type constraint (any type can be null-checked)
23
24use crate::dialect::DialectTypes;
25use crate::sql::{SQL, Token};
26use crate::traits::SQLParam;
27use crate::types::{Compatible, DataType, Textual};
28
29use super::{AggOr, AggregateKind, Expr, NonNull, SQLExpr};
30
31// =============================================================================
32// Internal Helper
33// =============================================================================
34
35fn binary_op<'a, V, L, R>(left: L, operator: Token, right: R) -> SQL<'a, V>
36where
37    V: SQLParam + 'a,
38    L: Expr<'a, V>,
39    R: ComparisonOperand<'a, V, L::SQLType>,
40{
41    let left_sql = operand_sql(left);
42    let right_sql = right.into_comparison_sql();
43
44    left_sql.push(operator).append(right_sql)
45}
46
47#[inline]
48fn operand_sql<'a, V, T>(value: T) -> SQL<'a, V>
49where
50    V: SQLParam + 'a,
51    T: Expr<'a, V>,
52{
53    value.into_expr_sql()
54}
55
56/// Type-safe operand for comparison functions.
57///
58/// This trait exists as an indirection layer between comparison functions
59/// (`eq`, `gt`, `like`, etc.) and the `Expr` trait. Rather than accepting
60/// any `Expr` directly, comparisons require `ComparisonOperand<'a, V, Expected>`
61/// where `Expected` is the left-hand side's SQL type.
62///
63/// The blanket impl below only fires when `Expected: Compatible<R::SQLType>`,
64/// so passing an incompatible type (e.g. comparing `Int` with `Text`) fails
65/// at compile time with a clear diagnostic.
66pub trait ComparisonOperand<'a, V, Expected>
67where
68    V: SQLParam + 'a,
69    Expected: DataType,
70{
71    type SQLType: DataType;
72    type Aggregate: AggregateKind;
73
74    fn into_comparison_sql(self) -> SQL<'a, V>;
75}
76
77impl<'a, V, Expected, R> ComparisonOperand<'a, V, Expected> for R
78where
79    V: SQLParam + 'a,
80    Expected: DataType + Compatible<R::SQLType>,
81    R: Expr<'a, V>,
82{
83    type SQLType = R::SQLType;
84    type Aggregate = R::Aggregate;
85
86    fn into_comparison_sql(self) -> SQL<'a, V> {
87        self.into_expr_sql()
88    }
89}
90
91// =============================================================================
92// Equality Comparisons
93// =============================================================================
94
95/// Equality comparison (`=`).
96///
97/// Requires both operands to have compatible SQL types.
98///
99/// # Type Safety
100///
101/// ```rust
102/// # let _ = r####"
103/// // ✅ OK: Int compared with i32
104/// eq(users.id, 10);
105///
106/// // ✅ OK: Int compared with BigInt (integer family)
107/// eq(users.id, users.big_id);
108///
109/// // ❌ Compile error: Int cannot be compared with Text
110/// eq(users.id, "hello");
111/// # "####;
112/// ```
113#[allow(clippy::type_complexity)]
114pub fn eq<'a, V, L, R>(
115    left: L,
116    right: R,
117) -> SQLExpr<
118    'a,
119    V,
120    <V::DialectMarker as DialectTypes>::Bool,
121    NonNull,
122    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>>::Output,
123>
124where
125    V: SQLParam + 'a,
126    L: Expr<'a, V>,
127    R: ComparisonOperand<'a, V, L::SQLType>,
128    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>,
129{
130    SQLExpr::new(binary_op(left, Token::EQ, right))
131}
132
133/// Inequality comparison (`<>` or `!=`).
134///
135/// Requires both operands to have compatible SQL types.
136#[allow(clippy::type_complexity)]
137pub fn neq<'a, V, L, R>(
138    left: L,
139    right: R,
140) -> SQLExpr<
141    'a,
142    V,
143    <V::DialectMarker as DialectTypes>::Bool,
144    NonNull,
145    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>>::Output,
146>
147where
148    V: SQLParam + 'a,
149    L: Expr<'a, V>,
150    R: ComparisonOperand<'a, V, L::SQLType>,
151    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>,
152{
153    SQLExpr::new(binary_op(left, Token::NE, right))
154}
155
156// =============================================================================
157// Ordering Comparisons
158// =============================================================================
159
160/// Greater-than comparison (`>`).
161///
162/// Requires both operands to have compatible SQL types.
163#[allow(clippy::type_complexity)]
164pub fn gt<'a, V, L, R>(
165    left: L,
166    right: R,
167) -> SQLExpr<
168    'a,
169    V,
170    <V::DialectMarker as DialectTypes>::Bool,
171    NonNull,
172    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>>::Output,
173>
174where
175    V: SQLParam + 'a,
176    L: Expr<'a, V>,
177    R: ComparisonOperand<'a, V, L::SQLType>,
178    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>,
179{
180    SQLExpr::new(binary_op(left, Token::GT, right))
181}
182
183/// Greater-than-or-equal comparison (`>=`).
184///
185/// Requires both operands to have compatible SQL types.
186#[allow(clippy::type_complexity)]
187pub fn gte<'a, V, L, R>(
188    left: L,
189    right: R,
190) -> SQLExpr<
191    'a,
192    V,
193    <V::DialectMarker as DialectTypes>::Bool,
194    NonNull,
195    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>>::Output,
196>
197where
198    V: SQLParam + 'a,
199    L: Expr<'a, V>,
200    R: ComparisonOperand<'a, V, L::SQLType>,
201    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>,
202{
203    SQLExpr::new(binary_op(left, Token::GE, right))
204}
205
206/// Less-than comparison (`<`).
207///
208/// Requires both operands to have compatible SQL types.
209#[allow(clippy::type_complexity)]
210pub fn lt<'a, V, L, R>(
211    left: L,
212    right: R,
213) -> SQLExpr<
214    'a,
215    V,
216    <V::DialectMarker as DialectTypes>::Bool,
217    NonNull,
218    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>>::Output,
219>
220where
221    V: SQLParam + 'a,
222    L: Expr<'a, V>,
223    R: ComparisonOperand<'a, V, L::SQLType>,
224    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>,
225{
226    SQLExpr::new(binary_op(left, Token::LT, right))
227}
228
229/// Less-than-or-equal comparison (`<=`).
230///
231/// Requires both operands to have compatible SQL types.
232#[allow(clippy::type_complexity)]
233pub fn lte<'a, V, L, R>(
234    left: L,
235    right: R,
236) -> SQLExpr<
237    'a,
238    V,
239    <V::DialectMarker as DialectTypes>::Bool,
240    NonNull,
241    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>>::Output,
242>
243where
244    V: SQLParam + 'a,
245    L: Expr<'a, V>,
246    R: ComparisonOperand<'a, V, L::SQLType>,
247    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>,
248{
249    SQLExpr::new(binary_op(left, Token::LE, right))
250}
251
252// =============================================================================
253// Pattern Matching
254// =============================================================================
255
256/// LIKE pattern matching.
257///
258/// Requires both operands to be textual types (TEXT, VARCHAR).
259///
260/// # Type Safety
261///
262/// ```rust
263/// # let _ = r####"
264/// // ✅ OK: Text column with text pattern
265/// like(users.name, "%Alice%");
266///
267/// // ❌ Compile error: Int is not Textual
268/// like(users.id, "%123%");
269/// # "####;
270/// ```
271#[allow(clippy::type_complexity)]
272pub fn like<'a, V, L, R>(
273    left: L,
274    pattern: R,
275) -> SQLExpr<
276    'a,
277    V,
278    <V::DialectMarker as DialectTypes>::Bool,
279    NonNull,
280    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>>::Output,
281>
282where
283    V: SQLParam + 'a,
284    L: Expr<'a, V>,
285    R: ComparisonOperand<'a, V, L::SQLType>,
286    L::SQLType: Textual,
287    <R as ComparisonOperand<'a, V, L::SQLType>>::SQLType: Textual,
288    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>,
289{
290    SQLExpr::new(
291        operand_sql(left)
292            .push(Token::LIKE)
293            .append(pattern.into_comparison_sql()),
294    )
295}
296
297/// NOT LIKE pattern matching.
298///
299/// Requires both operands to be textual types (TEXT, VARCHAR).
300#[allow(clippy::type_complexity)]
301pub fn not_like<'a, V, L, R>(
302    left: L,
303    pattern: R,
304) -> SQLExpr<
305    'a,
306    V,
307    <V::DialectMarker as DialectTypes>::Bool,
308    NonNull,
309    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>>::Output,
310>
311where
312    V: SQLParam + 'a,
313    L: Expr<'a, V>,
314    R: ComparisonOperand<'a, V, L::SQLType>,
315    L::SQLType: Textual,
316    <R as ComparisonOperand<'a, V, L::SQLType>>::SQLType: Textual,
317    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>,
318{
319    SQLExpr::new(
320        operand_sql(left)
321            .push(Token::NOT)
322            .push(Token::LIKE)
323            .append(pattern.into_comparison_sql()),
324    )
325}
326
327// =============================================================================
328// Range Comparisons
329// =============================================================================
330
331/// BETWEEN comparison.
332///
333/// Checks if expr is between low and high (inclusive).
334/// Requires expr type to be compatible with both bounds.
335#[allow(clippy::type_complexity)]
336pub fn between<'a, V, E, L, H>(
337    expr: E,
338    low: L,
339    high: H,
340) -> SQLExpr<
341    'a,
342    V,
343    <V::DialectMarker as DialectTypes>::Bool,
344    NonNull,
345    <<E::Aggregate as AggOr<<L as ComparisonOperand<'a, V, E::SQLType>>::Aggregate>>::Output as AggOr<<H as ComparisonOperand<'a, V, E::SQLType>>::Aggregate>>::Output,
346>
347where
348    V: SQLParam + 'a,
349    E: Expr<'a, V>,
350    L: ComparisonOperand<'a, V, E::SQLType>,
351    H: ComparisonOperand<'a, V, E::SQLType>,
352    E::Aggregate: AggOr<<L as ComparisonOperand<'a, V, E::SQLType>>::Aggregate>,
353    <E::Aggregate as AggOr<<L as ComparisonOperand<'a, V, E::SQLType>>::Aggregate>>::Output:
354        AggOr<<H as ComparisonOperand<'a, V, E::SQLType>>::Aggregate>,
355{
356    SQLExpr::new(
357        SQL::from(Token::LPAREN)
358            .append(operand_sql(expr))
359            .push(Token::BETWEEN)
360            .append(low.into_comparison_sql())
361            .push(Token::AND)
362            .append(high.into_comparison_sql())
363            .push(Token::RPAREN),
364    )
365}
366
367/// NOT BETWEEN comparison.
368///
369/// Requires expr type to be compatible with both bounds.
370#[allow(clippy::type_complexity)]
371pub fn not_between<'a, V, E, L, H>(
372    expr: E,
373    low: L,
374    high: H,
375) -> SQLExpr<
376    'a,
377    V,
378    <V::DialectMarker as DialectTypes>::Bool,
379    NonNull,
380    <<E::Aggregate as AggOr<<L as ComparisonOperand<'a, V, E::SQLType>>::Aggregate>>::Output as AggOr<<H as ComparisonOperand<'a, V, E::SQLType>>::Aggregate>>::Output,
381>
382where
383    V: SQLParam + 'a,
384    E: Expr<'a, V>,
385    L: ComparisonOperand<'a, V, E::SQLType>,
386    H: ComparisonOperand<'a, V, E::SQLType>,
387    E::Aggregate: AggOr<<L as ComparisonOperand<'a, V, E::SQLType>>::Aggregate>,
388    <E::Aggregate as AggOr<<L as ComparisonOperand<'a, V, E::SQLType>>::Aggregate>>::Output:
389        AggOr<<H as ComparisonOperand<'a, V, E::SQLType>>::Aggregate>,
390{
391    SQLExpr::new(
392        SQL::from(Token::LPAREN)
393            .append(operand_sql(expr))
394            .push(Token::NOT)
395            .push(Token::BETWEEN)
396            .append(low.into_comparison_sql())
397            .push(Token::AND)
398            .append(high.into_comparison_sql())
399            .push(Token::RPAREN),
400    )
401}
402
403// =============================================================================
404// NULL Checks
405// =============================================================================
406
407/// IS NULL check.
408///
409/// Returns a boolean expression checking if the value is NULL.
410/// Any expression type can be null-checked.
411pub fn is_null<'a, V, E>(
412    expr: E,
413) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, E::Aggregate>
414where
415    V: SQLParam + 'a,
416    E: Expr<'a, V>,
417{
418    SQLExpr::new(operand_sql(expr).push(Token::IS).push(Token::NULL))
419}
420
421/// IS NOT NULL check.
422///
423/// Returns a boolean expression checking if the value is not NULL.
424/// Any expression type can be null-checked.
425pub fn is_not_null<'a, V, E>(
426    expr: E,
427) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, E::Aggregate>
428where
429    V: SQLParam + 'a,
430    E: Expr<'a, V>,
431{
432    SQLExpr::new(
433        operand_sql(expr)
434            .push(Token::IS)
435            .push(Token::NOT)
436            .push(Token::NULL),
437    )
438}
439
440// =============================================================================
441// IS DISTINCT FROM
442// =============================================================================
443
444/// IS DISTINCT FROM - NULL-safe inequality comparison.
445///
446/// Unlike `<>`, this treats NULL as a comparable value:
447/// - `NULL IS DISTINCT FROM NULL` → false
448/// - `NULL IS DISTINCT FROM 5` → true
449/// - `5 IS DISTINCT FROM NULL` → true
450///
451/// Supported by both `SQLite` (3.39+) and `PostgreSQL`.
452#[allow(clippy::type_complexity)]
453pub fn is_distinct_from<'a, V, L, R>(
454    left: L,
455    right: R,
456) -> SQLExpr<
457    'a,
458    V,
459    <V::DialectMarker as DialectTypes>::Bool,
460    NonNull,
461    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>>::Output,
462>
463where
464    V: SQLParam + 'a,
465    L: Expr<'a, V>,
466    R: ComparisonOperand<'a, V, L::SQLType>,
467    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>,
468{
469    SQLExpr::new(
470        operand_sql(left)
471            .push(Token::IS)
472            .push(Token::DISTINCT)
473            .push(Token::FROM)
474            .append(right.into_comparison_sql()),
475    )
476}
477
478/// IS NOT DISTINCT FROM - NULL-safe equality comparison.
479///
480/// Unlike `=`, this treats NULL as a comparable value:
481/// - `NULL IS NOT DISTINCT FROM NULL` → true
482/// - `NULL IS NOT DISTINCT FROM 5` → false
483///
484/// Supported by both `SQLite` (3.39+) and `PostgreSQL`.
485#[allow(clippy::type_complexity)]
486pub fn is_not_distinct_from<'a, V, L, R>(
487    left: L,
488    right: R,
489) -> SQLExpr<
490    'a,
491    V,
492    <V::DialectMarker as DialectTypes>::Bool,
493    NonNull,
494    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>>::Output,
495>
496where
497    V: SQLParam + 'a,
498    L: Expr<'a, V>,
499    R: ComparisonOperand<'a, V, L::SQLType>,
500    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L::SQLType>>::Aggregate>,
501{
502    SQLExpr::new(
503        operand_sql(left)
504            .push(Token::IS)
505            .push(Token::NOT)
506            .push(Token::DISTINCT)
507            .push(Token::FROM)
508            .append(right.into_comparison_sql()),
509    )
510}
511
512// =============================================================================
513// Boolean Testing
514// =============================================================================
515
516/// IS TRUE - tests if a boolean expression is true.
517///
518/// Unlike `= TRUE`, this handles NULL correctly:
519/// - `TRUE IS TRUE` → true
520/// - `FALSE IS TRUE` → false
521/// - `NULL IS TRUE` → false (not NULL!)
522pub fn is_true<'a, V, E>(
523    expr: E,
524) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, E::Aggregate>
525where
526    V: SQLParam + 'a,
527    E: Expr<'a, V>,
528{
529    SQLExpr::new(operand_sql(expr).push(Token::IS).append(SQL::raw("TRUE")))
530}
531
532/// IS FALSE - tests if a boolean expression is false.
533///
534/// Unlike `= FALSE`, this handles NULL correctly:
535/// - `FALSE IS FALSE` → true
536/// - `TRUE IS FALSE` → false
537/// - `NULL IS FALSE` → false (not NULL!)
538pub fn is_false<'a, V, E>(
539    expr: E,
540) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, E::Aggregate>
541where
542    V: SQLParam + 'a,
543    E: Expr<'a, V>,
544{
545    SQLExpr::new(operand_sql(expr).push(Token::IS).append(SQL::raw("FALSE")))
546}
547
548// =============================================================================
549// Method-based Comparison API (Extension Trait)
550// =============================================================================
551
552/// Extension trait providing method-based comparisons for any `Expr` type.
553///
554/// This trait is blanket-implemented for all types implementing `Expr`,
555/// allowing method syntax on columns, literals, and expressions:
556///
557/// ```rust
558/// # let _ = r####"
559/// // Works on columns directly
560/// users.id.eq(42)
561/// users.age.gt(18)
562///
563/// // Chain with operators
564/// users.id.eq(42) & users.age.gt(18)
565/// # "####;
566/// ```
567pub trait ExprExt<'a, V: SQLParam>: Expr<'a, V> + Sized {
568    /// Equality comparison (`=`).
569    ///
570    /// ```rust
571    /// # let _ = r####"
572    /// users.id.eq(42)  // "users"."id" = 42
573    /// # "####;
574    /// ```
575    #[allow(clippy::type_complexity)]
576    fn eq<R>(
577        self,
578        other: R,
579    ) -> SQLExpr<
580        'a,
581        V,
582        <V::DialectMarker as DialectTypes>::Bool,
583        NonNull,
584        <Self::Aggregate as AggOr<
585            <R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
586        >>::Output,
587    >
588    where
589        R: ComparisonOperand<'a, V, Self::SQLType>,
590        Self::Aggregate:
591            AggOr<<R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
592    {
593        eq(self, other)
594    }
595
596    /// Inequality comparison (`<>`).
597    ///
598    /// ```rust
599    /// # let _ = r####"
600    /// users.id.ne(42)  // "users"."id" <> 42
601    /// # "####;
602    /// ```
603    #[allow(clippy::type_complexity)]
604    fn ne<R>(
605        self,
606        other: R,
607    ) -> SQLExpr<
608        'a,
609        V,
610        <V::DialectMarker as DialectTypes>::Bool,
611        NonNull,
612        <Self::Aggregate as AggOr<
613            <R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
614        >>::Output,
615    >
616    where
617        R: ComparisonOperand<'a, V, Self::SQLType>,
618        Self::Aggregate:
619            AggOr<<R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
620    {
621        neq(self, other)
622    }
623
624    /// Greater-than comparison (`>`).
625    ///
626    /// ```rust
627    /// # let _ = r####"
628    /// users.age.gt(18)  // "users"."age" > 18
629    /// # "####;
630    /// ```
631    #[allow(clippy::type_complexity)]
632    fn gt<R>(
633        self,
634        other: R,
635    ) -> SQLExpr<
636        'a,
637        V,
638        <V::DialectMarker as DialectTypes>::Bool,
639        NonNull,
640        <Self::Aggregate as AggOr<
641            <R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
642        >>::Output,
643    >
644    where
645        R: ComparisonOperand<'a, V, Self::SQLType>,
646        Self::Aggregate:
647            AggOr<<R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
648    {
649        gt(self, other)
650    }
651
652    /// Greater-than-or-equal comparison (`>=`).
653    ///
654    /// ```rust
655    /// # let _ = r####"
656    /// users.age.ge(18)  // "users"."age" >= 18
657    /// # "####;
658    /// ```
659    #[allow(clippy::type_complexity)]
660    fn ge<R>(
661        self,
662        other: R,
663    ) -> SQLExpr<
664        'a,
665        V,
666        <V::DialectMarker as DialectTypes>::Bool,
667        NonNull,
668        <Self::Aggregate as AggOr<
669            <R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
670        >>::Output,
671    >
672    where
673        R: ComparisonOperand<'a, V, Self::SQLType>,
674        Self::Aggregate:
675            AggOr<<R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
676    {
677        gte(self, other)
678    }
679
680    /// Less-than comparison (`<`).
681    ///
682    /// ```rust
683    /// # let _ = r####"
684    /// users.age.lt(65)  // "users"."age" < 65
685    /// # "####;
686    /// ```
687    #[allow(clippy::type_complexity)]
688    fn lt<R>(
689        self,
690        other: R,
691    ) -> SQLExpr<
692        'a,
693        V,
694        <V::DialectMarker as DialectTypes>::Bool,
695        NonNull,
696        <Self::Aggregate as AggOr<
697            <R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
698        >>::Output,
699    >
700    where
701        R: ComparisonOperand<'a, V, Self::SQLType>,
702        Self::Aggregate:
703            AggOr<<R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
704    {
705        lt(self, other)
706    }
707
708    /// Less-than-or-equal comparison (`<=`).
709    ///
710    /// ```rust
711    /// # let _ = r####"
712    /// users.age.le(65)  // "users"."age" <= 65
713    /// # "####;
714    /// ```
715    #[allow(clippy::type_complexity)]
716    fn le<R>(
717        self,
718        other: R,
719    ) -> SQLExpr<
720        'a,
721        V,
722        <V::DialectMarker as DialectTypes>::Bool,
723        NonNull,
724        <Self::Aggregate as AggOr<
725            <R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
726        >>::Output,
727    >
728    where
729        R: ComparisonOperand<'a, V, Self::SQLType>,
730        Self::Aggregate:
731            AggOr<<R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
732    {
733        lte(self, other)
734    }
735
736    /// LIKE pattern matching.
737    ///
738    /// ```rust
739    /// # let _ = r####"
740    /// users.name.like("%Alice%")  // "users"."name" LIKE '%Alice%'
741    /// # "####;
742    /// ```
743    #[allow(clippy::type_complexity)]
744    fn like<R>(
745        self,
746        pattern: R,
747    ) -> SQLExpr<
748        'a,
749        V,
750        <V::DialectMarker as DialectTypes>::Bool,
751        NonNull,
752        <Self::Aggregate as AggOr<
753            <R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
754        >>::Output,
755    >
756    where
757        R: ComparisonOperand<'a, V, Self::SQLType>,
758        Self::SQLType: Textual,
759        <R as ComparisonOperand<'a, V, Self::SQLType>>::SQLType: Textual,
760        Self::Aggregate:
761            AggOr<<R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
762    {
763        like(self, pattern)
764    }
765
766    /// NOT LIKE pattern matching.
767    ///
768    /// ```rust
769    /// # let _ = r####"
770    /// users.name.not_like("%Bot%")  // "users"."name" NOT LIKE '%Bot%'
771    /// # "####;
772    /// ```
773    #[allow(clippy::type_complexity)]
774    fn not_like<R>(
775        self,
776        pattern: R,
777    ) -> SQLExpr<
778        'a,
779        V,
780        <V::DialectMarker as DialectTypes>::Bool,
781        NonNull,
782        <Self::Aggregate as AggOr<
783            <R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
784        >>::Output,
785    >
786    where
787        R: ComparisonOperand<'a, V, Self::SQLType>,
788        Self::SQLType: Textual,
789        <R as ComparisonOperand<'a, V, Self::SQLType>>::SQLType: Textual,
790        Self::Aggregate:
791            AggOr<<R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
792    {
793        not_like(self, pattern)
794    }
795
796    /// IS NULL check.
797    ///
798    /// ```rust
799    /// # let _ = r####"
800    /// users.deleted_at.is_null()  // "users"."deleted_at" IS NULL
801    /// # "####;
802    /// ```
803    #[allow(clippy::wrong_self_convention)]
804    fn is_null(
805        self,
806    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate> {
807        is_null(self)
808    }
809
810    /// IS NOT NULL check.
811    ///
812    /// ```rust
813    /// # let _ = r####"
814    /// users.email.is_not_null()  // "users"."email" IS NOT NULL
815    /// # "####;
816    /// ```
817    #[allow(clippy::wrong_self_convention)]
818    fn is_not_null(
819        self,
820    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate> {
821        is_not_null(self)
822    }
823
824    /// BETWEEN comparison.
825    ///
826    /// Checks if the value is between low and high (inclusive).
827    ///
828    /// ```rust
829    /// # let _ = r####"
830    /// users.age.between(18, 65)  // ("users"."age" BETWEEN 18 AND 65)
831    /// # "####;
832    /// ```
833    #[allow(clippy::type_complexity)]
834    fn between<L, H>(
835        self,
836        low: L,
837        high: H,
838    ) -> SQLExpr<
839        'a,
840        V,
841        <V::DialectMarker as DialectTypes>::Bool,
842        NonNull,
843        <<Self::Aggregate as AggOr<
844            <L as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
845        >>::Output as AggOr<
846            <H as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
847        >>::Output,
848    >
849    where
850        L: ComparisonOperand<'a, V, Self::SQLType>,
851        H: ComparisonOperand<'a, V, Self::SQLType>,
852        Self::Aggregate:
853            AggOr<<L as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
854        <Self::Aggregate as AggOr<
855            <L as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
856        >>::Output:
857            AggOr<<H as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
858    {
859        between(self, low, high)
860    }
861
862    /// NOT BETWEEN comparison.
863    ///
864    /// Checks if the value is NOT between low and high.
865    ///
866    /// ```rust
867    /// # let _ = r####"
868    /// users.age.not_between(0, 17)  // ("users"."age" NOT BETWEEN 0 AND 17)
869    /// # "####;
870    /// ```
871    #[allow(clippy::type_complexity)]
872    fn not_between<L, H>(
873        self,
874        low: L,
875        high: H,
876    ) -> SQLExpr<
877        'a,
878        V,
879        <V::DialectMarker as DialectTypes>::Bool,
880        NonNull,
881        <<Self::Aggregate as AggOr<
882            <L as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
883        >>::Output as AggOr<
884            <H as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
885        >>::Output,
886    >
887    where
888        L: ComparisonOperand<'a, V, Self::SQLType>,
889        H: ComparisonOperand<'a, V, Self::SQLType>,
890        Self::Aggregate:
891            AggOr<<L as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
892        <Self::Aggregate as AggOr<
893            <L as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
894        >>::Output:
895            AggOr<<H as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
896    {
897        not_between(self, low, high)
898    }
899
900    /// IN array check.
901    ///
902    /// Checks if the value is in the provided array.
903    ///
904    /// ```rust
905    /// # let _ = r####"
906    /// users.role.in_array([Role::Admin, Role::Moderator])
907    /// // "users"."role" IN ('admin', 'moderator')
908    /// # "####;
909    /// ```
910    fn in_array<I, R>(
911        self,
912        values: I,
913    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate>
914    where
915        I: IntoIterator<Item = R>,
916        R: Expr<'a, V>,
917        Self::SQLType: Compatible<R::SQLType>,
918    {
919        crate::expr::in_array(self, values)
920    }
921
922    /// NOT IN array check.
923    ///
924    /// Checks if the value is NOT in the provided array.
925    ///
926    /// ```rust
927    /// # let _ = r####"
928    /// users.role.not_in_array([Role::Banned, Role::Suspended])
929    /// // "users"."role" NOT IN ('banned', 'suspended')
930    /// # "####;
931    /// ```
932    fn not_in_array<I, R>(
933        self,
934        values: I,
935    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate>
936    where
937        I: IntoIterator<Item = R>,
938        R: Expr<'a, V>,
939        Self::SQLType: Compatible<R::SQLType>,
940    {
941        crate::expr::not_in_array(self, values)
942    }
943
944    /// IN subquery check.
945    fn in_subquery<S>(
946        self,
947        subquery: S,
948    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate>
949    where
950        S: Expr<'a, V>,
951        Self::SQLType: Compatible<S::SQLType>,
952    {
953        crate::expr::in_subquery(self, subquery)
954    }
955
956    /// NOT IN subquery check.
957    fn not_in_subquery<S>(
958        self,
959        subquery: S,
960    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate>
961    where
962        S: Expr<'a, V>,
963        Self::SQLType: Compatible<S::SQLType>,
964    {
965        crate::expr::not_in_subquery(self, subquery)
966    }
967
968    /// IS DISTINCT FROM - NULL-safe inequality comparison.
969    ///
970    /// ```rust
971    /// # let _ = r####"
972    /// users.status.is_distinct_from("active")
973    /// // "users"."status" IS DISTINCT FROM 'active'
974    /// # "####;
975    /// ```
976    #[allow(clippy::type_complexity, clippy::wrong_self_convention)]
977    fn is_distinct_from<R>(
978        self,
979        other: R,
980    ) -> SQLExpr<
981        'a,
982        V,
983        <V::DialectMarker as DialectTypes>::Bool,
984        NonNull,
985        <Self::Aggregate as AggOr<
986            <R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
987        >>::Output,
988    >
989    where
990        R: ComparisonOperand<'a, V, Self::SQLType>,
991        Self::Aggregate:
992            AggOr<<R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
993    {
994        is_distinct_from(self, other)
995    }
996
997    /// IS NOT DISTINCT FROM - NULL-safe equality comparison.
998    ///
999    /// ```rust
1000    /// # let _ = r####"
1001    /// users.status.is_not_distinct_from("active")
1002    /// // "users"."status" IS NOT DISTINCT FROM 'active'
1003    /// # "####;
1004    /// ```
1005    #[allow(clippy::type_complexity, clippy::wrong_self_convention)]
1006    fn is_not_distinct_from<R>(
1007        self,
1008        other: R,
1009    ) -> SQLExpr<
1010        'a,
1011        V,
1012        <V::DialectMarker as DialectTypes>::Bool,
1013        NonNull,
1014        <Self::Aggregate as AggOr<
1015            <R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate,
1016        >>::Output,
1017    >
1018    where
1019        R: ComparisonOperand<'a, V, Self::SQLType>,
1020        Self::Aggregate:
1021            AggOr<<R as ComparisonOperand<'a, V, Self::SQLType>>::Aggregate>,
1022    {
1023        is_not_distinct_from(self, other)
1024    }
1025
1026    /// IS TRUE - boolean test that handles NULL.
1027    ///
1028    /// ```rust
1029    /// # let _ = r####"
1030    /// users.is_active.is_true()
1031    /// // "users"."is_active" IS TRUE
1032    /// # "####;
1033    /// ```
1034    #[allow(clippy::wrong_self_convention)]
1035    fn is_true(
1036        self,
1037    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate> {
1038        is_true(self)
1039    }
1040
1041    /// IS FALSE - boolean test that handles NULL.
1042    ///
1043    /// ```rust
1044    /// # let _ = r####"
1045    /// users.is_active.is_false()
1046    /// // "users"."is_active" IS FALSE
1047    /// # "####;
1048    /// ```
1049    #[allow(clippy::wrong_self_convention)]
1050    fn is_false(
1051        self,
1052    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate> {
1053        is_false(self)
1054    }
1055}
1056
1057/// Blanket implementation for all `Expr` types.
1058impl<'a, V: SQLParam, E: Expr<'a, V>> ExprExt<'a, V> for E {}