Skip to main content

drizzle_types/sql/
ops.rs

1//! Result types of SQL arithmetic ([`ArithmeticOutput`], [`NegOutput`]).
2
3use super::Numeric;
4
5/// Compatibility marker for dialects whose arithmetic promotion does not
6/// depend on the operator.
7#[doc(hidden)]
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
9pub struct ArithmeticOp;
10
11/// Type-level marker for SQL addition.
12#[doc(hidden)]
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
14pub struct AddOp;
15
16/// Type-level marker for SQL subtraction.
17#[doc(hidden)]
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
19pub struct SubOp;
20
21/// Type-level marker for SQL multiplication.
22#[doc(hidden)]
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
24pub struct MulOp;
25
26/// Type-level marker for SQL division.
27#[doc(hidden)]
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
29pub struct DivOp;
30
31/// Type-level marker for SQL remainder.
32#[doc(hidden)]
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
34pub struct RemOp;
35
36/// Arithmetic nullability follows the operands.
37#[doc(hidden)]
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
39pub struct PropagateNullability;
40
41/// Arithmetic can produce `NULL` independently of operand nullability.
42#[doc(hidden)]
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
44pub struct AlwaysNullable;
45
46/// Type-level policy for arithmetic nullability.
47#[doc(hidden)]
48pub trait ArithmeticNullability: super::private::Sealed + Copy + 'static {}
49
50impl super::private::Sealed for PropagateNullability {}
51impl super::private::Sealed for AlwaysNullable {}
52impl ArithmeticNullability for PropagateNullability {}
53impl ArithmeticNullability for AlwaysNullable {}
54
55/// The SQL type produced by `Self <op> Rhs`, where both sides are [`Numeric`].
56///
57/// The output follows each database's promotion rules:
58///
59/// - **SQLite**: `Integer op Integer` is `Integer`; any `Real` makes it
60///   `Real`; `Numeric` with `Integer` stays `Numeric`; `Any` with anything
61///   is `Any`.
62/// - **PostgreSQL**: integers widen to the wider integer (`Int2 + Int8` is
63///   `Int8`); an integer with a float gives a float (`Int4 + Float8` is
64///   `Float8`; only `Int2` with `Float4` stays `Float4`); `Numeric` with an
65///   integer stays `Numeric`; `Numeric` with a float gives `Float8`.
66/// - **MySQL**: integer `+`, `-` and `*` give `BigInt` (`BigIntUnsigned` if
67///   an operand is unsigned); integer `%` gives `BigInt` or `BigIntUnsigned`
68///   following the left operand; `/` of exact values gives `Decimal`;
69///   `Decimal` with an integer gives `Decimal`; any `Float` or `Double`
70///   operand gives `Double`. `Year` counts as an unsigned integer.
71///
72/// `Op` selects the operator (`AddOp`, `SubOp`, `MulOp`, `DivOp`, `RemOp`).
73/// The default, `ArithmeticOp`, is an operator-independent form implemented
74/// only for SQLite and PostgreSQL, whose result type does not depend on the
75/// operator.
76///
77/// # Examples
78///
79/// ```
80/// use drizzle_types::{ArithmeticOutput, Numeric};
81/// use drizzle_types::postgres::types::{Float8, Int2, Int4, Int8};
82///
83/// fn output<L, R, O>()
84/// where
85///     L: ArithmeticOutput<R, Output = O>,
86///     R: Numeric,
87///     O: Numeric,
88/// {
89/// }
90///
91/// output::<Int2, Int8, Int8>(); // smallint + bigint -> bigint
92/// output::<Int4, Float8, Float8>(); // integer + double -> double
93/// ```
94#[diagnostic::on_unimplemented(
95    message = "arithmetic between `{Self}` and `{Rhs}` is not supported",
96    label = "both operands must be Numeric (Int, BigInt, Float, Double, etc.)"
97)]
98pub trait ArithmeticOutput<Rhs: Numeric = Self, Op = ArithmeticOp>: Numeric {
99    /// The resulting SQL type of the arithmetic expression.
100    type Output: Numeric;
101
102    /// Whether the operator itself can produce `NULL` even from non-NULL
103    /// operands: `/` and `%` on SQLite's `Integer`, `Real` and `Numeric` and
104    /// on every MySQL numeric type, which return `NULL` for a zero divisor.
105    /// PostgreSQL raises an error instead, so its operators only propagate
106    /// operand nullability.
107    type Nullability: ArithmeticNullability;
108}
109
110/// The SQL type produced by unary negation, `-expr`.
111///
112/// Usually the input type. MySQL integers (signed or unsigned, and `Year`)
113/// negate to `BigInt`, and `Float` to `Double`.
114#[diagnostic::on_unimplemented(
115    message = "unary negation of `{Self}` is not supported",
116    label = "the dialect has no numeric result mapping for this operand"
117)]
118pub trait NegOutput: Numeric {
119    /// The resulting SQL type of `-expr`.
120    type Output: Numeric;
121}
122
123macro_rules! neg_output {
124    ($input:ty => $out:ty) => {
125        impl NegOutput for $input {
126            type Output = $out;
127        }
128    };
129}
130
131/// Implements the operator-independent compatibility form and every concrete
132/// arithmetic operator for a dialect/type pair.
133macro_rules! arithmetic_output {
134    ($lhs:ty, $rhs:ty => $out:ty) => {
135        arithmetic_output!($lhs, $rhs => $out; zero_divisor: PropagateNullability);
136    };
137    ($lhs:ty, $rhs:ty => $out:ty; zero_divisor: $zero_divisor:ty) => {
138        impl ArithmeticOutput<$rhs> for $lhs {
139            type Output = $out;
140            type Nullability = PropagateNullability;
141        }
142
143        impl ArithmeticOutput<$rhs, AddOp> for $lhs {
144            type Output = $out;
145            type Nullability = PropagateNullability;
146        }
147
148        impl ArithmeticOutput<$rhs, SubOp> for $lhs {
149            type Output = $out;
150            type Nullability = PropagateNullability;
151        }
152
153        impl ArithmeticOutput<$rhs, MulOp> for $lhs {
154            type Output = $out;
155            type Nullability = PropagateNullability;
156        }
157
158        impl ArithmeticOutput<$rhs, DivOp> for $lhs {
159            type Output = $out;
160            type Nullability = $zero_divisor;
161        }
162
163        impl ArithmeticOutput<$rhs, RemOp> for $lhs {
164            type Output = $out;
165            type Nullability = $zero_divisor;
166        }
167    };
168}
169
170// =============================================================================
171// SQLite arithmetic output
172// =============================================================================
173//
174// SQLite has only 3 numeric storage classes: Integer, Real, Numeric.
175// Integer + Integer → Integer, Real + anything → Real, etc.
176
177use crate::sqlite::types::{Integer, Numeric as SqliteNumeric, Real};
178
179// Integer op Integer → Integer
180arithmetic_output!(Integer, Integer => Integer; zero_divisor: AlwaysNullable);
181// Integer op Real → Real (widens to float)
182arithmetic_output!(Integer, Real => Real; zero_divisor: AlwaysNullable);
183// Integer op Numeric → Numeric
184arithmetic_output!(Integer, SqliteNumeric => SqliteNumeric; zero_divisor: AlwaysNullable);
185
186// Real op Integer → Real
187arithmetic_output!(Real, Integer => Real; zero_divisor: AlwaysNullable);
188// Real op Real → Real
189arithmetic_output!(Real, Real => Real; zero_divisor: AlwaysNullable);
190// Real op Numeric → Real
191arithmetic_output!(Real, SqliteNumeric => Real; zero_divisor: AlwaysNullable);
192
193// Numeric op Integer → Numeric
194arithmetic_output!(SqliteNumeric, Integer => SqliteNumeric; zero_divisor: AlwaysNullable);
195// Numeric op Real → Real (widens to float)
196arithmetic_output!(SqliteNumeric, Real => Real; zero_divisor: AlwaysNullable);
197// Numeric op Numeric → Numeric
198arithmetic_output!(SqliteNumeric, SqliteNumeric => SqliteNumeric; zero_divisor: AlwaysNullable);
199
200// SQLite Any ↔ all SQLite numeric types
201use crate::sqlite::types::Any as SqliteAny;
202
203arithmetic_output!(SqliteAny, SqliteAny => SqliteAny);
204arithmetic_output!(SqliteAny, Integer => SqliteAny);
205arithmetic_output!(SqliteAny, Real => SqliteAny);
206arithmetic_output!(SqliteAny, SqliteNumeric => SqliteAny);
207arithmetic_output!(Integer, SqliteAny => SqliteAny);
208arithmetic_output!(Real, SqliteAny => SqliteAny);
209arithmetic_output!(SqliteNumeric, SqliteAny => SqliteAny);
210
211neg_output!(Integer => Integer);
212neg_output!(Real => Real);
213neg_output!(SqliteNumeric => SqliteNumeric);
214neg_output!(SqliteAny => SqliteAny);
215
216// =============================================================================
217// PostgreSQL arithmetic output
218// =============================================================================
219//
220// PostgreSQL type promotion lattice:
221//   Int2 < Int4 < Int8 < Numeric
222//   Float4 < Float8
223//   Int + Float → Float (cross-family always widens to float)
224//   Any integer + Numeric → Numeric
225//   Any float + Numeric → Numeric (Float8)
226
227use crate::postgres::types::{Float4, Float8, Int2, Int4, Int8, Numeric as PgNumeric};
228
229// --- Int2 (SMALLINT) ---
230arithmetic_output!(Int2, Int2 => Int2);
231arithmetic_output!(Int2, Int4 => Int4); // widens to Int4
232arithmetic_output!(Int2, Int8 => Int8); // widens to Int8
233arithmetic_output!(Int2, Float4 => Float4); // cross-family → float
234arithmetic_output!(Int2, Float8 => Float8); // cross-family → float
235arithmetic_output!(Int2, PgNumeric => PgNumeric);
236
237// --- Int4 (INTEGER) ---
238arithmetic_output!(Int4, Int2 => Int4); // Int4 is wider
239arithmetic_output!(Int4, Int4 => Int4);
240arithmetic_output!(Int4, Int8 => Int8); // widens to Int8
241arithmetic_output!(Int4, Float4 => Float8); // cross-family → Float8 (PG rule)
242arithmetic_output!(Int4, Float8 => Float8); // cross-family → Float8
243arithmetic_output!(Int4, PgNumeric => PgNumeric);
244
245// --- Int8 (BIGINT) ---
246arithmetic_output!(Int8, Int2 => Int8); // Int8 is wider
247arithmetic_output!(Int8, Int4 => Int8); // Int8 is wider
248arithmetic_output!(Int8, Int8 => Int8);
249arithmetic_output!(Int8, Float4 => Float8); // cross-family → Float8
250arithmetic_output!(Int8, Float8 => Float8); // cross-family → Float8
251arithmetic_output!(Int8, PgNumeric => PgNumeric);
252
253// --- Float4 (REAL) ---
254arithmetic_output!(Float4, Int2 => Float4); // float absorbs int
255arithmetic_output!(Float4, Int4 => Float8); // PG: float4 + int4 → float8
256arithmetic_output!(Float4, Int8 => Float8); // PG: float4 + int8 → float8
257arithmetic_output!(Float4, Float4 => Float4);
258arithmetic_output!(Float4, Float8 => Float8); // widens to Float8
259arithmetic_output!(Float4, PgNumeric => Float8);
260
261// --- Float8 (DOUBLE PRECISION) ---
262arithmetic_output!(Float8, Int2 => Float8);
263arithmetic_output!(Float8, Int4 => Float8);
264arithmetic_output!(Float8, Int8 => Float8);
265arithmetic_output!(Float8, Float4 => Float8); // Float8 is wider
266arithmetic_output!(Float8, Float8 => Float8);
267arithmetic_output!(Float8, PgNumeric => Float8);
268
269// --- Numeric (NUMERIC/DECIMAL) ---
270arithmetic_output!(PgNumeric, Int2 => PgNumeric);
271arithmetic_output!(PgNumeric, Int4 => PgNumeric);
272arithmetic_output!(PgNumeric, Int8 => PgNumeric);
273arithmetic_output!(PgNumeric, Float4 => Float8); // PG casts numeric+float → float8
274arithmetic_output!(PgNumeric, Float8 => Float8);
275arithmetic_output!(PgNumeric, PgNumeric => PgNumeric);
276
277neg_output!(Int2 => Int2);
278neg_output!(Int4 => Int4);
279neg_output!(Int8 => Int8);
280neg_output!(Float4 => Float4);
281neg_output!(Float8 => Float8);
282neg_output!(PgNumeric => PgNumeric);
283
284// =============================================================================
285// MySQL arithmetic output
286// =============================================================================
287//
288// MySQL 8.0 evaluates integer +, -, and * as BIGINT. An unsigned integer
289// operand makes those results unsigned. Integer % keeps the left operand's
290// signedness. Exact-value division produces DECIMAL, while any
291// approximate-value operand makes the result DOUBLE.
292
293use crate::mysql::types::{
294    BigInt as MyBigInt, BigIntUnsigned as MyBigIntUnsigned, Decimal as MyDecimal,
295    Double as MyDouble,
296};
297
298macro_rules! mysql_arithmetic {
299    (
300        signed: [$($signed:ty),+ $(,)?],
301        unsigned: [$($unsigned:ty),+ $(,)?],
302        decimal: $decimal:ty,
303        approximate: [$($approximate:ty),+ $(,)?],
304    ) => {
305        mysql_arithmetic!(@matrix [AddOp, SubOp, MulOp], PropagateNullability;
306            [$($signed),+], [$($signed),+] => MyBigInt);
307        mysql_arithmetic!(@matrix [AddOp, SubOp, MulOp], PropagateNullability;
308            [$($signed),+], [$($unsigned),+] => MyBigIntUnsigned);
309        mysql_arithmetic!(@matrix [AddOp, SubOp, MulOp], PropagateNullability;
310            [$($unsigned),+], [$($signed),+, $($unsigned),+] => MyBigIntUnsigned);
311
312        mysql_arithmetic!(@matrix [AddOp, SubOp, MulOp], PropagateNullability;
313            [$($signed),+, $($unsigned),+], [$decimal] => MyDecimal);
314        mysql_arithmetic!(@matrix [AddOp, SubOp, MulOp], PropagateNullability;
315            [$decimal], [$($signed),+, $($unsigned),+, $decimal] => MyDecimal);
316
317        mysql_arithmetic!(@matrix [AddOp, SubOp, MulOp], PropagateNullability;
318            [$($signed),+, $($unsigned),+, $decimal], [$($approximate),+] => MyDouble);
319        mysql_arithmetic!(@matrix [AddOp, SubOp, MulOp], PropagateNullability;
320            [$($approximate),+],
321            [$($signed),+, $($unsigned),+, $decimal, $($approximate),+] => MyDouble);
322
323        mysql_arithmetic!(@matrix [RemOp], AlwaysNullable;
324            [$($signed),+], [$($signed),+, $($unsigned),+] => MyBigInt);
325        mysql_arithmetic!(@matrix [RemOp], AlwaysNullable;
326            [$($unsigned),+], [$($signed),+, $($unsigned),+] => MyBigIntUnsigned);
327        mysql_arithmetic!(@matrix [RemOp], AlwaysNullable;
328            [$($signed),+, $($unsigned),+], [$decimal] => MyDecimal);
329        mysql_arithmetic!(@matrix [RemOp], AlwaysNullable;
330            [$decimal], [$($signed),+, $($unsigned),+, $decimal] => MyDecimal);
331        mysql_arithmetic!(@matrix [RemOp], AlwaysNullable;
332            [$($signed),+, $($unsigned),+, $decimal], [$($approximate),+] => MyDouble);
333        mysql_arithmetic!(@matrix [RemOp], AlwaysNullable;
334            [$($approximate),+],
335            [$($signed),+, $($unsigned),+, $decimal, $($approximate),+] => MyDouble);
336
337        mysql_arithmetic!(@matrix [DivOp], AlwaysNullable;
338            [$($signed),+, $($unsigned),+, $decimal],
339            [$($signed),+, $($unsigned),+, $decimal] => MyDecimal);
340        mysql_arithmetic!(@matrix [DivOp], AlwaysNullable;
341            [$($signed),+, $($unsigned),+, $decimal], [$($approximate),+] => MyDouble);
342        mysql_arithmetic!(@matrix [DivOp], AlwaysNullable;
343            [$($approximate),+],
344            [$($signed),+, $($unsigned),+, $decimal, $($approximate),+] => MyDouble);
345
346        $(neg_output!($signed => MyBigInt);)+
347        $(neg_output!($unsigned => MyBigInt);)+
348        neg_output!($decimal => MyDecimal);
349        $(neg_output!($approximate => MyDouble);)+
350    };
351    (@matrix $ops:tt, $nullability:ty;
352        [$($lhs:ty),+], $rhs:tt => $out:ty
353    ) => {
354        $(mysql_arithmetic!(@row $ops, $nullability; $lhs, $rhs => $out);)+
355    };
356    (@row [$op:ty $(, $remaining:ty)*], $nullability:ty;
357        $lhs:ty, [$($rhs:ty),+] => $out:ty
358    ) => {
359        $(
360            impl ArithmeticOutput<$rhs, $op> for $lhs {
361                type Output = $out;
362                type Nullability = $nullability;
363            }
364        )+
365        mysql_arithmetic!(@row [$($remaining),*], $nullability;
366            $lhs, [$($rhs),+] => $out);
367    };
368    (@row [], $nullability:ty; $lhs:ty, $rhs:tt => $out:ty) => {};
369}
370
371mysql_arithmetic! {
372    signed: [
373        crate::mysql::types::TinyInt,
374        crate::mysql::types::SmallInt,
375        crate::mysql::types::MediumInt,
376        crate::mysql::types::Int,
377        crate::mysql::types::BigInt,
378    ],
379    unsigned: [
380        crate::mysql::types::TinyIntUnsigned,
381        crate::mysql::types::SmallIntUnsigned,
382        crate::mysql::types::MediumIntUnsigned,
383        crate::mysql::types::IntUnsigned,
384        crate::mysql::types::BigIntUnsigned,
385        crate::mysql::types::Year,
386    ],
387    decimal: crate::mysql::types::Decimal,
388    approximate: [crate::mysql::types::Float, crate::mysql::types::Double],
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::mysql::types as my;
395    use crate::postgres::types as pg;
396    use crate::sqlite::types as sqlite;
397
398    trait Same<T> {}
399    impl<T> Same<T> for T {}
400
401    fn assert_output<Lhs, Rhs, Op, Output, Nullability>()
402    where
403        Lhs: ArithmeticOutput<Rhs, Op, Output = Output>,
404        Rhs: Numeric,
405        Output: Numeric,
406        <Lhs as ArithmeticOutput<Rhs, Op>>::Nullability: Same<Nullability>,
407    {
408    }
409
410    fn assert_neg_output<Input, Output>()
411    where
412        Input: NegOutput<Output = Output>,
413        Output: Numeric,
414    {
415    }
416
417    #[test]
418    fn mysql_operator_result_types_follow_server_categories() {
419        assert_output::<my::Int, my::SmallInt, AddOp, my::BigInt, PropagateNullability>();
420        assert_output::<my::Int, my::IntUnsigned, SubOp, my::BigIntUnsigned, PropagateNullability>(
421        );
422        assert_output::<my::BigIntUnsigned, my::Int, MulOp, my::BigIntUnsigned, PropagateNullability>(
423        );
424        assert_output::<my::Int, my::Int, DivOp, my::Decimal, AlwaysNullable>();
425        assert_output::<my::Int, my::IntUnsigned, RemOp, my::BigInt, AlwaysNullable>();
426        assert_output::<my::IntUnsigned, my::Int, RemOp, my::BigIntUnsigned, AlwaysNullable>();
427        assert_output::<my::Decimal, my::Int, AddOp, my::Decimal, PropagateNullability>();
428        assert_output::<my::Float, my::Int, AddOp, my::Double, PropagateNullability>();
429        assert_output::<my::Int, my::Double, DivOp, my::Double, AlwaysNullable>();
430    }
431
432    #[test]
433    fn every_mysql_numeric_marker_has_operator_and_negation_policy() {
434        macro_rules! assert_numeric_policy {
435            ($($ty:ty),+ $(,)?) => {
436                $(
437                    assert_output::<$ty, $ty, AddOp, _, PropagateNullability>();
438                    assert_output::<$ty, $ty, DivOp, _, AlwaysNullable>();
439                    assert_output::<$ty, $ty, RemOp, _, AlwaysNullable>();
440                    assert_neg_output::<$ty, _>();
441                )+
442            };
443        }
444
445        assert_numeric_policy!(
446            my::TinyInt,
447            my::TinyIntUnsigned,
448            my::SmallInt,
449            my::SmallIntUnsigned,
450            my::MediumInt,
451            my::MediumIntUnsigned,
452            my::Int,
453            my::IntUnsigned,
454            my::BigInt,
455            my::BigIntUnsigned,
456            my::Year,
457            my::Decimal,
458            my::Float,
459            my::Double,
460        );
461    }
462
463    #[test]
464    fn legacy_operator_independent_projection_remains_available() {
465        fn assert_legacy<Lhs, Rhs, Output>()
466        where
467            Lhs: ArithmeticOutput<Rhs, Output = Output>,
468            Rhs: Numeric,
469            Output: Numeric,
470        {
471        }
472
473        assert_legacy::<sqlite::Integer, sqlite::Real, sqlite::Real>();
474        assert_legacy::<pg::Int4, pg::Float8, pg::Float8>();
475    }
476
477    #[test]
478    fn sqlite_zero_divisor_operators_are_nullable() {
479        assert_output::<sqlite::Integer, sqlite::Integer, DivOp, sqlite::Integer, AlwaysNullable>();
480        assert_output::<sqlite::Integer, sqlite::Integer, RemOp, sqlite::Integer, AlwaysNullable>();
481        assert_output::<sqlite::Real, sqlite::Integer, DivOp, sqlite::Real, AlwaysNullable>();
482    }
483
484    #[test]
485    fn mysql_unary_negation_widens_to_a_signed_result() {
486        assert_neg_output::<my::TinyInt, my::BigInt>();
487        assert_neg_output::<my::BigIntUnsigned, my::BigInt>();
488        assert_neg_output::<my::Decimal, my::Decimal>();
489        assert_neg_output::<my::Float, my::Double>();
490    }
491}