drizzle_core/expr/string.rs
1//! Type-safe string functions.
2//!
3//! These functions require `Textual` types (Text, VarChar) and provide
4//! compile-time enforcement of string operations.
5//!
6//! # Type Safety
7//!
8//! - `upper`, `lower`, `trim`: Require `Textual` types
9//! - `length`: Returns BigInt from `Textual` input
10//! - `substr`, `replace`, `instr`: Require `Textual` types
11
12use crate::sql::{SQL, Token};
13use crate::traits::SQLParam;
14use crate::types::{BigInt, Text, Textual};
15
16use super::{Expr, NullOr, Nullability, SQLExpr, Scalar};
17
18// =============================================================================
19// CASE CONVERSION
20// =============================================================================
21
22/// UPPER - converts string to uppercase.
23///
24/// Preserves the nullability of the input expression.
25///
26/// # Type Safety
27///
28/// ```ignore
29/// // ✅ OK: Text column
30/// upper(users.name);
31///
32/// // ❌ Compile error: Int is not Textual
33/// upper(users.id);
34/// ```
35pub fn upper<'a, V, E>(expr: E) -> SQLExpr<'a, V, Text, E::Nullable, Scalar>
36where
37 V: SQLParam + 'a,
38 E: Expr<'a, V>,
39 E::SQLType: Textual,
40{
41 SQLExpr::new(SQL::func("UPPER", expr.to_sql()))
42}
43
44/// LOWER - converts string to lowercase.
45///
46/// Preserves the nullability of the input expression.
47///
48/// # Example
49///
50/// ```ignore
51/// use drizzle_core::expr::lower;
52///
53/// // SELECT LOWER(users.email)
54/// let email_lower = lower(users.email);
55/// ```
56pub fn lower<'a, V, E>(expr: E) -> SQLExpr<'a, V, Text, E::Nullable, Scalar>
57where
58 V: SQLParam + 'a,
59 E: Expr<'a, V>,
60 E::SQLType: Textual,
61{
62 SQLExpr::new(SQL::func("LOWER", expr.to_sql()))
63}
64
65// =============================================================================
66// TRIM FUNCTIONS
67// =============================================================================
68
69/// TRIM - removes leading and trailing whitespace.
70///
71/// Preserves the nullability of the input expression.
72///
73/// # Example
74///
75/// ```ignore
76/// use drizzle_core::expr::trim;
77///
78/// // SELECT TRIM(users.name)
79/// let trimmed = trim(users.name);
80/// ```
81pub fn trim<'a, V, E>(expr: E) -> SQLExpr<'a, V, Text, E::Nullable, Scalar>
82where
83 V: SQLParam + 'a,
84 E: Expr<'a, V>,
85 E::SQLType: Textual,
86{
87 SQLExpr::new(SQL::func("TRIM", expr.to_sql()))
88}
89
90/// LTRIM - removes leading whitespace.
91///
92/// Preserves the nullability of the input expression.
93pub fn ltrim<'a, V, E>(expr: E) -> SQLExpr<'a, V, Text, E::Nullable, Scalar>
94where
95 V: SQLParam + 'a,
96 E: Expr<'a, V>,
97 E::SQLType: Textual,
98{
99 SQLExpr::new(SQL::func("LTRIM", expr.to_sql()))
100}
101
102/// RTRIM - removes trailing whitespace.
103///
104/// Preserves the nullability of the input expression.
105pub fn rtrim<'a, V, E>(expr: E) -> SQLExpr<'a, V, Text, E::Nullable, Scalar>
106where
107 V: SQLParam + 'a,
108 E: Expr<'a, V>,
109 E::SQLType: Textual,
110{
111 SQLExpr::new(SQL::func("RTRIM", expr.to_sql()))
112}
113
114// =============================================================================
115// LENGTH
116// =============================================================================
117
118/// LENGTH - returns the length of a string.
119///
120/// Returns BigInt type, preserves nullability.
121///
122/// # Example
123///
124/// ```ignore
125/// use drizzle_core::expr::length;
126///
127/// // SELECT LENGTH(users.name)
128/// let name_len = length(users.name);
129/// ```
130pub fn length<'a, V, E>(expr: E) -> SQLExpr<'a, V, BigInt, E::Nullable, Scalar>
131where
132 V: SQLParam + 'a,
133 E: Expr<'a, V>,
134 E::SQLType: Textual,
135{
136 SQLExpr::new(SQL::func("LENGTH", expr.to_sql()))
137}
138
139// =============================================================================
140// SUBSTRING
141// =============================================================================
142
143/// SUBSTR - extracts a substring from a string.
144///
145/// Extracts `len` characters starting at position `start` (1-indexed).
146/// Preserves the nullability of the input expression.
147///
148/// # Example
149///
150/// ```ignore
151/// use drizzle_core::expr::substr;
152///
153/// // SELECT SUBSTR(users.name, 1, 3) -- first 3 characters
154/// let prefix = substr(users.name, 1, 3);
155/// ```
156pub fn substr<'a, V, E, S, L>(
157 expr: E,
158 start: S,
159 len: L,
160) -> SQLExpr<'a, V, Text, E::Nullable, Scalar>
161where
162 V: SQLParam + 'a,
163 E: Expr<'a, V>,
164 E::SQLType: Textual,
165 S: Expr<'a, V>,
166 L: Expr<'a, V>,
167{
168 SQLExpr::new(SQL::func(
169 "SUBSTR",
170 expr.to_sql()
171 .push(Token::COMMA)
172 .append(start.to_sql())
173 .push(Token::COMMA)
174 .append(len.to_sql()),
175 ))
176}
177
178// =============================================================================
179// REPLACE
180// =============================================================================
181
182/// REPLACE - replaces occurrences of a substring.
183///
184/// Replaces all occurrences of `from` with `to` in the expression.
185/// Preserves the nullability of the input expression.
186///
187/// # Example
188///
189/// ```ignore
190/// use drizzle_core::expr::replace;
191///
192/// // SELECT REPLACE(users.email, '@old.com', '@new.com')
193/// let new_email = replace(users.email, "@old.com", "@new.com");
194/// ```
195pub fn replace<'a, V, E, F, T>(expr: E, from: F, to: T) -> SQLExpr<'a, V, Text, E::Nullable, Scalar>
196where
197 V: SQLParam + 'a,
198 E: Expr<'a, V>,
199 E::SQLType: Textual,
200 F: Expr<'a, V>,
201 F::SQLType: Textual,
202 T: Expr<'a, V>,
203 T::SQLType: Textual,
204{
205 SQLExpr::new(SQL::func(
206 "REPLACE",
207 expr.to_sql()
208 .push(Token::COMMA)
209 .append(from.to_sql())
210 .push(Token::COMMA)
211 .append(to.to_sql()),
212 ))
213}
214
215// =============================================================================
216// INSTR
217// =============================================================================
218
219/// INSTR - finds the position of a substring.
220///
221/// Returns the 1-indexed position of the first occurrence of `search`
222/// in the expression, or 0 if not found. Returns BigInt.
223/// Preserves the nullability of the input expression.
224///
225/// # Example
226///
227/// ```ignore
228/// use drizzle_core::expr::instr;
229///
230/// // SELECT INSTR(users.email, '@')
231/// let at_pos = instr(users.email, "@");
232/// ```
233pub fn instr<'a, V, E, S>(expr: E, search: S) -> SQLExpr<'a, V, BigInt, E::Nullable, Scalar>
234where
235 V: SQLParam + 'a,
236 E: Expr<'a, V>,
237 E::SQLType: Textual,
238 S: Expr<'a, V>,
239 S::SQLType: Textual,
240{
241 SQLExpr::new(SQL::func(
242 "INSTR",
243 expr.to_sql().push(Token::COMMA).append(search.to_sql()),
244 ))
245}
246
247// =============================================================================
248// CONCAT (with NULL propagation)
249// =============================================================================
250
251/// Concatenate two string expressions using || operator.
252///
253/// Nullability follows SQL concatenation rules: if either input is nullable,
254/// the result is nullable. `string_concat` is a compatibility alias.
255///
256/// # Type Safety
257///
258/// ```ignore
259/// // ✅ OK: Both are Text
260/// concat(users.first_name, users.last_name);
261///
262/// // ✅ OK: Text with string literal
263/// concat(users.first_name, " ");
264///
265/// // ❌ Compile error: Int is not Textual
266/// concat(users.id, users.name);
267/// ```
268///
269/// # Example
270///
271/// ```ignore
272/// use drizzle_core::expr::concat;
273///
274/// // SELECT users.first_name || ' ' || users.last_name
275/// let full_name = concat(concat(users.first_name, " "), users.last_name);
276/// ```
277pub fn concat<'a, V, E1, E2>(
278 expr1: E1,
279 expr2: E2,
280) -> SQLExpr<'a, V, Text, <E1::Nullable as NullOr<E2::Nullable>>::Output, Scalar>
281where
282 V: SQLParam + 'a,
283 E1: Expr<'a, V>,
284 E1::SQLType: Textual,
285 E2: Expr<'a, V>,
286 E2::SQLType: Textual,
287 E1::Nullable: NullOr<E2::Nullable>,
288 E2::Nullable: Nullability,
289{
290 SQLExpr::new(expr1.to_sql().push(Token::CONCAT).append(expr2.to_sql()))
291}