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