Skip to main content

reinhardt_query/expr/
expr_trait.rs

1//! ExprTrait - Expression operations trait.
2//!
3//! This module provides [`ExprTrait`], which defines all the operations
4//! that can be performed on expressions (comparisons, logical ops, etc.).
5
6use super::simple_expr::{Keyword, SimpleExpr};
7use crate::types::{BinOper, UnOper};
8use crate::value::Value;
9
10/// Escape SQL LIKE wildcard characters in user input.
11///
12/// Escapes `\` -> `\\`, `%` -> `\%`, and `_` -> `\_` so that user-supplied
13/// strings are treated as literal text in LIKE patterns.
14///
15/// This escaping relies on the `ESCAPE '\'` clause being present in the
16/// generated SQL. The helper functions [`ExprTrait::starts_with`],
17/// [`ExprTrait::ends_with`], and [`ExprTrait::contains`] automatically
18/// include this clause.
19fn escape_like_pattern(input: &str) -> String {
20	let mut escaped = String::with_capacity(input.len());
21	for ch in input.chars() {
22		match ch {
23			'\\' => escaped.push_str("\\\\"),
24			'%' => escaped.push_str("\\%"),
25			'_' => escaped.push_str("\\_"),
26			_ => escaped.push(ch),
27		}
28	}
29	escaped
30}
31
32/// Build a LIKE expression with an explicit `ESCAPE '\'` clause.
33///
34/// Produces: `<expr> LIKE <pattern> ESCAPE '\'`
35///
36/// The explicit ESCAPE clause ensures that backslash escaping works
37/// consistently across all SQL backends, including those that do not treat
38/// `\` as a LIKE escape character by default (e.g., SQLite).
39fn like_with_escape(expr: SimpleExpr, pattern: String) -> SimpleExpr {
40	SimpleExpr::CustomWithExpr(
41		"? LIKE ? ESCAPE '\\'".to_string(),
42		vec![
43			expr,
44			SimpleExpr::Value(Value::String(Some(Box::new(pattern)))),
45		],
46	)
47}
48
49/// Trait for expression operations.
50///
51/// This trait provides methods for building complex expressions through
52/// operator chaining. It is implemented for [`Expr`](crate::expr::Expr) and [`SimpleExpr`].
53///
54/// # Example
55///
56/// ```rust,ignore
57/// use reinhardt_query::{Expr, ExprTrait};
58///
59/// // Comparison
60/// let expr = Expr::col("age").gte(18);
61///
62/// // Logical operations
63/// let expr = Expr::col("active").eq(true).and(Expr::col("verified").eq(true));
64///
65/// // Arithmetic
66/// let expr = Expr::col("price").mul(Expr::col("quantity"));
67/// ```
68// Expression trait methods consume self for builder-pattern chaining,
69// so is_*/as_* methods intentionally take self by value.
70#[allow(clippy::wrong_self_convention)]
71pub trait ExprTrait: Sized {
72	/// Build the final SimpleExpr.
73	fn into_simple_expr(self) -> SimpleExpr;
74
75	// =========================================================================
76	// Comparison operations
77	// =========================================================================
78
79	/// Equal (`=`).
80	///
81	/// # Example
82	///
83	/// ```rust,ignore
84	/// Expr::col("name").eq("Alice")
85	/// // Generates: "name" = 'Alice'
86	/// ```
87	fn eq<V>(self, v: V) -> SimpleExpr
88	where
89		V: Into<SimpleExpr>,
90	{
91		SimpleExpr::Binary(
92			Box::new(self.into_simple_expr()),
93			BinOper::Equal,
94			Box::new(v.into()),
95		)
96	}
97
98	/// Not equal (`<>`).
99	fn ne<V>(self, v: V) -> SimpleExpr
100	where
101		V: Into<SimpleExpr>,
102	{
103		SimpleExpr::Binary(
104			Box::new(self.into_simple_expr()),
105			BinOper::NotEqual,
106			Box::new(v.into()),
107		)
108	}
109
110	/// Less than (`<`).
111	fn lt<V>(self, v: V) -> SimpleExpr
112	where
113		V: Into<SimpleExpr>,
114	{
115		SimpleExpr::Binary(
116			Box::new(self.into_simple_expr()),
117			BinOper::SmallerThan,
118			Box::new(v.into()),
119		)
120	}
121
122	/// Less than or equal (`<=`).
123	fn lte<V>(self, v: V) -> SimpleExpr
124	where
125		V: Into<SimpleExpr>,
126	{
127		SimpleExpr::Binary(
128			Box::new(self.into_simple_expr()),
129			BinOper::SmallerThanOrEqual,
130			Box::new(v.into()),
131		)
132	}
133
134	/// Greater than (`>`).
135	fn gt<V>(self, v: V) -> SimpleExpr
136	where
137		V: Into<SimpleExpr>,
138	{
139		SimpleExpr::Binary(
140			Box::new(self.into_simple_expr()),
141			BinOper::GreaterThan,
142			Box::new(v.into()),
143		)
144	}
145
146	/// Greater than or equal (`>=`).
147	fn gte<V>(self, v: V) -> SimpleExpr
148	where
149		V: Into<SimpleExpr>,
150	{
151		SimpleExpr::Binary(
152			Box::new(self.into_simple_expr()),
153			BinOper::GreaterThanOrEqual,
154			Box::new(v.into()),
155		)
156	}
157
158	// =========================================================================
159	// NULL checks
160	// =========================================================================
161
162	/// IS NULL.
163	fn is_null(self) -> SimpleExpr {
164		SimpleExpr::Binary(
165			Box::new(self.into_simple_expr()),
166			BinOper::Is,
167			Box::new(SimpleExpr::Constant(Keyword::Null)),
168		)
169	}
170
171	/// IS NOT NULL.
172	fn is_not_null(self) -> SimpleExpr {
173		SimpleExpr::Binary(
174			Box::new(self.into_simple_expr()),
175			BinOper::IsNot,
176			Box::new(SimpleExpr::Constant(Keyword::Null)),
177		)
178	}
179
180	// =========================================================================
181	// Range operations
182	// =========================================================================
183
184	/// BETWEEN.
185	///
186	/// # Example
187	///
188	/// ```rust,ignore
189	/// Expr::col("age").between(18, 65)
190	/// // Generates: "age" BETWEEN 18 AND 65
191	/// ```
192	fn between<A, B>(self, a: A, b: B) -> SimpleExpr
193	where
194		A: Into<SimpleExpr>,
195		B: Into<SimpleExpr>,
196	{
197		SimpleExpr::Binary(
198			Box::new(self.into_simple_expr()),
199			BinOper::Between,
200			Box::new(SimpleExpr::Tuple(vec![a.into(), b.into()])),
201		)
202	}
203
204	/// NOT BETWEEN.
205	fn not_between<A, B>(self, a: A, b: B) -> SimpleExpr
206	where
207		A: Into<SimpleExpr>,
208		B: Into<SimpleExpr>,
209	{
210		SimpleExpr::Binary(
211			Box::new(self.into_simple_expr()),
212			BinOper::NotBetween,
213			Box::new(SimpleExpr::Tuple(vec![a.into(), b.into()])),
214		)
215	}
216
217	// =========================================================================
218	// Set membership
219	// =========================================================================
220
221	/// IN.
222	///
223	/// Returns `FALSE` when the iterator is empty, since `x IN ()` is invalid SQL.
224	///
225	/// # Example
226	///
227	/// ```rust,ignore
228	/// Expr::col("status").is_in(["active", "pending"])
229	/// // Generates: "status" IN ('active', 'pending')
230	/// ```
231	fn is_in<I, V>(self, values: I) -> SimpleExpr
232	where
233		I: IntoIterator<Item = V>,
234		V: Into<SimpleExpr>,
235	{
236		let collected: Vec<SimpleExpr> = values.into_iter().map(|v| v.into()).collect();
237		if collected.is_empty() {
238			// Empty IN () is invalid SQL in all databases; use FALSE instead
239			return SimpleExpr::Constant(Keyword::False);
240		}
241		SimpleExpr::Binary(
242			Box::new(self.into_simple_expr()),
243			BinOper::In,
244			Box::new(SimpleExpr::Tuple(collected)),
245		)
246	}
247
248	/// NOT IN.
249	///
250	/// Returns `TRUE` when the iterator is empty, since `x NOT IN ()` is invalid SQL
251	/// and logically equivalent to `TRUE`.
252	fn is_not_in<I, V>(self, values: I) -> SimpleExpr
253	where
254		I: IntoIterator<Item = V>,
255		V: Into<SimpleExpr>,
256	{
257		let collected: Vec<SimpleExpr> = values.into_iter().map(|v| v.into()).collect();
258		if collected.is_empty() {
259			// Empty NOT IN () is invalid SQL in all databases; use TRUE instead
260			return SimpleExpr::Constant(Keyword::True);
261		}
262		SimpleExpr::Binary(
263			Box::new(self.into_simple_expr()),
264			BinOper::NotIn,
265			Box::new(SimpleExpr::Tuple(collected)),
266		)
267	}
268
269	// =========================================================================
270	// Pattern matching
271	// =========================================================================
272
273	/// LIKE.
274	///
275	/// # Example
276	///
277	/// ```rust,ignore
278	/// Expr::col("name").like("%john%")
279	/// // Generates: "name" LIKE '%john%'
280	/// ```
281	fn like<V>(self, pattern: V) -> SimpleExpr
282	where
283		V: Into<SimpleExpr>,
284	{
285		SimpleExpr::Binary(
286			Box::new(self.into_simple_expr()),
287			BinOper::Like,
288			Box::new(pattern.into()),
289		)
290	}
291
292	/// NOT LIKE.
293	fn not_like<V>(self, pattern: V) -> SimpleExpr
294	where
295		V: Into<SimpleExpr>,
296	{
297		SimpleExpr::Binary(
298			Box::new(self.into_simple_expr()),
299			BinOper::NotLike,
300			Box::new(pattern.into()),
301		)
302	}
303
304	/// ILIKE (case-insensitive LIKE, PostgreSQL).
305	fn ilike<V>(self, pattern: V) -> SimpleExpr
306	where
307		V: Into<SimpleExpr>,
308	{
309		SimpleExpr::Binary(
310			Box::new(self.into_simple_expr()),
311			BinOper::ILike,
312			Box::new(pattern.into()),
313		)
314	}
315
316	/// NOT ILIKE (PostgreSQL).
317	fn not_ilike<V>(self, pattern: V) -> SimpleExpr
318	where
319		V: Into<SimpleExpr>,
320	{
321		SimpleExpr::Binary(
322			Box::new(self.into_simple_expr()),
323			BinOper::NotILike,
324			Box::new(pattern.into()),
325		)
326	}
327
328	/// Helper for LIKE with prefix wildcard.
329	///
330	/// SQL wildcard characters (`%`, `_`) and the escape character (`\`) in
331	/// user input are escaped before constructing the pattern. The generated
332	/// SQL includes an explicit `ESCAPE '\'` clause so that the backslash
333	/// escaping is portable across all backends (including SQLite, which does
334	/// not treat `\` as an escape character by default).
335	fn starts_with<S>(self, prefix: S) -> SimpleExpr
336	where
337		S: Into<String>,
338	{
339		let escaped = escape_like_pattern(&prefix.into());
340		let pattern = format!("{}%", escaped);
341		like_with_escape(self.into_simple_expr(), pattern)
342	}
343
344	/// Helper for LIKE with suffix wildcard.
345	///
346	/// SQL wildcard characters (`%`, `_`) and the escape character (`\`) in
347	/// user input are escaped before constructing the pattern. The generated
348	/// SQL includes an explicit `ESCAPE '\'` clause for cross-backend
349	/// portability.
350	fn ends_with<S>(self, suffix: S) -> SimpleExpr
351	where
352		S: Into<String>,
353	{
354		let escaped = escape_like_pattern(&suffix.into());
355		let pattern = format!("%{}", escaped);
356		like_with_escape(self.into_simple_expr(), pattern)
357	}
358
359	/// Helper for LIKE with both wildcards.
360	///
361	/// SQL wildcard characters (`%`, `_`) and the escape character (`\`) in
362	/// user input are escaped before constructing the pattern. The generated
363	/// SQL includes an explicit `ESCAPE '\'` clause for cross-backend
364	/// portability.
365	fn contains<S>(self, substring: S) -> SimpleExpr
366	where
367		S: Into<String>,
368	{
369		let escaped = escape_like_pattern(&substring.into());
370		let pattern = format!("%{}%", escaped);
371		like_with_escape(self.into_simple_expr(), pattern)
372	}
373
374	// =========================================================================
375	// Logical operations
376	// =========================================================================
377
378	/// AND.
379	fn and<E>(self, other: E) -> SimpleExpr
380	where
381		E: Into<SimpleExpr>,
382	{
383		SimpleExpr::Binary(
384			Box::new(self.into_simple_expr()),
385			BinOper::And,
386			Box::new(other.into()),
387		)
388	}
389
390	/// OR.
391	fn or<E>(self, other: E) -> SimpleExpr
392	where
393		E: Into<SimpleExpr>,
394	{
395		SimpleExpr::Binary(
396			Box::new(self.into_simple_expr()),
397			BinOper::Or,
398			Box::new(other.into()),
399		)
400	}
401
402	/// NOT (unary).
403	fn not(self) -> SimpleExpr {
404		SimpleExpr::Unary(UnOper::Not, Box::new(self.into_simple_expr()))
405	}
406
407	// =========================================================================
408	// Arithmetic operations
409	// =========================================================================
410
411	/// Addition (`+`).
412	fn add<V>(self, v: V) -> SimpleExpr
413	where
414		V: Into<SimpleExpr>,
415	{
416		SimpleExpr::Binary(
417			Box::new(self.into_simple_expr()),
418			BinOper::Add,
419			Box::new(v.into()),
420		)
421	}
422
423	/// Subtraction (`-`).
424	fn sub<V>(self, v: V) -> SimpleExpr
425	where
426		V: Into<SimpleExpr>,
427	{
428		SimpleExpr::Binary(
429			Box::new(self.into_simple_expr()),
430			BinOper::Sub,
431			Box::new(v.into()),
432		)
433	}
434
435	/// Multiplication (`*`).
436	fn mul<V>(self, v: V) -> SimpleExpr
437	where
438		V: Into<SimpleExpr>,
439	{
440		SimpleExpr::Binary(
441			Box::new(self.into_simple_expr()),
442			BinOper::Mul,
443			Box::new(v.into()),
444		)
445	}
446
447	/// Division (`/`).
448	fn div<V>(self, v: V) -> SimpleExpr
449	where
450		V: Into<SimpleExpr>,
451	{
452		SimpleExpr::Binary(
453			Box::new(self.into_simple_expr()),
454			BinOper::Div,
455			Box::new(v.into()),
456		)
457	}
458
459	/// Modulo (`%`).
460	fn modulo<V>(self, v: V) -> SimpleExpr
461	where
462		V: Into<SimpleExpr>,
463	{
464		SimpleExpr::Binary(
465			Box::new(self.into_simple_expr()),
466			BinOper::Mod,
467			Box::new(v.into()),
468		)
469	}
470
471	// =========================================================================
472	// Bitwise operations
473	// =========================================================================
474
475	/// Bitwise AND (`&`).
476	fn bit_and<V>(self, v: V) -> SimpleExpr
477	where
478		V: Into<SimpleExpr>,
479	{
480		SimpleExpr::Binary(
481			Box::new(self.into_simple_expr()),
482			BinOper::BitAnd,
483			Box::new(v.into()),
484		)
485	}
486
487	/// Bitwise OR (`|`).
488	fn bit_or<V>(self, v: V) -> SimpleExpr
489	where
490		V: Into<SimpleExpr>,
491	{
492		SimpleExpr::Binary(
493			Box::new(self.into_simple_expr()),
494			BinOper::BitOr,
495			Box::new(v.into()),
496		)
497	}
498
499	/// Left shift (`<<`).
500	fn left_shift<V>(self, v: V) -> SimpleExpr
501	where
502		V: Into<SimpleExpr>,
503	{
504		SimpleExpr::Binary(
505			Box::new(self.into_simple_expr()),
506			BinOper::LShift,
507			Box::new(v.into()),
508		)
509	}
510
511	/// Right shift (`>>`).
512	fn right_shift<V>(self, v: V) -> SimpleExpr
513	where
514		V: Into<SimpleExpr>,
515	{
516		SimpleExpr::Binary(
517			Box::new(self.into_simple_expr()),
518			BinOper::RShift,
519			Box::new(v.into()),
520		)
521	}
522
523	// =========================================================================
524	// Type casting
525	// =========================================================================
526
527	/// CAST expression.
528	///
529	/// # Example
530	///
531	/// ```rust,ignore
532	/// Expr::col("age").cast_as("TEXT")
533	/// // Generates: CAST("age" AS TEXT)
534	/// ```
535	fn cast_as<T>(self, type_name: T) -> SimpleExpr
536	where
537		T: crate::types::IntoIden,
538	{
539		SimpleExpr::Cast(Box::new(self.into_simple_expr()), type_name.into_iden())
540	}
541
542	/// AS ENUM expression (PostgreSQL).
543	fn as_enum<T>(self, type_name: T) -> SimpleExpr
544	where
545		T: crate::types::IntoIden,
546	{
547		SimpleExpr::AsEnum(type_name.into_iden(), Box::new(self.into_simple_expr()))
548	}
549}
550
551// Implement ExprTrait for SimpleExpr
552impl ExprTrait for SimpleExpr {
553	fn into_simple_expr(self) -> SimpleExpr {
554		self
555	}
556}
557
558// Implement ExprTrait for Expr
559impl ExprTrait for super::expr::Expr {
560	fn into_simple_expr(self) -> SimpleExpr {
561		self.into_simple_expr()
562	}
563}
564
565#[cfg(test)]
566mod tests {
567	use super::*;
568	use crate::expr::Expr;
569	use rstest::rstest;
570
571	#[rstest]
572	fn test_eq() {
573		let expr = Expr::col("name").eq("Alice");
574		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::Equal, _)));
575	}
576
577	#[rstest]
578	fn test_ne() {
579		let expr = Expr::col("name").ne("Bob");
580		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::NotEqual, _)));
581	}
582
583	#[rstest]
584	fn test_lt() {
585		let expr = Expr::col("age").lt(18);
586		assert!(matches!(
587			expr,
588			SimpleExpr::Binary(_, BinOper::SmallerThan, _)
589		));
590	}
591
592	#[rstest]
593	fn test_lte() {
594		let expr = Expr::col("age").lte(65);
595		assert!(matches!(
596			expr,
597			SimpleExpr::Binary(_, BinOper::SmallerThanOrEqual, _)
598		));
599	}
600
601	#[rstest]
602	fn test_gt() {
603		let expr = Expr::col("age").gt(18);
604		assert!(matches!(
605			expr,
606			SimpleExpr::Binary(_, BinOper::GreaterThan, _)
607		));
608	}
609
610	#[rstest]
611	fn test_gte() {
612		let expr = Expr::col("age").gte(18);
613		assert!(matches!(
614			expr,
615			SimpleExpr::Binary(_, BinOper::GreaterThanOrEqual, _)
616		));
617	}
618
619	#[rstest]
620	fn test_is_null() {
621		let expr = Expr::col("deleted_at").is_null();
622		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::Is, _)));
623	}
624
625	#[rstest]
626	fn test_is_not_null() {
627		let expr = Expr::col("name").is_not_null();
628		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::IsNot, _)));
629	}
630
631	#[rstest]
632	fn test_between() {
633		let expr = Expr::col("age").between(18, 65);
634		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::Between, _)));
635	}
636
637	#[rstest]
638	fn test_not_between() {
639		let expr = Expr::col("age").not_between(0, 17);
640		assert!(matches!(
641			expr,
642			SimpleExpr::Binary(_, BinOper::NotBetween, _)
643		));
644	}
645
646	#[rstest]
647	fn test_is_in() {
648		let expr = Expr::col("status").is_in(["active", "pending"]);
649		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::In, _)));
650	}
651
652	#[rstest]
653	fn test_is_not_in() {
654		let expr = Expr::col("status").is_not_in(["deleted", "banned"]);
655		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::NotIn, _)));
656	}
657
658	#[rstest]
659	fn test_like() {
660		let expr = Expr::col("name").like("%john%");
661		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::Like, _)));
662	}
663
664	#[rstest]
665	fn test_not_like() {
666		let expr = Expr::col("name").not_like("%admin%");
667		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::NotLike, _)));
668	}
669
670	#[rstest]
671	fn test_starts_with() {
672		let expr = Expr::col("name").starts_with("John");
673		// starts_with uses CustomWithExpr to include ESCAPE clause
674		assert!(matches!(expr, SimpleExpr::CustomWithExpr(_, _)));
675		if let SimpleExpr::CustomWithExpr(template, _) = &expr {
676			assert_eq!(template, "? LIKE ? ESCAPE '\\'");
677		}
678	}
679
680	#[rstest]
681	fn test_ends_with() {
682		let expr = Expr::col("email").ends_with("@example.com");
683		// ends_with uses CustomWithExpr to include ESCAPE clause
684		assert!(matches!(expr, SimpleExpr::CustomWithExpr(_, _)));
685		if let SimpleExpr::CustomWithExpr(template, _) = &expr {
686			assert_eq!(template, "? LIKE ? ESCAPE '\\'");
687		}
688	}
689
690	#[rstest]
691	fn test_contains() {
692		let expr = Expr::col("description").contains("important");
693		// contains uses CustomWithExpr to include ESCAPE clause
694		assert!(matches!(expr, SimpleExpr::CustomWithExpr(_, _)));
695		if let SimpleExpr::CustomWithExpr(template, _) = &expr {
696			assert_eq!(template, "? LIKE ? ESCAPE '\\'");
697		}
698	}
699
700	#[rstest]
701	fn test_and() {
702		let expr = Expr::col("active")
703			.eq(true)
704			.and(Expr::col("verified").eq(true));
705		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::And, _)));
706	}
707
708	#[rstest]
709	fn test_or() {
710		let expr = Expr::col("role")
711			.eq("admin")
712			.or(Expr::col("role").eq("moderator"));
713		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::Or, _)));
714	}
715
716	#[rstest]
717	fn test_not() {
718		let expr = Expr::col("deleted").not();
719		assert!(matches!(expr, SimpleExpr::Unary(UnOper::Not, _)));
720	}
721
722	#[rstest]
723	fn test_add() {
724		let expr = Expr::col("price").add(10);
725		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::Add, _)));
726	}
727
728	#[rstest]
729	fn test_sub() {
730		let expr = Expr::col("quantity").sub(1);
731		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::Sub, _)));
732	}
733
734	#[rstest]
735	fn test_mul() {
736		let expr = Expr::col("price").mul(Expr::col("quantity"));
737		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::Mul, _)));
738	}
739
740	#[rstest]
741	fn test_div() {
742		let expr = Expr::col("total").div(Expr::col("count"));
743		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::Div, _)));
744	}
745
746	#[rstest]
747	fn test_modulo() {
748		let expr = Expr::col("value").modulo(2);
749		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::Mod, _)));
750	}
751
752	#[rstest]
753	fn test_cast_as() {
754		let expr = Expr::col("age").cast_as("TEXT");
755		assert!(matches!(expr, SimpleExpr::Cast(_, _)));
756	}
757
758	#[rstest]
759	fn test_chained_operations() {
760		// Test complex expression chaining
761		let expr = Expr::col("age")
762			.gte(18)
763			.and(Expr::col("active").eq(true))
764			.and(Expr::col("verified").is_not_null());
765
766		assert!(matches!(expr, SimpleExpr::Binary(_, BinOper::And, _)));
767	}
768}