drizzle_core/expr/
cmp.rs

1//! Type-safe comparison functions.
2//!
3//! This module provides both function-based and method-based comparisons:
4//!
5//! ```ignore
6//! // Function style
7//! eq(users.id, 42)
8//! gt(users.age, 18)
9//!
10//! // Method style (on SQLExpr)
11//! users.id.eq(42)
12//! users.age.gt(18)
13//! ```
14//!
15//! # Type Safety
16//!
17//! - `eq`, `neq`, `gt`, `gte`, `lt`, `lte`: Require compatible types
18//! - `like`, `not_like`: Require textual types on both sides
19//! - `between`: Requires expr compatible with both bounds
20//! - `is_null`, `is_not_null`: No type constraint (any type can be null-checked)
21
22use crate::sql::{SQL, Token};
23use crate::traits::{SQLParam, ToSQL};
24use crate::types::{Bool, Compatible, Textual};
25
26use super::{Expr, NonNull, SQLExpr, Scalar};
27
28// =============================================================================
29// Internal Helper
30// =============================================================================
31
32fn binary_op<'a, V, L, R>(left: L, operator: Token, right: R) -> SQL<'a, V>
33where
34    V: SQLParam + 'a,
35    L: ToSQL<'a, V>,
36    R: ToSQL<'a, V>,
37{
38    let right_sql = right.to_sql();
39    // Wrap subqueries (starting with SELECT) in parentheses
40    let right_sql = if right_sql.is_subquery() {
41        right_sql.parens()
42    } else {
43        right_sql
44    };
45    left.to_sql().push(operator).append(right_sql)
46}
47
48// =============================================================================
49// Equality Comparisons
50// =============================================================================
51
52/// Equality comparison (`=`).
53///
54/// Requires both operands to have compatible SQL types.
55///
56/// # Type Safety
57///
58/// ```ignore
59/// // ✅ OK: Int compared with i32
60/// eq(users.id, 10);
61///
62/// // ✅ OK: Int compared with BigInt (integer family)
63/// eq(users.id, users.big_id);
64///
65/// // ❌ Compile error: Int cannot be compared with Text
66/// eq(users.id, "hello");
67/// ```
68pub fn eq<'a, V, L, R>(left: L, right: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
69where
70    V: SQLParam + 'a,
71    L: Expr<'a, V>,
72    R: Expr<'a, V>,
73    L::SQLType: Compatible<R::SQLType>,
74{
75    SQLExpr::new(binary_op(left, Token::EQ, right))
76}
77
78/// Inequality comparison (`<>` or `!=`).
79///
80/// Requires both operands to have compatible SQL types.
81pub fn neq<'a, V, L, R>(left: L, right: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
82where
83    V: SQLParam + 'a,
84    L: Expr<'a, V>,
85    R: Expr<'a, V>,
86    L::SQLType: Compatible<R::SQLType>,
87{
88    SQLExpr::new(binary_op(left, Token::NE, right))
89}
90
91// =============================================================================
92// Ordering Comparisons
93// =============================================================================
94
95/// Greater-than comparison (`>`).
96///
97/// Requires both operands to have compatible SQL types.
98pub fn gt<'a, V, L, R>(left: L, right: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
99where
100    V: SQLParam + 'a,
101    L: Expr<'a, V>,
102    R: Expr<'a, V>,
103    L::SQLType: Compatible<R::SQLType>,
104{
105    SQLExpr::new(binary_op(left, Token::GT, right))
106}
107
108/// Greater-than-or-equal comparison (`>=`).
109///
110/// Requires both operands to have compatible SQL types.
111pub fn gte<'a, V, L, R>(left: L, right: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
112where
113    V: SQLParam + 'a,
114    L: Expr<'a, V>,
115    R: Expr<'a, V>,
116    L::SQLType: Compatible<R::SQLType>,
117{
118    SQLExpr::new(binary_op(left, Token::GE, right))
119}
120
121/// Less-than comparison (`<`).
122///
123/// Requires both operands to have compatible SQL types.
124pub fn lt<'a, V, L, R>(left: L, right: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
125where
126    V: SQLParam + 'a,
127    L: Expr<'a, V>,
128    R: Expr<'a, V>,
129    L::SQLType: Compatible<R::SQLType>,
130{
131    SQLExpr::new(binary_op(left, Token::LT, right))
132}
133
134/// Less-than-or-equal comparison (`<=`).
135///
136/// Requires both operands to have compatible SQL types.
137pub fn lte<'a, V, L, R>(left: L, right: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
138where
139    V: SQLParam + 'a,
140    L: Expr<'a, V>,
141    R: Expr<'a, V>,
142    L::SQLType: Compatible<R::SQLType>,
143{
144    SQLExpr::new(binary_op(left, Token::LE, right))
145}
146
147// =============================================================================
148// Pattern Matching
149// =============================================================================
150
151/// LIKE pattern matching.
152///
153/// Requires both operands to be textual types (TEXT, VARCHAR).
154///
155/// # Type Safety
156///
157/// ```ignore
158/// // ✅ OK: Text column with text pattern
159/// like(users.name, "%Alice%");
160///
161/// // ❌ Compile error: Int is not Textual
162/// like(users.id, "%123%");
163/// ```
164pub fn like<'a, V, L, R>(left: L, pattern: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
165where
166    V: SQLParam + 'a,
167    L: Expr<'a, V>,
168    R: Expr<'a, V>,
169    L::SQLType: Textual,
170    R::SQLType: Textual,
171{
172    SQLExpr::new(left.to_sql().push(Token::LIKE).append(pattern.to_sql()))
173}
174
175/// NOT LIKE pattern matching.
176///
177/// Requires both operands to be textual types (TEXT, VARCHAR).
178pub fn not_like<'a, V, L, R>(left: L, pattern: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
179where
180    V: SQLParam + 'a,
181    L: Expr<'a, V>,
182    R: Expr<'a, V>,
183    L::SQLType: Textual,
184    R::SQLType: Textual,
185{
186    SQLExpr::new(
187        left.to_sql()
188            .push(Token::NOT)
189            .push(Token::LIKE)
190            .append(pattern.to_sql()),
191    )
192}
193
194// =============================================================================
195// Range Comparisons
196// =============================================================================
197
198/// BETWEEN comparison.
199///
200/// Checks if expr is between low and high (inclusive).
201/// Requires expr type to be compatible with both bounds.
202pub fn between<'a, V, E, L, H>(expr: E, low: L, high: H) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
203where
204    V: SQLParam + 'a,
205    E: Expr<'a, V>,
206    L: Expr<'a, V>,
207    H: Expr<'a, V>,
208    E::SQLType: Compatible<L::SQLType> + Compatible<H::SQLType>,
209{
210    SQLExpr::new(
211        SQL::from(Token::LPAREN)
212            .append(expr.to_sql())
213            .push(Token::BETWEEN)
214            .append(low.to_sql())
215            .push(Token::AND)
216            .append(high.to_sql())
217            .push(Token::RPAREN),
218    )
219}
220
221/// NOT BETWEEN comparison.
222///
223/// Requires expr type to be compatible with both bounds.
224pub fn not_between<'a, V, E, L, H>(
225    expr: E,
226    low: L,
227    high: H,
228) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
229where
230    V: SQLParam + 'a,
231    E: Expr<'a, V>,
232    L: Expr<'a, V>,
233    H: Expr<'a, V>,
234    E::SQLType: Compatible<L::SQLType> + Compatible<H::SQLType>,
235{
236    SQLExpr::new(
237        SQL::from(Token::LPAREN)
238            .append(expr.to_sql())
239            .push(Token::NOT)
240            .push(Token::BETWEEN)
241            .append(low.to_sql())
242            .push(Token::AND)
243            .append(high.to_sql())
244            .push(Token::RPAREN),
245    )
246}
247
248// =============================================================================
249// NULL Checks
250// =============================================================================
251
252/// IS NULL check.
253///
254/// Returns a boolean expression checking if the value is NULL.
255/// Any expression type can be null-checked.
256pub fn is_null<'a, V, E>(expr: E) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
257where
258    V: SQLParam + 'a,
259    E: Expr<'a, V>,
260{
261    SQLExpr::new(expr.to_sql().push(Token::IS).push(Token::NULL))
262}
263
264/// IS NOT NULL check.
265///
266/// Returns a boolean expression checking if the value is not NULL.
267/// Any expression type can be null-checked.
268pub fn is_not_null<'a, V, E>(expr: E) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
269where
270    V: SQLParam + 'a,
271    E: Expr<'a, V>,
272{
273    SQLExpr::new(
274        expr.to_sql()
275            .push(Token::IS)
276            .push(Token::NOT)
277            .push(Token::NULL),
278    )
279}
280
281// =============================================================================
282// Method-based Comparison API (Extension Trait)
283// =============================================================================
284
285/// Extension trait providing method-based comparisons for any `Expr` type.
286///
287/// This trait is blanket-implemented for all types implementing `Expr`,
288/// allowing method syntax on columns, literals, and expressions:
289///
290/// ```ignore
291/// // Works on columns directly
292/// users.id.eq(42)
293/// users.age.gt(18)
294///
295/// // Chain with operators
296/// users.id.eq(42) & users.age.gt(18)
297/// ```
298pub trait ExprExt<'a, V: SQLParam>: Expr<'a, V> + Sized {
299    /// Equality comparison (`=`).
300    ///
301    /// ```ignore
302    /// users.id.eq(42)  // "users"."id" = 42
303    /// ```
304    fn eq<R>(self, other: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
305    where
306        R: Expr<'a, V>,
307        Self::SQLType: Compatible<R::SQLType>,
308    {
309        eq(self, other)
310    }
311
312    /// Inequality comparison (`<>`).
313    ///
314    /// ```ignore
315    /// users.id.ne(42)  // "users"."id" <> 42
316    /// ```
317    fn ne<R>(self, other: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
318    where
319        R: Expr<'a, V>,
320        Self::SQLType: Compatible<R::SQLType>,
321    {
322        neq(self, other)
323    }
324
325    /// Greater-than comparison (`>`).
326    ///
327    /// ```ignore
328    /// users.age.gt(18)  // "users"."age" > 18
329    /// ```
330    fn gt<R>(self, other: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
331    where
332        R: Expr<'a, V>,
333        Self::SQLType: Compatible<R::SQLType>,
334    {
335        gt(self, other)
336    }
337
338    /// Greater-than-or-equal comparison (`>=`).
339    ///
340    /// ```ignore
341    /// users.age.ge(18)  // "users"."age" >= 18
342    /// ```
343    fn ge<R>(self, other: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
344    where
345        R: Expr<'a, V>,
346        Self::SQLType: Compatible<R::SQLType>,
347    {
348        gte(self, other)
349    }
350
351    /// Less-than comparison (`<`).
352    ///
353    /// ```ignore
354    /// users.age.lt(65)  // "users"."age" < 65
355    /// ```
356    fn lt<R>(self, other: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
357    where
358        R: Expr<'a, V>,
359        Self::SQLType: Compatible<R::SQLType>,
360    {
361        lt(self, other)
362    }
363
364    /// Less-than-or-equal comparison (`<=`).
365    ///
366    /// ```ignore
367    /// users.age.le(65)  // "users"."age" <= 65
368    /// ```
369    fn le<R>(self, other: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
370    where
371        R: Expr<'a, V>,
372        Self::SQLType: Compatible<R::SQLType>,
373    {
374        lte(self, other)
375    }
376
377    /// LIKE pattern matching.
378    ///
379    /// ```ignore
380    /// users.name.like("%Alice%")  // "users"."name" LIKE '%Alice%'
381    /// ```
382    fn like<R>(self, pattern: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
383    where
384        R: Expr<'a, V>,
385        Self::SQLType: Textual,
386        R::SQLType: Textual,
387    {
388        like(self, pattern)
389    }
390
391    /// NOT LIKE pattern matching.
392    ///
393    /// ```ignore
394    /// users.name.not_like("%Bot%")  // "users"."name" NOT LIKE '%Bot%'
395    /// ```
396    fn not_like<R>(self, pattern: R) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
397    where
398        R: Expr<'a, V>,
399        Self::SQLType: Textual,
400        R::SQLType: Textual,
401    {
402        not_like(self, pattern)
403    }
404
405    /// IS NULL check.
406    ///
407    /// ```ignore
408    /// users.deleted_at.is_null()  // "users"."deleted_at" IS NULL
409    /// ```
410    #[allow(clippy::wrong_self_convention)]
411    fn is_null(self) -> SQLExpr<'a, V, Bool, NonNull, Scalar> {
412        is_null(self)
413    }
414
415    /// IS NOT NULL check.
416    ///
417    /// ```ignore
418    /// users.email.is_not_null()  // "users"."email" IS NOT NULL
419    /// ```
420    #[allow(clippy::wrong_self_convention)]
421    fn is_not_null(self) -> SQLExpr<'a, V, Bool, NonNull, Scalar> {
422        is_not_null(self)
423    }
424
425    /// BETWEEN comparison.
426    ///
427    /// Checks if the value is between low and high (inclusive).
428    ///
429    /// ```ignore
430    /// users.age.between(18, 65)  // ("users"."age" BETWEEN 18 AND 65)
431    /// ```
432    fn between<L, H>(self, low: L, high: H) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
433    where
434        L: Expr<'a, V>,
435        H: Expr<'a, V>,
436        Self::SQLType: Compatible<L::SQLType> + Compatible<H::SQLType>,
437    {
438        between(self, low, high)
439    }
440
441    /// NOT BETWEEN comparison.
442    ///
443    /// Checks if the value is NOT between low and high.
444    ///
445    /// ```ignore
446    /// users.age.not_between(0, 17)  // ("users"."age" NOT BETWEEN 0 AND 17)
447    /// ```
448    fn not_between<L, H>(self, low: L, high: H) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
449    where
450        L: Expr<'a, V>,
451        H: Expr<'a, V>,
452        Self::SQLType: Compatible<L::SQLType> + Compatible<H::SQLType>,
453    {
454        not_between(self, low, high)
455    }
456
457    /// IN array check.
458    ///
459    /// Checks if the value is in the provided array.
460    ///
461    /// ```ignore
462    /// users.role.in_array([Role::Admin, Role::Moderator])
463    /// // "users"."role" IN ('admin', 'moderator')
464    /// ```
465    fn in_array<I, R>(self, values: I) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
466    where
467        I: IntoIterator<Item = R>,
468        R: Expr<'a, V>,
469        Self::SQLType: Compatible<R::SQLType>,
470    {
471        crate::expr::in_array(self, values)
472    }
473
474    /// NOT IN array check.
475    ///
476    /// Checks if the value is NOT in the provided array.
477    ///
478    /// ```ignore
479    /// users.role.not_in_array([Role::Banned, Role::Suspended])
480    /// // "users"."role" NOT IN ('banned', 'suspended')
481    /// ```
482    fn not_in_array<I, R>(self, values: I) -> SQLExpr<'a, V, Bool, NonNull, Scalar>
483    where
484        I: IntoIterator<Item = R>,
485        R: Expr<'a, V>,
486        Self::SQLType: Compatible<R::SQLType>,
487    {
488        crate::expr::not_in_array(self, values)
489    }
490}
491
492/// Blanket implementation for all `Expr` types.
493impl<'a, V: SQLParam, E: Expr<'a, V>> ExprExt<'a, V> for E {}