Skip to main content

drizzle_core/expr/
math.rs

1//! Type-safe math functions.
2//!
3//! These functions require `Numeric` types (`SmallInt`, Int, `BigInt`, Float, Double)
4//! and provide compile-time enforcement of mathematical operations.
5//!
6//! # Type Safety
7//!
8//! - `abs`, `round`, `ceil`, `floor`: Require `Numeric` types
9//! - `sqrt`, `power`, `log`, `exp`: Require `Numeric` types, return Double
10//! - `mod_`: Modulo operation requiring `Numeric` types
11
12use crate::dialect::DialectTypes;
13use crate::sql::{SQL, Token};
14use crate::traits::SQLParam;
15use crate::types::{DataType, Integral, Numeric};
16use crate::{Dialect, MySQLDialect, PostgresDialect, SQLiteDialect};
17use drizzle_types::mysql::types::{
18    BigInt as MyBigInt, BigIntUnsigned as MyBigIntUnsigned, Decimal as MyDecimal,
19    Double as MyDouble, Float as MyFloat, Int as MyInt, IntUnsigned as MyIntUnsigned,
20    MediumInt as MyMediumInt, MediumIntUnsigned as MyMediumIntUnsigned, SmallInt as MySmallInt,
21    SmallIntUnsigned as MySmallIntUnsigned, TinyInt as MyTinyInt,
22    TinyIntUnsigned as MyTinyIntUnsigned, Year as MyYear,
23};
24use drizzle_types::postgres::types::{Float4, Float8, Int2, Int4, Int8, Numeric as PgNumeric};
25use drizzle_types::sqlite::types::{
26    Integer as SqliteInteger, Numeric as SqliteNumeric, Real as SqliteReal,
27};
28
29use super::{AggOr, Expr, NullOr, Nullability, SQLExpr, Scalar};
30
31/// Math functions that are optional on SQLite.
32///
33/// `CEIL`, `FLOOR`, `TRUNC`, `SQRT`, `POWER`, `EXP`, `LN`, `LOG`, `LOG10`,
34/// `LOG2` and `PI` are built into PostgreSQL and MySQL, but SQLite only has
35/// them when it is compiled with `SQLITE_ENABLE_MATH_FUNCTIONS`, which the
36/// bundled `rusqlite` and `libsql` builds do not set. The `math` cargo feature
37/// is the promise that the linked SQLite provides them; without it these
38/// functions do not type-check for SQLite instead of failing at runtime with
39/// "no such function".
40#[diagnostic::on_unimplemented(
41    message = "`{Self}` does not provide this math function",
42    label = "SQLite only has CEIL/FLOOR/TRUNC/SQRT/POWER/EXP/LN/LOG*/PI with SQLITE_ENABLE_MATH_FUNCTIONS",
43    note = "enable drizzle's `math` feature and build SQLite with the math functions, e.g. `LIBSQLITE3_FLAGS=\"-DSQLITE_ENABLE_MATH_FUNCTIONS\"` for bundled rusqlite"
44)]
45pub trait MathExt {}
46
47impl MathExt for PostgresDialect {}
48impl MathExt for MySQLDialect {}
49#[cfg(feature = "math")]
50impl MathExt for SQLiteDialect {}
51
52#[diagnostic::on_unimplemented(
53    message = "this math function is not available for this dialect",
54    label = "use a dialect-specific alternative"
55)]
56pub trait Log2Policy {
57    type Nullable: Nullability;
58}
59
60/// Nullability policy for math functions whose numeric domain is narrower
61/// than their SQL input type.
62#[doc(hidden)]
63pub trait DomainMathPolicy<Input: Nullability> {
64    type Nullable: Nullability;
65}
66
67#[diagnostic::on_unimplemented(
68    message = "this math function is not available for this dialect",
69    label = "use a dialect-specific alternative"
70)]
71pub trait PiSupport {}
72
73impl Log2Policy for SQLiteDialect {
74    type Nullable = super::Null;
75}
76impl Log2Policy for MySQLDialect {
77    type Nullable = super::Null;
78}
79impl<Input: Nullability> DomainMathPolicy<Input> for SQLiteDialect {
80    type Nullable = super::Null;
81}
82impl<Input: Nullability> DomainMathPolicy<Input> for MySQLDialect {
83    type Nullable = super::Null;
84}
85impl<Input: Nullability> DomainMathPolicy<Input> for PostgresDialect {
86    type Nullable = Input;
87}
88impl PiSupport for PostgresDialect {}
89impl PiSupport for MySQLDialect {}
90#[cfg(feature = "math")]
91impl PiSupport for SQLiteDialect {}
92
93/// Dialect-specific return type for `RANDOM()`.
94///
95/// `SQLite` `RANDOM()` returns an integer in [-2^63, 2^63).
96/// `PostgreSQL` `RANDOM()` returns a float in [0, 1).
97#[diagnostic::on_unimplemented(
98    message = "no RANDOM return type defined for this dialect",
99    label = "RANDOM result type is not configured for this dialect marker"
100)]
101pub trait RandomPolicy {
102    type Random: DataType;
103}
104
105impl RandomPolicy for SQLiteDialect {
106    type Random = SqliteInteger;
107}
108
109impl RandomPolicy for PostgresDialect {
110    type Random = drizzle_types::postgres::types::Float8;
111}
112
113impl RandomPolicy for MySQLDialect {
114    type Random = drizzle_types::mysql::types::Double;
115}
116
117#[diagnostic::on_unimplemented(
118    message = "no rounding policy for `{Self}` on this dialect",
119    label = "round/ceil/floor/trunc return type is not defined for this SQL type/dialect"
120)]
121pub trait RoundingPolicy<D>: Numeric {
122    type Output: DataType;
123
124    /// Prepare the operand of `ROUND(expr, precision)`.
125    ///
126    /// PostgreSQL only defines the two-argument `ROUND` for `numeric`, and
127    /// `double precision` does not cast to it implicitly.
128    fn precision_operand<'a, V: SQLParam + 'a>(expr: SQL<'a, V>) -> SQL<'a, V> {
129        expr
130    }
131
132    /// Coerce a rounding function's result to [`Self::Output`].
133    ///
134    /// PostgreSQL returns `numeric` for every rounding function unless the
135    /// argument is `double precision`; the declared output is `float8`.
136    fn coerce_result<'a, V: SQLParam + 'a>(sql: SQL<'a, V>) -> SQL<'a, V> {
137        sql
138    }
139}
140
141/// Coerce a math function's result to DOUBLE PRECISION on PostgreSQL.
142///
143/// PostgreSQL resolves `SQRT`, `EXP`, `LN`, `LOG`, `POWER` and `SIGN` to their
144/// `numeric` overloads for integer or `numeric` arguments, while the declared
145/// result type is the dialect's double. The cast is a no-op for `float8`.
146pub(super) fn pg_double<'a, V: SQLParam + 'a>(sql: SQL<'a, V>) -> SQL<'a, V> {
147    match V::DIALECT {
148        Dialect::PostgreSQL => pg_cast(sql, "DOUBLE PRECISION"),
149        Dialect::SQLite | Dialect::MySQL => sql,
150    }
151}
152
153/// `CAST(expr AS type)` for the PostgreSQL rounding policies.
154pub(super) fn pg_cast<'a, V: SQLParam + 'a>(
155    expr: SQL<'a, V>,
156    type_name: &'static str,
157) -> SQL<'a, V> {
158    SQL::func("CAST", expr.push(Token::AS).append(SQL::raw(type_name)))
159}
160
161impl RoundingPolicy<SQLiteDialect> for SqliteInteger {
162    type Output = SqliteReal;
163}
164impl RoundingPolicy<SQLiteDialect> for SqliteReal {
165    type Output = Self;
166}
167impl RoundingPolicy<SQLiteDialect> for SqliteNumeric {
168    type Output = SqliteReal;
169}
170
171// Integers and NUMERIC round through NUMERIC on PostgreSQL; the result is
172// cast to DOUBLE PRECISION so it decodes as the declared `Float8`.
173macro_rules! postgres_numeric_rounding_policy {
174    ($($ty:ty),+ $(,)?) => {
175        $(
176            impl RoundingPolicy<PostgresDialect> for $ty {
177                type Output = Float8;
178
179                fn coerce_result<'a, V: SQLParam + 'a>(sql: SQL<'a, V>) -> SQL<'a, V> {
180                    pg_cast(sql, "DOUBLE PRECISION")
181                }
182            }
183        )+
184    };
185}
186postgres_numeric_rounding_policy!(Int2, Int4, Int8, PgNumeric);
187
188// Floats round natively, but `ROUND(float, n)` only exists for NUMERIC, so the
189// precision form casts in and back out.
190macro_rules! postgres_float_rounding_policy {
191    ($($ty:ty),+ $(,)?) => {
192        $(
193            impl RoundingPolicy<PostgresDialect> for $ty {
194                type Output = Float8;
195
196                fn precision_operand<'a, V: SQLParam + 'a>(expr: SQL<'a, V>) -> SQL<'a, V> {
197                    pg_cast(expr, "NUMERIC")
198                }
199
200                fn coerce_result<'a, V: SQLParam + 'a>(sql: SQL<'a, V>) -> SQL<'a, V> {
201                    pg_cast(sql, "DOUBLE PRECISION")
202                }
203            }
204        )+
205    };
206}
207postgres_float_rounding_policy!(Float4, Float8);
208
209macro_rules! mysql_rounding_policy {
210    ($output:ty; $($ty:ty),+ $(,)?) => {
211        $(
212            impl RoundingPolicy<MySQLDialect> for $ty {
213                type Output = $output;
214            }
215        )+
216    };
217}
218
219mysql_rounding_policy!(MyBigInt; MyTinyInt, MySmallInt, MyMediumInt, MyInt, MyBigInt,);
220mysql_rounding_policy!(MyBigIntUnsigned;
221    MyTinyIntUnsigned,
222    MySmallIntUnsigned,
223    MyMediumIntUnsigned,
224    MyIntUnsigned,
225    MyBigIntUnsigned,
226    MyYear,
227);
228
229impl RoundingPolicy<MySQLDialect> for MyFloat {
230    type Output = MyDouble;
231}
232impl RoundingPolicy<MySQLDialect> for MyDouble {
233    type Output = Self;
234}
235impl RoundingPolicy<MySQLDialect> for MyDecimal {
236    type Output = Self;
237}
238
239// =============================================================================
240// ABSOLUTE VALUE
241// =============================================================================
242
243/// ABS - returns the absolute value of a number.
244///
245/// Preserves the SQL type and nullability of the input expression.
246///
247/// # Type Safety
248///
249/// ```rust
250/// # let _ = r####"
251/// // ✅ OK: Int column
252/// abs(users.balance);
253///
254/// // ❌ Compile error: Text is not Numeric
255/// abs(users.name);
256/// # "####;
257/// ```
258pub fn abs<'a, V, E>(expr: E) -> SQLExpr<'a, V, E::SQLType, E::Nullable, E::Aggregate>
259where
260    V: SQLParam + 'a,
261    E: Expr<'a, V>,
262    E::SQLType: Numeric,
263{
264    SQLExpr::new(SQL::func("ABS", expr.into_sql()))
265}
266
267// =============================================================================
268// ROUNDING FUNCTIONS
269// =============================================================================
270
271/// ROUND - rounds a number to the nearest integer (or specified precision).
272///
273/// Returns a dialect-aware float type, preserves nullability.
274///
275/// # Example
276///
277/// ```rust
278/// # let _ = r####"
279/// use drizzle_core::expr::round;
280///
281/// // SELECT ROUND(users.price)
282/// let rounded = round(users.price);
283/// # "####;
284/// ```
285#[allow(clippy::type_complexity)]
286pub fn round<'a, V, E>(
287    expr: E,
288) -> SQLExpr<
289    'a,
290    V,
291    <E::SQLType as RoundingPolicy<V::DialectMarker>>::Output,
292    E::Nullable,
293    E::Aggregate,
294>
295where
296    V: SQLParam + 'a,
297    E: Expr<'a, V>,
298    E::SQLType: RoundingPolicy<V::DialectMarker>,
299{
300    SQLExpr::new(
301        <E::SQLType as RoundingPolicy<V::DialectMarker>>::coerce_result(SQL::func(
302            "ROUND",
303            expr.into_sql(),
304        )),
305    )
306}
307
308/// ROUND with precision - rounds a number to specified decimal places.
309///
310/// Returns a dialect-aware float type, preserves nullability of the input expression.
311///
312/// # Example
313///
314/// ```rust
315/// # let _ = r####"
316/// use drizzle_core::expr::round_to;
317///
318/// // SELECT ROUND(users.price, 2)
319/// let rounded = round_to(users.price, 2);
320/// # "####;
321/// ```
322#[allow(clippy::type_complexity)]
323pub fn round_to<'a, V, E, P>(
324    expr: E,
325    precision: P,
326) -> SQLExpr<
327    'a,
328    V,
329    <E::SQLType as RoundingPolicy<V::DialectMarker>>::Output,
330    <E::Nullable as NullOr<P::Nullable>>::Output,
331    <E::Aggregate as AggOr<P::Aggregate>>::Output,
332>
333where
334    V: SQLParam + 'a,
335    E: Expr<'a, V>,
336    E::SQLType: RoundingPolicy<V::DialectMarker>,
337    P: Expr<'a, V>,
338    P::SQLType: Integral,
339    E::Nullable: NullOr<P::Nullable>,
340    P::Nullable: Nullability,
341    E::Aggregate: AggOr<P::Aggregate>,
342{
343    SQLExpr::new(
344        <E::SQLType as RoundingPolicy<V::DialectMarker>>::coerce_result(SQL::func(
345            "ROUND",
346            <E::SQLType as RoundingPolicy<V::DialectMarker>>::precision_operand(expr.into_sql())
347                .push(Token::COMMA)
348                .append(precision.into_sql()),
349        )),
350    )
351}
352
353/// CEIL / CEILING - rounds a number up to the nearest integer.
354///
355/// Returns a dialect-aware float type, preserves nullability.
356///
357/// # Example
358///
359/// ```rust
360/// # let _ = r####"
361/// use drizzle_core::expr::ceil;
362///
363/// // SELECT CEIL(users.price)
364/// let ceiling = ceil(users.price);
365/// # "####;
366/// ```
367#[allow(clippy::type_complexity)]
368pub fn ceil<'a, V, E>(
369    expr: E,
370) -> SQLExpr<
371    'a,
372    V,
373    <E::SQLType as RoundingPolicy<V::DialectMarker>>::Output,
374    E::Nullable,
375    E::Aggregate,
376>
377where
378    V: SQLParam + 'a,
379    V::DialectMarker: MathExt,
380    E: Expr<'a, V>,
381    E::SQLType: RoundingPolicy<V::DialectMarker>,
382{
383    SQLExpr::new(
384        <E::SQLType as RoundingPolicy<V::DialectMarker>>::coerce_result(SQL::func(
385            "CEIL",
386            expr.into_sql(),
387        )),
388    )
389}
390
391/// FLOOR - rounds a number down to the nearest integer.
392///
393/// Returns a dialect-aware float type, preserves nullability.
394///
395/// # Example
396///
397/// ```rust
398/// # let _ = r####"
399/// use drizzle_core::expr::floor;
400///
401/// // SELECT FLOOR(users.price)
402/// let floored = floor(users.price);
403/// # "####;
404/// ```
405#[allow(clippy::type_complexity)]
406pub fn floor<'a, V, E>(
407    expr: E,
408) -> SQLExpr<
409    'a,
410    V,
411    <E::SQLType as RoundingPolicy<V::DialectMarker>>::Output,
412    E::Nullable,
413    E::Aggregate,
414>
415where
416    V: SQLParam + 'a,
417    V::DialectMarker: MathExt,
418    E: Expr<'a, V>,
419    E::SQLType: RoundingPolicy<V::DialectMarker>,
420{
421    SQLExpr::new(
422        <E::SQLType as RoundingPolicy<V::DialectMarker>>::coerce_result(SQL::func(
423            "FLOOR",
424            expr.into_sql(),
425        )),
426    )
427}
428
429/// TRUNC - truncates a number towards zero.
430///
431/// Returns a dialect-aware float type, preserves nullability.
432///
433/// # Example
434///
435/// ```rust
436/// # let _ = r####"
437/// use drizzle_core::expr::trunc;
438///
439/// // SELECT TRUNC(users.price), or TRUNCATE(users.price, 0) on MySQL
440/// let truncated = trunc(users.price);
441/// # "####;
442/// ```
443#[allow(clippy::type_complexity)]
444pub fn trunc<'a, V, E>(
445    expr: E,
446) -> SQLExpr<
447    'a,
448    V,
449    <E::SQLType as RoundingPolicy<V::DialectMarker>>::Output,
450    E::Nullable,
451    E::Aggregate,
452>
453where
454    V: SQLParam + 'a,
455    V::DialectMarker: MathExt,
456    E: Expr<'a, V>,
457    E::SQLType: RoundingPolicy<V::DialectMarker>,
458{
459    let expr = expr.into_sql();
460    let truncated = match V::DIALECT {
461        Dialect::MySQL => SQL::func("TRUNCATE", expr.push(Token::COMMA).append(SQL::raw("0"))),
462        Dialect::SQLite | Dialect::PostgreSQL => SQL::func("TRUNC", expr),
463    };
464    SQLExpr::new(<E::SQLType as RoundingPolicy<V::DialectMarker>>::coerce_result(truncated))
465}
466
467// =============================================================================
468// POWER AND ROOT FUNCTIONS
469// =============================================================================
470
471/// SQRT - returns the square root of a number.
472///
473/// Returns a dialect-aware double type. SQLite and MySQL return `NULL` for a
474/// negative argument, while PostgreSQL reports an error.
475///
476/// # Example
477///
478/// ```rust
479/// # let _ = r####"
480/// use drizzle_core::expr::sqrt;
481///
482/// // SELECT SQRT(users.area)
483/// let root = sqrt(users.area);
484/// # "####;
485/// ```
486#[allow(clippy::type_complexity)]
487pub fn sqrt<'a, V, E>(
488    expr: E,
489) -> SQLExpr<
490    'a,
491    V,
492    <V::DialectMarker as DialectTypes>::Double,
493    <V::DialectMarker as DomainMathPolicy<E::Nullable>>::Nullable,
494    E::Aggregate,
495>
496where
497    V: SQLParam + 'a,
498    V::DialectMarker: MathExt,
499    V::DialectMarker: DomainMathPolicy<E::Nullable>,
500    E: Expr<'a, V>,
501    E::SQLType: Numeric,
502{
503    SQLExpr::new(pg_double(SQL::func("SQRT", expr.into_sql())))
504}
505
506/// POWER - raises a number to a power.
507///
508/// Returns a dialect-aware double type. The result is nullable if either input is nullable.
509///
510/// # Example
511///
512/// ```rust
513/// # let _ = r####"
514/// use drizzle_core::expr::power;
515///
516/// // SELECT POWER(users.base, 2)
517/// let squared = power(users.base, 2);
518/// # "####;
519/// ```
520#[allow(clippy::type_complexity)]
521pub fn power<'a, V, E1, E2>(
522    base: E1,
523    exponent: E2,
524) -> SQLExpr<
525    'a,
526    V,
527    <V::DialectMarker as DialectTypes>::Double,
528    <E1::Nullable as NullOr<E2::Nullable>>::Output,
529    <E1::Aggregate as AggOr<E2::Aggregate>>::Output,
530>
531where
532    V: SQLParam + 'a,
533    V::DialectMarker: MathExt,
534    E1: Expr<'a, V>,
535    E1::SQLType: Numeric,
536    E2: Expr<'a, V>,
537    E2::SQLType: Numeric,
538    E1::Nullable: NullOr<E2::Nullable>,
539    E2::Nullable: Nullability,
540    E1::Aggregate: AggOr<E2::Aggregate>,
541{
542    SQLExpr::new(pg_double(SQL::func(
543        "POWER",
544        base.into_sql()
545            .push(Token::COMMA)
546            .append(exponent.into_sql()),
547    )))
548}
549
550// =============================================================================
551// LOGARITHMIC AND EXPONENTIAL FUNCTIONS
552// =============================================================================
553
554/// EXP - returns e raised to the power of the argument.
555///
556/// Returns a dialect-aware double type and preserves nullability.
557///
558/// # Example
559///
560/// ```rust
561/// # let _ = r####"
562/// use drizzle_core::expr::exp;
563///
564/// // SELECT EXP(users.rate)
565/// let exponential = exp(users.rate);
566/// # "####;
567/// ```
568pub fn exp<'a, V, E>(
569    expr: E,
570) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Double, E::Nullable, E::Aggregate>
571where
572    V: SQLParam + 'a,
573    V::DialectMarker: MathExt,
574    E: Expr<'a, V>,
575    E::SQLType: Numeric,
576{
577    SQLExpr::new(pg_double(SQL::func("EXP", expr.into_sql())))
578}
579
580/// LN - returns the natural logarithm of a number.
581///
582/// SQLite and MySQL return `NULL` outside the logarithm domain. PostgreSQL
583/// reports an error for invalid non-NULL input.
584///
585/// # Example
586///
587/// ```rust
588/// # let _ = r####"
589/// use drizzle_core::expr::ln;
590///
591/// // SELECT LN(users.value)
592/// let natural_log = ln(users.value);
593/// # "####;
594/// ```
595#[allow(clippy::type_complexity)]
596pub fn ln<'a, V, E>(
597    expr: E,
598) -> SQLExpr<
599    'a,
600    V,
601    <V::DialectMarker as DialectTypes>::Double,
602    <V::DialectMarker as DomainMathPolicy<E::Nullable>>::Nullable,
603    E::Aggregate,
604>
605where
606    V: SQLParam + 'a,
607    V::DialectMarker: MathExt,
608    V::DialectMarker: DomainMathPolicy<E::Nullable>,
609    E: Expr<'a, V>,
610    E::SQLType: Numeric,
611{
612    SQLExpr::new(pg_double(SQL::func("LN", expr.into_sql())))
613}
614
615/// LOG10 - returns the base-10 logarithm of a number.
616///
617/// SQLite and MySQL return `NULL` outside the logarithm domain. PostgreSQL
618/// reports an error for invalid non-NULL input.
619///
620/// # Example
621///
622/// ```rust
623/// # let _ = r####"
624/// use drizzle_core::expr::log10;
625///
626/// // SELECT LOG10(users.value)
627/// let log_base_10 = log10(users.value);
628/// # "####;
629/// ```
630#[allow(clippy::type_complexity)]
631pub fn log10<'a, V, E>(
632    expr: E,
633) -> SQLExpr<
634    'a,
635    V,
636    <V::DialectMarker as DialectTypes>::Double,
637    <V::DialectMarker as DomainMathPolicy<E::Nullable>>::Nullable,
638    E::Aggregate,
639>
640where
641    V: SQLParam + 'a,
642    V::DialectMarker: MathExt,
643    V::DialectMarker: DomainMathPolicy<E::Nullable>,
644    E: Expr<'a, V>,
645    E::SQLType: Numeric,
646{
647    SQLExpr::new(pg_double(SQL::func("LOG10", expr.into_sql())))
648}
649
650/// LOG - returns the logarithm of a number with a specified base.
651///
652/// Returns a dialect-aware double type. The result is nullable if either input is nullable.
653///
654/// # Example
655///
656/// ```rust
657/// # let _ = r####"
658/// use drizzle_core::expr::log;
659///
660/// // SELECT LOG(2, users.value)
661/// let log_base_2 = log(2, users.value);
662/// # "####;
663/// ```
664#[allow(clippy::type_complexity)]
665pub fn log<'a, V, E1, E2>(
666    base: E1,
667    value: E2,
668) -> SQLExpr<
669    'a,
670    V,
671    <V::DialectMarker as DialectTypes>::Double,
672    <V::DialectMarker as DomainMathPolicy<
673        <E1::Nullable as NullOr<E2::Nullable>>::Output,
674    >>::Nullable,
675    <E1::Aggregate as AggOr<E2::Aggregate>>::Output,
676>
677where
678    V: SQLParam + 'a,
679    V::DialectMarker: MathExt,
680    V::DialectMarker:
681        DomainMathPolicy<<E1::Nullable as NullOr<E2::Nullable>>::Output>,
682    E1: Expr<'a, V>,
683    E1::SQLType: Numeric,
684    E2: Expr<'a, V>,
685    E2::SQLType: Numeric,
686    E1::Nullable: NullOr<E2::Nullable>,
687    E2::Nullable: Nullability,
688    E1::Aggregate: AggOr<E2::Aggregate>,
689{
690    let (base, value) = (base.into_sql(), value.into_sql());
691    // PostgreSQL only defines the two-argument LOG for NUMERIC operands.
692    let (base, value) = match V::DIALECT {
693        Dialect::PostgreSQL => (pg_cast(base, "NUMERIC"), pg_cast(value, "NUMERIC")),
694        Dialect::SQLite | Dialect::MySQL => (base, value),
695    };
696    SQLExpr::new(pg_double(SQL::func(
697        "LOG",
698        base.push(Token::COMMA).append(value),
699    )))
700}
701
702// =============================================================================
703// SIGN AND MODULO
704// =============================================================================
705
706/// The type `SIGN` returns on each dialect.
707///
708/// SQLite and MySQL answer an integer; PostgreSQL answers `numeric` or
709/// `double precision` depending on the argument, which [`sign`] coerces to
710/// `double precision`.
711pub trait SignPolicy {
712    /// The SQL type of `SIGN(expr)`.
713    type Sign: DataType;
714}
715
716impl SignPolicy for SQLiteDialect {
717    type Sign = SqliteInteger;
718}
719
720impl SignPolicy for PostgresDialect {
721    type Sign = Float8;
722}
723
724impl SignPolicy for MySQLDialect {
725    type Sign = MyBigInt;
726}
727
728/// SIGN - returns the sign of a number (-1, 0, or 1).
729///
730/// Returns the dialect's [`SignPolicy::Sign`] type (an integer on SQLite and
731/// MySQL, `double precision` on PostgreSQL), preserves nullability.
732///
733/// # Example
734///
735/// ```rust
736/// # let _ = r####"
737/// use drizzle_core::expr::sign;
738///
739/// // SELECT SIGN(users.balance)
740/// let balance_sign = sign(users.balance);
741/// # "####;
742/// ```
743pub fn sign<'a, V, E>(
744    expr: E,
745) -> SQLExpr<'a, V, <V::DialectMarker as SignPolicy>::Sign, E::Nullable, E::Aggregate>
746where
747    V: SQLParam + 'a,
748    V::DialectMarker: SignPolicy,
749    E: Expr<'a, V>,
750    E::SQLType: Numeric,
751{
752    SQLExpr::new(pg_double(SQL::func("SIGN", expr.into_sql())))
753}
754
755/// MOD - returns the remainder of division (using % operator).
756///
757/// Returns the same type as the dividend. The result is nullable if either input is nullable.
758/// Named `mod_` to avoid conflict with Rust's `mod` keyword.
759///
760/// Note: Uses the `%` operator which works on both `SQLite` and `PostgreSQL`.
761///
762/// # Example
763///
764/// ```rust
765/// # let _ = r####"
766/// use drizzle_core::expr::mod_;
767///
768/// // SELECT users.value % 3
769/// let remainder = mod_(users.value, 3);
770/// # "####;
771/// ```
772#[allow(clippy::type_complexity)]
773pub fn mod_<'a, V, E1, E2>(
774    dividend: E1,
775    divisor: E2,
776) -> SQLExpr<
777    'a,
778    V,
779    E1::SQLType,
780    <E1::Nullable as NullOr<E2::Nullable>>::Output,
781    <E1::Aggregate as AggOr<E2::Aggregate>>::Output,
782>
783where
784    V: SQLParam + 'a,
785    E1: Expr<'a, V>,
786    E1::SQLType: Numeric,
787    E2: Expr<'a, V>,
788    E2::SQLType: Numeric,
789    E1::Nullable: NullOr<E2::Nullable>,
790    E2::Nullable: Nullability,
791    E1::Aggregate: AggOr<E2::Aggregate>,
792{
793    SQLExpr::new(super::ops::binary_operator_sql(
794        dividend.into_expr_sql(),
795        Token::REM,
796        divisor.into_expr_sql(),
797    ))
798}
799
800// =============================================================================
801// CONSTANTS AND RANDOM
802// =============================================================================
803
804/// PI - returns the mathematical constant pi (`PostgreSQL` and `MySQL`).
805///
806/// # Example
807///
808/// ```rust
809/// # let _ = r####"
810/// use drizzle_core::expr::pi;
811///
812/// // SELECT PI()
813/// let pi_val = pi::<PostgresValue>();
814/// # "####;
815/// ```
816#[must_use]
817pub fn pi<'a, V>()
818-> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Double, super::NonNull, Scalar>
819where
820    V: SQLParam + 'a,
821    V::DialectMarker: MathExt,
822    V::DialectMarker: PiSupport,
823{
824    SQLExpr::new(SQL::raw("PI()"))
825}
826
827/// RANDOM - returns a random value.
828///
829/// Return type is dialect-aware:
830/// - `SQLite`: integer in [-2^63, 2^63)
831/// - `PostgreSQL` and `MySQL`: float in [0, 1)
832///
833/// # Example
834///
835/// ```rust
836/// # let _ = r####"
837/// use drizzle_core::expr::random;
838///
839/// // SELECT RANDOM() on SQLite/PostgreSQL, SELECT RAND() on MySQL
840/// let rnd = random::<SQLiteValue>();
841/// # "####;
842/// ```
843#[must_use]
844pub fn random<'a, V>()
845-> SQLExpr<'a, V, <V::DialectMarker as RandomPolicy>::Random, super::NonNull, Scalar>
846where
847    V: SQLParam + 'a,
848    V::DialectMarker: RandomPolicy,
849{
850    SQLExpr::new(SQL::raw(match V::DIALECT {
851        Dialect::MySQL => "RAND()",
852        Dialect::SQLite | Dialect::PostgreSQL => "RANDOM()",
853    }))
854}
855
856// =============================================================================
857// Dialect-gated Math Functions
858// =============================================================================
859
860/// LOG2 - returns the base-2 logarithm of a number.
861///
862/// Available in `SQLite` when compiled with `SQLITE_ENABLE_MATH_FUNCTIONS`,
863/// and natively in `MySQL`.
864/// Returns a nullable dialect-aware double because invalid domains produce
865/// `NULL` in both dialects.
866///
867/// # Example
868///
869/// ```rust
870/// # let _ = r####"
871/// use drizzle_core::expr::log2;
872///
873/// // SELECT LOG2(users.value)
874/// let log_base_2 = log2(users.value);
875/// # "####;
876/// ```
877#[allow(clippy::type_complexity)]
878pub fn log2<'a, V, E>(
879    expr: E,
880) -> SQLExpr<
881    'a,
882    V,
883    <V::DialectMarker as DialectTypes>::Double,
884    <V::DialectMarker as Log2Policy>::Nullable,
885    E::Aggregate,
886>
887where
888    V: SQLParam + 'a,
889    V::DialectMarker: MathExt,
890    V::DialectMarker: Log2Policy,
891    E: Expr<'a, V>,
892    E::SQLType: Numeric,
893{
894    SQLExpr::new(SQL::func("LOG2", expr.into_sql()))
895}