Skip to main content

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`: Dialect-aware integer output from text input
10//! - `substr`, `replace`, `instr`: Require `Textual` types
11
12use crate::dialect::{Dialect, DialectTypes};
13use crate::sql::{SQL, Token};
14use crate::traits::{SQLParam, ToSQL};
15use crate::types::{DataType, Integral, Textual};
16use crate::{MySQLDialect, PostgresDialect, SQLiteDialect};
17use drizzle_types::postgres::types::{
18    Char as PgChar, Int4 as PgInt4, Text as PgText, Varchar as PgVarchar,
19};
20use drizzle_types::sqlite::types::{Integer as SqliteInteger, Text as SqliteText};
21
22use super::{AggOr, AggregateKind, Expr, NonNull, NullOr, Nullability, SQLExpr};
23
24#[diagnostic::on_unimplemented(
25    message = "no length policy for `{Self}` on this dialect",
26    label = "length return type is not defined for this SQL type/dialect"
27)]
28pub trait LengthPolicy<D>: DataType {
29    type Output: DataType;
30}
31
32#[diagnostic::on_unimplemented(
33    message = "this string function is not available for this dialect",
34    label = "use a dialect-specific alternative"
35)]
36pub trait PostgresStringSupport {}
37
38#[diagnostic::on_unimplemented(
39    message = "INSTR is not available for this dialect",
40    label = "use a dialect-specific substring-position function"
41)]
42pub trait InstrPolicy {
43    type Output: DataType;
44}
45
46#[diagnostic::on_unimplemented(
47    message = "LEFT/RIGHT are not available for this dialect",
48    label = "use a dialect-specific substring function"
49)]
50pub trait LeftRightSupport {}
51
52#[diagnostic::on_unimplemented(
53    message = "LPAD/RPAD are not available for this dialect",
54    label = "use a dialect-specific padding expression"
55)]
56pub trait PadSupport {}
57
58#[diagnostic::on_unimplemented(
59    message = "REVERSE is not available for this dialect",
60    label = "use a dialect-specific string expression"
61)]
62pub trait ReverseSupport {}
63
64#[diagnostic::on_unimplemented(
65    message = "REPEAT is not available for this dialect",
66    label = "use a dialect-specific string expression"
67)]
68pub trait RepeatSupport {}
69
70impl LengthPolicy<SQLiteDialect> for SqliteText {
71    type Output = SqliteInteger;
72}
73impl LengthPolicy<SQLiteDialect> for drizzle_types::sqlite::types::Any {
74    type Output = SqliteInteger;
75}
76
77impl LengthPolicy<PostgresDialect> for PgVarchar {
78    type Output = PgInt4;
79}
80impl LengthPolicy<PostgresDialect> for PgText {
81    type Output = PgInt4;
82}
83impl LengthPolicy<PostgresDialect> for PgChar {
84    type Output = PgInt4;
85}
86
87macro_rules! mysql_length_policy {
88    ($($ty:ty),+ $(,)?) => {
89        $(
90            impl LengthPolicy<MySQLDialect> for $ty {
91                type Output = drizzle_types::mysql::types::BigInt;
92            }
93        )+
94    };
95}
96
97mysql_length_policy!(
98    drizzle_types::mysql::types::Char,
99    drizzle_types::mysql::types::Varchar,
100    drizzle_types::mysql::types::TinyText,
101    drizzle_types::mysql::types::Text,
102    drizzle_types::mysql::types::MediumText,
103    drizzle_types::mysql::types::LongText,
104    drizzle_types::mysql::types::Enum,
105    drizzle_types::mysql::types::Set,
106);
107
108impl PostgresStringSupport for PostgresDialect {}
109
110impl InstrPolicy for SQLiteDialect {
111    type Output = SqliteInteger;
112}
113impl InstrPolicy for MySQLDialect {
114    type Output = drizzle_types::mysql::types::BigInt;
115}
116
117impl LeftRightSupport for PostgresDialect {}
118impl LeftRightSupport for MySQLDialect {}
119impl PadSupport for PostgresDialect {}
120impl PadSupport for MySQLDialect {}
121impl ReverseSupport for PostgresDialect {}
122impl ReverseSupport for MySQLDialect {}
123impl RepeatSupport for PostgresDialect {}
124impl RepeatSupport for MySQLDialect {}
125
126// =============================================================================
127// CASE CONVERSION
128// =============================================================================
129
130/// UPPER - converts string to uppercase.
131///
132/// Preserves the nullability of the input expression.
133///
134/// # Type Safety
135///
136/// ```rust
137/// # let _ = r####"
138/// // ✅ OK: Text column
139/// upper(users.name);
140///
141/// // ❌ Compile error: Int is not Textual
142/// upper(users.id);
143/// # "####;
144/// ```
145pub fn upper<'a, V, E>(
146    expr: E,
147) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Text, E::Nullable, E::Aggregate>
148where
149    V: SQLParam + 'a,
150    E: Expr<'a, V>,
151    E::SQLType: Textual,
152{
153    SQLExpr::new(SQL::func("UPPER", expr.into_sql()))
154}
155
156/// LOWER - converts string to lowercase.
157///
158/// Preserves the nullability of the input expression.
159///
160/// # Example
161///
162/// ```rust
163/// # let _ = r####"
164/// use drizzle_core::expr::lower;
165///
166/// // SELECT LOWER(users.email)
167/// let email_lower = lower(users.email);
168/// # "####;
169/// ```
170pub fn lower<'a, V, E>(
171    expr: E,
172) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Text, E::Nullable, E::Aggregate>
173where
174    V: SQLParam + 'a,
175    E: Expr<'a, V>,
176    E::SQLType: Textual,
177{
178    SQLExpr::new(SQL::func("LOWER", expr.into_sql()))
179}
180
181// =============================================================================
182// TRIM FUNCTIONS
183// =============================================================================
184
185/// TRIM - removes leading and trailing whitespace.
186///
187/// Preserves the nullability of the input expression.
188///
189/// # Example
190///
191/// ```rust
192/// # let _ = r####"
193/// use drizzle_core::expr::trim;
194///
195/// // SELECT TRIM(users.name)
196/// let trimmed = trim(users.name);
197/// # "####;
198/// ```
199pub fn trim<'a, V, E>(
200    expr: E,
201) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Text, E::Nullable, E::Aggregate>
202where
203    V: SQLParam + 'a,
204    E: Expr<'a, V>,
205    E::SQLType: Textual,
206{
207    SQLExpr::new(SQL::func("TRIM", expr.into_sql()))
208}
209
210/// LTRIM - removes leading whitespace.
211///
212/// Preserves the nullability of the input expression.
213pub fn ltrim<'a, V, E>(
214    expr: E,
215) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Text, E::Nullable, E::Aggregate>
216where
217    V: SQLParam + 'a,
218    E: Expr<'a, V>,
219    E::SQLType: Textual,
220{
221    SQLExpr::new(SQL::func("LTRIM", expr.into_sql()))
222}
223
224/// RTRIM - removes trailing whitespace.
225///
226/// Preserves the nullability of the input expression.
227pub fn rtrim<'a, V, E>(
228    expr: E,
229) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Text, E::Nullable, E::Aggregate>
230where
231    V: SQLParam + 'a,
232    E: Expr<'a, V>,
233    E::SQLType: Textual,
234{
235    SQLExpr::new(SQL::func("RTRIM", expr.into_sql()))
236}
237
238// =============================================================================
239// COLLATE
240// =============================================================================
241
242/// `COLLATE` - apply a named collation to a text expression.
243///
244/// Preserves the input expression's SQL type, nullability, and aggregate
245/// kind. The collation name is emitted as a quoted identifier
246/// (`expr COLLATE "name"`) which works in both SQLite (which also accepts
247/// unquoted built-ins like `NOCASE`) and PostgreSQL (which requires
248/// quoting).
249///
250/// # Example
251///
252/// ```rust
253/// # let _ = r####"
254/// use drizzle_core::expr::collate;
255///
256/// // Case-insensitive comparison on SQLite:
257/// // SELECT * FROM users WHERE name COLLATE "NOCASE" = ?
258/// db.select(()).from(users)
259///   .r#where(eq(collate(users.name, "NOCASE"), "alice"))
260///   .all()?;
261///
262/// // PostgreSQL with the built-in `"C"` collation:
263/// // SELECT * FROM products ORDER BY label COLLATE "C"
264/// db.select(()).from(products)
265///   .order_by(asc(collate(products.label, "C")))
266///   .all()?;
267/// # "####;
268/// ```
269pub fn collate<'a, V, E>(
270    expr: E,
271    name: &'static str,
272) -> SQLExpr<'a, V, E::SQLType, E::Nullable, E::Aggregate>
273where
274    V: SQLParam + 'a,
275    E: Expr<'a, V>,
276    E::SQLType: Textual,
277{
278    let inner = expr.into_sql().parens_if_subquery();
279    SQLExpr::new(
280        SQL::token(Token::LPAREN)
281            .append(inner)
282            .push(Token::COLLATE)
283            .append(SQL::ident(name))
284            .push(Token::RPAREN),
285    )
286}
287
288// =============================================================================
289// LENGTH
290// =============================================================================
291
292/// `LENGTH` - returns the length of a string.
293///
294/// MySQL counts bytes. SQLite and PostgreSQL follow their native `LENGTH`
295/// semantics. Use [`char_length`] when character count is the intended value.
296/// The result type is dialect-aware and preserves nullability.
297///
298/// # Example
299///
300/// ```rust
301/// # let _ = r####"
302/// use drizzle_core::expr::length;
303///
304/// // SELECT LENGTH(users.name)
305/// let name_len = length(users.name);
306/// # "####;
307/// ```
308#[allow(clippy::type_complexity)]
309pub fn length<'a, V, E>(
310    expr: E,
311) -> SQLExpr<'a, V, <E::SQLType as LengthPolicy<V::DialectMarker>>::Output, E::Nullable, E::Aggregate>
312where
313    V: SQLParam + 'a,
314    E: Expr<'a, V>,
315    E::SQLType: LengthPolicy<V::DialectMarker>,
316{
317    SQLExpr::new(SQL::func("LENGTH", expr.into_sql()))
318}
319
320// =============================================================================
321// SUBSTRING
322// =============================================================================
323
324/// SUBSTR - extracts a substring from a string.
325///
326/// Extracts `len` characters starting at position `start` (1-indexed).
327/// The result is nullable when any argument is nullable.
328///
329/// # Example
330///
331/// ```rust
332/// # let _ = r####"
333/// use drizzle_core::expr::substr;
334///
335/// // SELECT SUBSTR(users.name, 1, 3) -- first 3 characters
336/// let prefix = substr(users.name, 1, 3);
337/// # "####;
338/// ```
339#[allow(clippy::type_complexity)]
340pub fn substr<'a, V, E, S, L>(
341    expr: E,
342    start: S,
343    len: L,
344) -> SQLExpr<
345    'a,
346    V,
347    <V::DialectMarker as DialectTypes>::Text,
348    <<E::Nullable as NullOr<S::Nullable>>::Output as NullOr<L::Nullable>>::Output,
349    <<E::Aggregate as AggOr<S::Aggregate>>::Output as AggOr<L::Aggregate>>::Output,
350>
351where
352    V: SQLParam + 'a,
353    E: Expr<'a, V>,
354    E::SQLType: Textual,
355    S: Expr<'a, V>,
356    S::SQLType: Integral,
357    S::Nullable: Nullability,
358    S::Aggregate: AggregateKind,
359    L: Expr<'a, V>,
360    L::SQLType: Integral,
361    L::Nullable: Nullability,
362    L::Aggregate: AggregateKind,
363    E::Nullable: NullOr<S::Nullable>,
364    <E::Nullable as NullOr<S::Nullable>>::Output: NullOr<L::Nullable>,
365    E::Aggregate: AggOr<S::Aggregate>,
366    <E::Aggregate as AggOr<S::Aggregate>>::Output: AggOr<L::Aggregate>,
367{
368    SQLExpr::new(SQL::func(
369        "SUBSTR",
370        expr.into_sql()
371            .push(Token::COMMA)
372            .append(start.into_sql())
373            .push(Token::COMMA)
374            .append(len.into_sql()),
375    ))
376}
377
378// =============================================================================
379// REPLACE
380// =============================================================================
381
382/// REPLACE - replaces occurrences of a substring.
383///
384/// Replaces all occurrences of `from` with `to` in the expression.
385/// The result is nullable when any argument is nullable.
386///
387/// # Example
388///
389/// ```rust
390/// # let _ = r####"
391/// use drizzle_core::expr::replace;
392///
393/// // SELECT REPLACE(users.email, '@old.com', '@new.com')
394/// let new_email = replace(users.email, "@old.com", "@new.com");
395/// # "####;
396/// ```
397#[allow(clippy::type_complexity)]
398pub fn replace<'a, V, E, F, T>(
399    expr: E,
400    from: F,
401    to: T,
402) -> SQLExpr<
403    'a,
404    V,
405    <V::DialectMarker as DialectTypes>::Text,
406    <<E::Nullable as NullOr<F::Nullable>>::Output as NullOr<T::Nullable>>::Output,
407    <<E::Aggregate as AggOr<F::Aggregate>>::Output as AggOr<T::Aggregate>>::Output,
408>
409where
410    V: SQLParam + 'a,
411    E: Expr<'a, V>,
412    E::SQLType: Textual,
413    F: Expr<'a, V>,
414    F::SQLType: Textual,
415    F::Nullable: Nullability,
416    F::Aggregate: AggregateKind,
417    T: Expr<'a, V>,
418    T::SQLType: Textual,
419    T::Nullable: Nullability,
420    T::Aggregate: AggregateKind,
421    E::Nullable: NullOr<F::Nullable>,
422    <E::Nullable as NullOr<F::Nullable>>::Output: NullOr<T::Nullable>,
423    E::Aggregate: AggOr<F::Aggregate>,
424    <E::Aggregate as AggOr<F::Aggregate>>::Output: AggOr<T::Aggregate>,
425{
426    SQLExpr::new(SQL::func(
427        "REPLACE",
428        expr.into_sql()
429            .push(Token::COMMA)
430            .append(from.into_sql())
431            .push(Token::COMMA)
432            .append(to.into_sql()),
433    ))
434}
435
436// =============================================================================
437// INSTR
438// =============================================================================
439
440/// INSTR - finds the position of a substring.
441///
442/// Returns the 1-indexed position of the first occurrence of `search`
443/// in the expression, or 0 if not found. The result type is dialect-aware.
444/// The result is nullable when either argument is nullable.
445///
446/// # Example
447///
448/// ```rust
449/// # let _ = r####"
450/// use drizzle_core::expr::instr;
451///
452/// // SELECT INSTR(users.email, '@')
453/// let at_pos = instr(users.email, "@");
454/// # "####;
455/// ```
456#[allow(clippy::type_complexity)]
457pub fn instr<'a, V, E, S>(
458    expr: E,
459    search: S,
460) -> SQLExpr<
461    'a,
462    V,
463    <V::DialectMarker as InstrPolicy>::Output,
464    <E::Nullable as NullOr<S::Nullable>>::Output,
465    <E::Aggregate as AggOr<S::Aggregate>>::Output,
466>
467where
468    V: SQLParam + 'a,
469    V::DialectMarker: InstrPolicy,
470    E: Expr<'a, V>,
471    E::SQLType: Textual,
472    S: Expr<'a, V>,
473    S::SQLType: Textual,
474    S::Nullable: Nullability,
475    E::Nullable: NullOr<S::Nullable>,
476    S::Aggregate: AggregateKind,
477    E::Aggregate: AggOr<S::Aggregate>,
478{
479    SQLExpr::new(SQL::func(
480        "INSTR",
481        expr.into_sql().push(Token::COMMA).append(search.into_sql()),
482    ))
483}
484
485/// STRPOS - finds the position of a substring (`PostgreSQL`).
486#[allow(clippy::type_complexity)]
487pub fn strpos<'a, V, E, S>(
488    expr: E,
489    search: S,
490) -> SQLExpr<
491    'a,
492    V,
493    drizzle_types::postgres::types::Int4,
494    <E::Nullable as NullOr<S::Nullable>>::Output,
495    <E::Aggregate as AggOr<S::Aggregate>>::Output,
496>
497where
498    V: SQLParam + 'a,
499    V::DialectMarker: PostgresStringSupport,
500    E: Expr<'a, V>,
501    E::SQLType: Textual,
502    S: Expr<'a, V>,
503    S::SQLType: Textual,
504    S::Nullable: Nullability,
505    E::Nullable: NullOr<S::Nullable>,
506    S::Aggregate: AggregateKind,
507    E::Aggregate: AggOr<S::Aggregate>,
508{
509    SQLExpr::new(SQL::func(
510        "STRPOS",
511        expr.into_sql().push(Token::COMMA).append(search.into_sql()),
512    ))
513}
514
515// =============================================================================
516// CONCAT (with NULL propagation)
517// =============================================================================
518
519/// Concatenate two string expressions.
520///
521/// Nullability follows SQL concatenation rules: if either input is nullable,
522/// the result is nullable. `string_concat` is a compatibility alias.
523/// `MySQL` renders `CONCAT(left, right)` because its default SQL mode treats
524/// `||` as logical OR. `SQLite` and `PostgreSQL` use `||`.
525///
526/// # Type Safety
527///
528/// ```rust
529/// # let _ = r####"
530/// // ✅ OK: Both are Text
531/// concat(users.first_name, users.last_name);
532///
533/// // ✅ OK: Text with string literal
534/// concat(users.first_name, " ");
535///
536/// // ❌ Compile error: Int is not Textual
537/// concat(users.id, users.name);
538/// # "####;
539/// ```
540///
541/// # Example
542///
543/// ```rust
544/// # let _ = r####"
545/// use drizzle_core::expr::concat;
546///
547/// // SELECT users.first_name || ' ' || users.last_name
548/// let full_name = concat(concat(users.first_name, " "), users.last_name);
549/// # "####;
550/// ```
551#[allow(clippy::type_complexity)]
552pub fn concat<'a, V, E1, E2>(
553    expr1: E1,
554    expr2: E2,
555) -> SQLExpr<
556    'a,
557    V,
558    <V::DialectMarker as DialectTypes>::Text,
559    <E1::Nullable as NullOr<E2::Nullable>>::Output,
560    <E1::Aggregate as AggOr<E2::Aggregate>>::Output,
561>
562where
563    V: SQLParam + 'a,
564    E1: Expr<'a, V>,
565    E1::SQLType: Textual,
566    E2: Expr<'a, V>,
567    E2::SQLType: Textual,
568    E1::Nullable: NullOr<E2::Nullable>,
569    E2::Nullable: Nullability,
570    E2::Aggregate: AggregateKind,
571    E1::Aggregate: AggOr<E2::Aggregate>,
572{
573    let left = expr1.into_sql();
574    let right = expr2.into_sql();
575    let sql = match V::DIALECT {
576        Dialect::MySQL => SQL::func("CONCAT", left.push(Token::COMMA).append(right)),
577        Dialect::SQLite | Dialect::PostgreSQL => {
578            super::ops::binary_operator_sql(left, Token::CONCAT, right)
579        }
580    };
581    SQLExpr::new(sql)
582}
583
584// =============================================================================
585// CONCAT_WS (with separator)
586// =============================================================================
587
588/// `CONCAT_WS` - concatenates values with a separator, skipping NULLs.
589///
590/// Unlike `||`, `CONCAT_WS` skips NULL values and never returns NULL
591/// (unless the separator itself is NULL).
592///
593/// Supported by both `SQLite` (3.44+) and `PostgreSQL`.
594///
595/// # Example
596///
597/// ```rust
598/// # let _ = r####"
599/// use drizzle_core::expr::concat_ws;
600///
601/// // SELECT CONCAT_WS(', ', users.city, users.state, users.country)
602/// let location = concat_ws(", ", [users.city, users.state, users.country]);
603/// # "####;
604/// ```
605#[allow(clippy::type_complexity)]
606pub fn concat_ws<'a, V, S, I>(
607    sep: S,
608    values: I,
609) -> SQLExpr<
610    'a,
611    V,
612    <V::DialectMarker as DialectTypes>::Text,
613    S::Nullable,
614    <S::Aggregate as AggOr<<I::Item as Expr<'a, V>>::Aggregate>>::Output,
615>
616where
617    V: SQLParam + 'a,
618    S: Expr<'a, V>,
619    S::SQLType: Textual,
620    I: IntoIterator,
621    I::Item: Expr<'a, V>,
622    <I::Item as Expr<'a, V>>::SQLType: Textual,
623    S::Aggregate: AggOr<<I::Item as Expr<'a, V>>::Aggregate>,
624    <I::Item as Expr<'a, V>>::Aggregate: AggregateKind,
625{
626    let mut sql = sep.into_sql();
627    for value in values {
628        sql = sql.push(Token::COMMA).append(value.into_sql());
629    }
630    SQLExpr::new(SQL::func("CONCAT_WS", sql))
631}
632
633// =============================================================================
634// Dialect-gated String Functions
635// =============================================================================
636
637/// LEFT - returns the first n characters of a string (`PostgreSQL` and `MySQL`).
638///
639/// The result is nullable when either argument is nullable.
640///
641/// # Example
642///
643/// ```rust
644/// # let _ = r####"
645/// use drizzle_core::expr::left;
646///
647/// // SELECT LEFT(users.name, 3)
648/// let prefix = left(users.name, 3);
649/// # "####;
650/// ```
651#[allow(clippy::type_complexity)]
652pub fn left<'a, V, E, N>(
653    expr: E,
654    n: N,
655) -> SQLExpr<
656    'a,
657    V,
658    <V::DialectMarker as DialectTypes>::Text,
659    <E::Nullable as NullOr<N::Nullable>>::Output,
660    <E::Aggregate as AggOr<N::Aggregate>>::Output,
661>
662where
663    V: SQLParam + 'a,
664    V::DialectMarker: LeftRightSupport,
665    E: Expr<'a, V>,
666    E::SQLType: Textual,
667    N: Expr<'a, V>,
668    N::SQLType: Integral,
669    N::Nullable: Nullability,
670    E::Nullable: NullOr<N::Nullable>,
671    N::Aggregate: AggregateKind,
672    E::Aggregate: AggOr<N::Aggregate>,
673{
674    SQLExpr::new(SQL::func(
675        "LEFT",
676        expr.into_sql().push(Token::COMMA).append(n.into_sql()),
677    ))
678}
679
680/// RIGHT - returns the last n characters of a string (`PostgreSQL` and `MySQL`).
681///
682/// The result is nullable when either argument is nullable.
683///
684/// # Example
685///
686/// ```rust
687/// # let _ = r####"
688/// use drizzle_core::expr::right;
689///
690/// // SELECT RIGHT(users.phone, 4)
691/// let last_four = right(users.phone, 4);
692/// # "####;
693/// ```
694#[allow(clippy::type_complexity)]
695pub fn right<'a, V, E, N>(
696    expr: E,
697    n: N,
698) -> SQLExpr<
699    'a,
700    V,
701    <V::DialectMarker as DialectTypes>::Text,
702    <E::Nullable as NullOr<N::Nullable>>::Output,
703    <E::Aggregate as AggOr<N::Aggregate>>::Output,
704>
705where
706    V: SQLParam + 'a,
707    V::DialectMarker: LeftRightSupport,
708    E: Expr<'a, V>,
709    E::SQLType: Textual,
710    N: Expr<'a, V>,
711    N::SQLType: Integral,
712    N::Nullable: Nullability,
713    E::Nullable: NullOr<N::Nullable>,
714    N::Aggregate: AggregateKind,
715    E::Aggregate: AggOr<N::Aggregate>,
716{
717    SQLExpr::new(SQL::func(
718        "RIGHT",
719        expr.into_sql().push(Token::COMMA).append(n.into_sql()),
720    ))
721}
722
723/// `SPLIT_PART` - splits a string and returns the nth field (`PostgreSQL`).
724///
725/// Returns the field at position `n` (1-indexed) when splitting by `delimiter`.
726///
727/// # Example
728///
729/// ```rust
730/// # let _ = r####"
731/// use drizzle_core::expr::split_part;
732///
733/// // SELECT SPLIT_PART(users.email, '@', 2)  -- get domain
734/// let domain = split_part(users.email, "@", 2);
735/// # "####;
736/// ```
737#[allow(clippy::type_complexity)]
738pub fn split_part<'a, V, E, D, N>(
739    expr: E,
740    delimiter: D,
741    n: N,
742) -> SQLExpr<
743    'a,
744    V,
745    <V::DialectMarker as DialectTypes>::Text,
746    <<E::Nullable as NullOr<D::Nullable>>::Output as NullOr<N::Nullable>>::Output,
747    <<E::Aggregate as AggOr<D::Aggregate>>::Output as AggOr<N::Aggregate>>::Output,
748>
749where
750    V: SQLParam + 'a,
751    V::DialectMarker: PostgresStringSupport,
752    E: Expr<'a, V>,
753    E::SQLType: Textual,
754    D: Expr<'a, V>,
755    D::SQLType: Textual,
756    D::Nullable: Nullability,
757    D::Aggregate: AggregateKind,
758    N: Expr<'a, V>,
759    N::SQLType: Integral,
760    N::Nullable: Nullability,
761    N::Aggregate: AggregateKind,
762    E::Nullable: NullOr<D::Nullable>,
763    <E::Nullable as NullOr<D::Nullable>>::Output: NullOr<N::Nullable>,
764    E::Aggregate: AggOr<D::Aggregate>,
765    <E::Aggregate as AggOr<D::Aggregate>>::Output: AggOr<N::Aggregate>,
766{
767    SQLExpr::new(SQL::func(
768        "SPLIT_PART",
769        expr.into_sql()
770            .push(Token::COMMA)
771            .append(delimiter.into_sql())
772            .push(Token::COMMA)
773            .append(n.into_sql()),
774    ))
775}
776
777/// LPAD - pads a string on the left to a specified length (`PostgreSQL` and `MySQL`).
778///
779/// # Example
780///
781/// ```rust
782/// # let _ = r####"
783/// use drizzle_core::expr::lpad;
784///
785/// // SELECT LPAD(users.id::text, 5, '0')  -- zero-pad to 5 digits
786/// let padded = lpad(users.code, 5, "0");
787/// # "####;
788/// ```
789#[allow(clippy::type_complexity)]
790pub fn lpad<'a, V, E, L, F>(
791    expr: E,
792    length: L,
793    fill: F,
794) -> SQLExpr<
795    'a,
796    V,
797    <V::DialectMarker as DialectTypes>::Text,
798    <<E::Nullable as NullOr<L::Nullable>>::Output as NullOr<F::Nullable>>::Output,
799    <<E::Aggregate as AggOr<L::Aggregate>>::Output as AggOr<F::Aggregate>>::Output,
800>
801where
802    V: SQLParam + 'a,
803    V::DialectMarker: PadSupport,
804    E: Expr<'a, V>,
805    E::SQLType: Textual,
806    L: Expr<'a, V>,
807    L::SQLType: Integral,
808    L::Nullable: Nullability,
809    L::Aggregate: AggregateKind,
810    F: Expr<'a, V>,
811    F::SQLType: Textual,
812    F::Nullable: Nullability,
813    E::Nullable: NullOr<L::Nullable>,
814    <E::Nullable as NullOr<L::Nullable>>::Output: NullOr<F::Nullable>,
815    F::Aggregate: AggregateKind,
816    E::Aggregate: AggOr<L::Aggregate>,
817    <E::Aggregate as AggOr<L::Aggregate>>::Output: AggOr<F::Aggregate>,
818{
819    SQLExpr::new(SQL::func(
820        "LPAD",
821        expr.into_sql()
822            .push(Token::COMMA)
823            .append(length.into_sql())
824            .push(Token::COMMA)
825            .append(fill.into_sql()),
826    ))
827}
828
829/// RPAD - pads a string on the right to a specified length (`PostgreSQL` and `MySQL`).
830///
831/// # Example
832///
833/// ```rust
834/// # let _ = r####"
835/// use drizzle_core::expr::rpad;
836///
837/// // SELECT RPAD(users.name, 20, '.')
838/// let padded = rpad(users.name, 20, ".");
839/// # "####;
840/// ```
841#[allow(clippy::type_complexity)]
842pub fn rpad<'a, V, E, L, F>(
843    expr: E,
844    length: L,
845    fill: F,
846) -> SQLExpr<
847    'a,
848    V,
849    <V::DialectMarker as DialectTypes>::Text,
850    <<E::Nullable as NullOr<L::Nullable>>::Output as NullOr<F::Nullable>>::Output,
851    <<E::Aggregate as AggOr<L::Aggregate>>::Output as AggOr<F::Aggregate>>::Output,
852>
853where
854    V: SQLParam + 'a,
855    V::DialectMarker: PadSupport,
856    E: Expr<'a, V>,
857    E::SQLType: Textual,
858    L: Expr<'a, V>,
859    L::SQLType: Integral,
860    L::Nullable: Nullability,
861    L::Aggregate: AggregateKind,
862    F: Expr<'a, V>,
863    F::SQLType: Textual,
864    F::Nullable: Nullability,
865    E::Nullable: NullOr<L::Nullable>,
866    <E::Nullable as NullOr<L::Nullable>>::Output: NullOr<F::Nullable>,
867    F::Aggregate: AggregateKind,
868    E::Aggregate: AggOr<L::Aggregate>,
869    <E::Aggregate as AggOr<L::Aggregate>>::Output: AggOr<F::Aggregate>,
870{
871    SQLExpr::new(SQL::func(
872        "RPAD",
873        expr.into_sql()
874            .push(Token::COMMA)
875            .append(length.into_sql())
876            .push(Token::COMMA)
877            .append(fill.into_sql()),
878    ))
879}
880
881/// INITCAP - converts the first letter of each word to uppercase (`PostgreSQL`).
882///
883/// Preserves the nullability of the input expression.
884pub fn initcap<'a, V, E>(
885    expr: E,
886) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Text, E::Nullable, E::Aggregate>
887where
888    V: SQLParam + 'a,
889    V::DialectMarker: PostgresStringSupport,
890    E: Expr<'a, V>,
891    E::SQLType: Textual,
892{
893    SQLExpr::new(SQL::func("INITCAP", expr.into_sql()))
894}
895
896/// REVERSE - reverses a string (`PostgreSQL` and `MySQL`).
897///
898/// Preserves the nullability of the input expression.
899pub fn reverse<'a, V, E>(
900    expr: E,
901) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Text, E::Nullable, E::Aggregate>
902where
903    V: SQLParam + 'a,
904    V::DialectMarker: ReverseSupport,
905    E: Expr<'a, V>,
906    E::SQLType: Textual,
907{
908    SQLExpr::new(SQL::func("REVERSE", expr.into_sql()))
909}
910
911/// REPEAT - repeats a string n times (`PostgreSQL` and `MySQL`).
912///
913/// # Example
914///
915/// ```rust
916/// # let _ = r####"
917/// use drizzle_core::expr::repeat;
918///
919/// // SELECT REPEAT('-', 40)
920/// let separator = repeat("-", 40);
921/// # "####;
922/// ```
923#[allow(clippy::type_complexity)]
924pub fn repeat<'a, V, E, N>(
925    expr: E,
926    n: N,
927) -> SQLExpr<
928    'a,
929    V,
930    <V::DialectMarker as DialectTypes>::Text,
931    <E::Nullable as NullOr<N::Nullable>>::Output,
932    <E::Aggregate as AggOr<N::Aggregate>>::Output,
933>
934where
935    V: SQLParam + 'a,
936    V::DialectMarker: RepeatSupport,
937    E: Expr<'a, V>,
938    E::SQLType: Textual,
939    N: Expr<'a, V>,
940    N::SQLType: Integral,
941    N::Nullable: Nullability,
942    E::Nullable: NullOr<N::Nullable>,
943    N::Aggregate: AggregateKind,
944    E::Aggregate: AggOr<N::Aggregate>,
945{
946    SQLExpr::new(SQL::func(
947        "REPEAT",
948        expr.into_sql().push(Token::COMMA).append(n.into_sql()),
949    ))
950}
951
952/// `STARTS_WITH` - tests if a string starts with a prefix (`PostgreSQL`).
953///
954/// Returns a boolean expression. Follows comparison operator convention
955/// of returning `NonNull`.
956///
957/// # Example
958///
959/// ```rust
960/// # let _ = r####"
961/// use drizzle_core::expr::starts_with;
962///
963/// // SELECT * FROM users WHERE STARTS_WITH(email, 'admin')
964/// let is_admin = starts_with(users.email, "admin");
965/// # "####;
966/// ```
967#[allow(clippy::type_complexity)]
968pub fn starts_with<'a, V, E, P>(
969    expr: E,
970    prefix: P,
971) -> SQLExpr<
972    'a,
973    V,
974    <V::DialectMarker as DialectTypes>::Bool,
975    NonNull,
976    <E::Aggregate as AggOr<P::Aggregate>>::Output,
977>
978where
979    V: SQLParam + 'a,
980    V::DialectMarker: PostgresStringSupport,
981    E: Expr<'a, V>,
982    E::SQLType: Textual,
983    P: Expr<'a, V>,
984    P::SQLType: Textual,
985    P::Aggregate: AggregateKind,
986    E::Aggregate: AggOr<P::Aggregate>,
987{
988    SQLExpr::new(SQL::func(
989        "STARTS_WITH",
990        expr.into_sql().push(Token::COMMA).append(prefix.into_sql()),
991    ))
992}
993
994// =============================================================================
995// CHAR_LENGTH / OCTET_LENGTH (Standard SQL)
996// =============================================================================
997
998/// Dialect-aware function name for `CHAR_LENGTH`.
999///
1000/// PostgreSQL and MySQL use `CHAR_LENGTH`; SQLite uses `LENGTH`.
1001pub trait CharLengthPolicy {
1002    const CHAR_LENGTH_FN: &'static str;
1003}
1004
1005impl CharLengthPolicy for SQLiteDialect {
1006    const CHAR_LENGTH_FN: &'static str = "LENGTH";
1007}
1008
1009impl CharLengthPolicy for PostgresDialect {
1010    const CHAR_LENGTH_FN: &'static str = "CHAR_LENGTH";
1011}
1012
1013impl CharLengthPolicy for crate::MySQLDialect {
1014    const CHAR_LENGTH_FN: &'static str = "CHAR_LENGTH";
1015}
1016
1017/// `CHAR_LENGTH` - returns the number of characters in a string.
1018///
1019/// Standard SQL function. Emits `CHAR_LENGTH` on `PostgreSQL` and `MySQL`,
1020/// and `LENGTH` on `SQLite`.
1021///
1022/// # Example
1023///
1024/// ```rust
1025/// # let _ = r####"
1026/// use drizzle_core::expr::char_length;
1027///
1028/// // SELECT CHAR_LENGTH(users.name)  -- PostgreSQL/MySQL
1029/// // SELECT LENGTH(users.name)       -- SQLite
1030/// let name_len = char_length(users.name);
1031/// # "####;
1032/// ```
1033#[allow(clippy::type_complexity)]
1034pub fn char_length<'a, V, E>(
1035    expr: E,
1036) -> SQLExpr<'a, V, <E::SQLType as LengthPolicy<V::DialectMarker>>::Output, E::Nullable, E::Aggregate>
1037where
1038    V: SQLParam + 'a,
1039    V::DialectMarker: CharLengthPolicy,
1040    E: Expr<'a, V>,
1041    E::SQLType: LengthPolicy<V::DialectMarker>,
1042{
1043    SQLExpr::new(SQL::func(
1044        <V::DialectMarker as CharLengthPolicy>::CHAR_LENGTH_FN,
1045        expr.into_sql(),
1046    ))
1047}
1048
1049/// `OCTET_LENGTH` - returns the number of bytes in a string.
1050///
1051/// Standard SQL function. Works on SQLite (3.43+), PostgreSQL, and MySQL.
1052///
1053/// # Example
1054///
1055/// ```rust
1056/// # let _ = r####"
1057/// use drizzle_core::expr::octet_length;
1058///
1059/// // SELECT OCTET_LENGTH(users.name)
1060/// let byte_len = octet_length(users.name);
1061/// # "####;
1062/// ```
1063#[allow(clippy::type_complexity)]
1064pub fn octet_length<'a, V, E>(
1065    expr: E,
1066) -> SQLExpr<'a, V, <E::SQLType as LengthPolicy<V::DialectMarker>>::Output, E::Nullable, E::Aggregate>
1067where
1068    V: SQLParam + 'a,
1069    E: Expr<'a, V>,
1070    E::SQLType: LengthPolicy<V::DialectMarker>,
1071{
1072    SQLExpr::new(SQL::func("OCTET_LENGTH", expr.into_sql()))
1073}
1074
1075// =============================================================================
1076// TRANSLATE (PostgreSQL)
1077// =============================================================================
1078
1079/// TRANSLATE - replaces each character in `from` with the corresponding
1080/// character in `to` (`PostgreSQL`).
1081///
1082/// Characters in `from` that have no match in `to` are removed.
1083///
1084/// # Example
1085///
1086/// ```rust
1087/// # let _ = r####"
1088/// use drizzle_core::expr::translate;
1089///
1090/// // SELECT TRANSLATE(users.phone, '()-', '')
1091/// let clean_phone = translate(users.phone, "()-", "");
1092/// # "####;
1093/// ```
1094#[allow(clippy::type_complexity)]
1095pub fn translate<'a, V, E, F, T>(
1096    expr: E,
1097    from: F,
1098    to: T,
1099) -> SQLExpr<
1100    'a,
1101    V,
1102    <V::DialectMarker as DialectTypes>::Text,
1103    E::Nullable,
1104    <<E::Aggregate as AggOr<F::Aggregate>>::Output as AggOr<T::Aggregate>>::Output,
1105>
1106where
1107    V: SQLParam + 'a,
1108    V::DialectMarker: PostgresStringSupport,
1109    E: Expr<'a, V>,
1110    E::SQLType: Textual,
1111    F: Expr<'a, V>,
1112    F::SQLType: Textual,
1113    F::Aggregate: AggregateKind,
1114    T: Expr<'a, V>,
1115    T::SQLType: Textual,
1116    T::Aggregate: AggregateKind,
1117    E::Aggregate: AggOr<F::Aggregate>,
1118    <E::Aggregate as AggOr<F::Aggregate>>::Output: AggOr<T::Aggregate>,
1119{
1120    SQLExpr::new(SQL::func(
1121        "TRANSLATE",
1122        expr.into_sql()
1123            .push(Token::COMMA)
1124            .append(from.into_sql())
1125            .push(Token::COMMA)
1126            .append(to.into_sql()),
1127    ))
1128}
1129
1130// =============================================================================
1131// REGEXP_REPLACE / REGEXP_MATCH (PostgreSQL)
1132// =============================================================================
1133
1134/// `REGEXP_REPLACE` - replaces substrings matching a POSIX regex (`PostgreSQL`).
1135///
1136/// Replaces the first match of `pattern` in `expr` with `replacement`.
1137/// Use optional flags (e.g., `"g"` for global) via `regexp_replace_flags`.
1138///
1139/// # Example
1140///
1141/// ```rust
1142/// # let _ = r####"
1143/// use drizzle_core::expr::regexp_replace;
1144///
1145/// // SELECT REGEXP_REPLACE(users.phone, '[^0-9]', '')
1146/// let digits_only = regexp_replace(users.phone, "[^0-9]", "");
1147/// # "####;
1148/// ```
1149#[allow(clippy::type_complexity)]
1150pub fn regexp_replace<'a, V, E, P, R>(
1151    expr: E,
1152    pattern: P,
1153    replacement: R,
1154) -> SQLExpr<
1155    'a,
1156    V,
1157    <V::DialectMarker as DialectTypes>::Text,
1158    E::Nullable,
1159    <<E::Aggregate as AggOr<P::Aggregate>>::Output as AggOr<R::Aggregate>>::Output,
1160>
1161where
1162    V: SQLParam + 'a,
1163    V::DialectMarker: PostgresStringSupport,
1164    E: Expr<'a, V>,
1165    E::SQLType: Textual,
1166    P: Expr<'a, V>,
1167    P::SQLType: Textual,
1168    P::Aggregate: AggregateKind,
1169    R: Expr<'a, V>,
1170    R::SQLType: Textual,
1171    R::Aggregate: AggregateKind,
1172    E::Aggregate: AggOr<P::Aggregate>,
1173    <E::Aggregate as AggOr<P::Aggregate>>::Output: AggOr<R::Aggregate>,
1174{
1175    SQLExpr::new(SQL::func(
1176        "REGEXP_REPLACE",
1177        expr.into_sql()
1178            .push(Token::COMMA)
1179            .append(pattern.into_sql())
1180            .push(Token::COMMA)
1181            .append(replacement.into_sql()),
1182    ))
1183}
1184
1185/// `REGEXP_REPLACE` with flags - replaces substrings matching a POSIX regex (`PostgreSQL`).
1186///
1187/// Common flags: `"g"` (global), `"i"` (case-insensitive), `"gi"` (both).
1188///
1189/// # Example
1190///
1191/// ```rust
1192/// # let _ = r####"
1193/// use drizzle_core::expr::regexp_replace_flags;
1194///
1195/// // SELECT REGEXP_REPLACE(users.phone, '[^0-9]', '', 'g')
1196/// let digits_only = regexp_replace_flags(users.phone, "[^0-9]", "", "g");
1197/// # "####;
1198/// ```
1199#[allow(clippy::type_complexity)]
1200pub fn regexp_replace_flags<'a, V, E, P, R, F>(
1201    expr: E,
1202    pattern: P,
1203    replacement: R,
1204    flags: F,
1205) -> SQLExpr<
1206    'a,
1207    V,
1208    <V::DialectMarker as DialectTypes>::Text,
1209    E::Nullable,
1210    <<<E::Aggregate as AggOr<P::Aggregate>>::Output as AggOr<R::Aggregate>>::Output as AggOr<
1211        F::Aggregate,
1212    >>::Output,
1213>
1214where
1215    V: SQLParam + 'a,
1216    V::DialectMarker: PostgresStringSupport,
1217    E: Expr<'a, V>,
1218    E::SQLType: Textual,
1219    P: Expr<'a, V>,
1220    P::SQLType: Textual,
1221    P::Aggregate: AggregateKind,
1222    R: Expr<'a, V>,
1223    R::SQLType: Textual,
1224    R::Aggregate: AggregateKind,
1225    F: Expr<'a, V>,
1226    F::SQLType: Textual,
1227    F::Aggregate: AggregateKind,
1228    E::Aggregate: AggOr<P::Aggregate>,
1229    <E::Aggregate as AggOr<P::Aggregate>>::Output: AggOr<R::Aggregate>,
1230    <<E::Aggregate as AggOr<P::Aggregate>>::Output as AggOr<R::Aggregate>>::Output:
1231        AggOr<F::Aggregate>,
1232{
1233    SQLExpr::new(SQL::func(
1234        "REGEXP_REPLACE",
1235        expr.into_sql()
1236            .push(Token::COMMA)
1237            .append(pattern.into_sql())
1238            .push(Token::COMMA)
1239            .append(replacement.into_sql())
1240            .push(Token::COMMA)
1241            .append(flags.into_sql()),
1242    ))
1243}
1244
1245/// `REGEXP_MATCH` - returns captured groups from the first POSIX regex match (`PostgreSQL`).
1246///
1247/// Returns a text array of captured groups. If the pattern has no groups,
1248/// the result is a single-element array with the whole match.
1249///
1250/// # Example
1251///
1252/// ```rust
1253/// # let _ = r####"
1254/// use drizzle_core::expr::regexp_match;
1255///
1256/// // SELECT REGEXP_MATCH(users.email, '(.+)@(.+)')
1257/// let parts = regexp_match(users.email, "(.+)@(.+)");
1258/// # "####;
1259/// ```
1260#[allow(clippy::type_complexity)]
1261pub fn regexp_match<'a, V, E, P>(
1262    expr: E,
1263    pattern: P,
1264) -> SQLExpr<
1265    'a,
1266    V,
1267    crate::types::Array<<V::DialectMarker as DialectTypes>::Text>,
1268    super::Null,
1269    <E::Aggregate as AggOr<P::Aggregate>>::Output,
1270>
1271where
1272    V: SQLParam + 'a,
1273    V::DialectMarker: PostgresStringSupport,
1274    E: Expr<'a, V>,
1275    E::SQLType: Textual,
1276    P: Expr<'a, V>,
1277    P::SQLType: Textual,
1278    P::Aggregate: AggregateKind,
1279    E::Aggregate: AggOr<P::Aggregate>,
1280{
1281    SQLExpr::new(SQL::func(
1282        "REGEXP_MATCH",
1283        expr.into_sql()
1284            .push(Token::COMMA)
1285            .append(pattern.into_sql()),
1286    ))
1287}
1288
1289/// `REGEXP_MATCH` with flags (`PostgreSQL`).
1290///
1291/// Common flags: `"i"` (case-insensitive), `"g"` (not valid for `regexp_match`, use `regexp_matches`).
1292///
1293/// # Example
1294///
1295/// ```rust
1296/// # let _ = r####"
1297/// use drizzle_core::expr::regexp_match_flags;
1298///
1299/// // SELECT REGEXP_MATCH(users.email, '(.+)@(.+)', 'i')
1300/// let parts = regexp_match_flags(users.email, "(.+)@(.+)", "i");
1301/// # "####;
1302/// ```
1303#[allow(clippy::type_complexity)]
1304pub fn regexp_match_flags<'a, V, E, P, F>(
1305    expr: E,
1306    pattern: P,
1307    flags: F,
1308) -> SQLExpr<
1309    'a,
1310    V,
1311    crate::types::Array<<V::DialectMarker as DialectTypes>::Text>,
1312    super::Null,
1313    <<E::Aggregate as AggOr<P::Aggregate>>::Output as AggOr<F::Aggregate>>::Output,
1314>
1315where
1316    V: SQLParam + 'a,
1317    V::DialectMarker: PostgresStringSupport,
1318    E: Expr<'a, V>,
1319    E::SQLType: Textual,
1320    P: Expr<'a, V>,
1321    P::SQLType: Textual,
1322    P::Aggregate: AggregateKind,
1323    F: Expr<'a, V>,
1324    F::SQLType: Textual,
1325    F::Aggregate: AggregateKind,
1326    E::Aggregate: AggOr<P::Aggregate>,
1327    <E::Aggregate as AggOr<P::Aggregate>>::Output: AggOr<F::Aggregate>,
1328{
1329    SQLExpr::new(SQL::func(
1330        "REGEXP_MATCH",
1331        expr.into_sql()
1332            .push(Token::COMMA)
1333            .append(pattern.into_sql())
1334            .push(Token::COMMA)
1335            .append(flags.into_sql()),
1336    ))
1337}