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 {}