Skip to main content

drizzle_types/sql/
ops.rs

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