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