vibesql_types/sql_mode/
types.rs

1use crate::SqlValue;
2
3/// Represents the value type category for type inference and coercion
4///
5/// This enum is used to determine the result type of operations based
6/// on the SQL mode's type system behavior.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum ValueType {
9    /// Integer type (whole numbers)
10    Integer,
11
12    /// Exact decimal type (MySQL DECIMAL/NUMERIC)
13    ///
14    /// This represents exact decimal arithmetic with fixed precision.
15    /// MySQL uses this for division results to preserve precision.
16    Numeric,
17
18    /// Approximate floating-point type (MySQL FLOAT/DOUBLE, SQLite REAL)
19    ///
20    /// This represents IEEE 754 floating-point numbers with potential
21    /// rounding errors but better performance for large ranges.
22    Float,
23
24    /// Text/String type
25    Text,
26
27    /// Binary blob type
28    Blob,
29
30    /// NULL value
31    Null,
32}
33
34/// Type system behavior trait for SQL modes
35///
36/// This trait defines how different SQL modes handle type-related operations
37/// such as type inference, coercion, and result type determination.
38///
39/// # Examples
40///
41/// ```
42/// use vibesql_types::{MySqlModeFlags, SqlMode, SqlValue, TypeBehavior, ValueType};
43///
44/// let mysql_mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
45/// let sqlite_mode = SqlMode::SQLite;
46///
47/// // MySQL always returns Numeric for division
48/// assert_eq!(
49///     mysql_mode.division_result_type(&SqlValue::Integer(5), &SqlValue::Integer(2)),
50///     ValueType::Numeric
51/// );
52///
53/// // SQLite returns Integer for int/int division
54/// assert_eq!(
55///     sqlite_mode.division_result_type(&SqlValue::Integer(5), &SqlValue::Integer(2)),
56///     ValueType::Integer
57/// );
58///
59/// // SQLite returns Float when any operand is real
60/// assert_eq!(
61///     sqlite_mode.division_result_type(&SqlValue::Float(5.0), &SqlValue::Integer(2)),
62///     ValueType::Float
63/// );
64/// ```
65pub trait TypeBehavior {
66    /// Whether this mode has a distinct DECIMAL/NUMERIC type
67    ///
68    /// - **MySQL**: true - has separate DECIMAL type for exact arithmetic
69    /// - **SQLite**: false - only has INTEGER and REAL types
70    fn has_decimal_type(&self) -> bool;
71
72    /// Whether this mode uses dynamic typing (type affinity)
73    ///
74    /// Dynamic typing means values can change types during operations
75    /// and type checking is lenient.
76    ///
77    /// - **MySQL**: false - uses static typing with defined column types
78    /// - **SQLite**: true - uses type affinity system, types are suggestions
79    fn uses_dynamic_typing(&self) -> bool;
80
81    /// Get the result type for division operation
82    ///
83    /// This determines what type will result from dividing two values,
84    /// which varies significantly between SQL modes.
85    ///
86    /// # Arguments
87    ///
88    /// * `left` - The left operand (dividend)
89    /// * `right` - The right operand (divisor)
90    ///
91    /// # Returns
92    ///
93    /// The `ValueType` that should result from this division
94    ///
95    /// # MySQL Behavior
96    ///
97    /// By default, MySQL returns Numeric (exact decimal) for division to preserve
98    /// precision:
99    /// - `INTEGER / INTEGER → Numeric` (e.g., 5 / 2 = 2.5000)
100    /// - `FLOAT / INTEGER → Numeric`
101    /// - Any division → Numeric (unless NULL)
102    ///
103    /// When `sqlite_division_semantics` flag is set in `MySqlModeFlags`,
104    /// MySQL mode will use SQLite's integer-preserving division instead.
105    /// This is useful for compatibility with test suites that use MySQL
106    /// syntax but expect SQLite division results.
107    ///
108    /// # SQLite Behavior
109    ///
110    /// SQLite uses type affinity rules:
111    /// - `INTEGER / INTEGER → Integer` (truncated, e.g., 5 / 2 = 2)
112    /// - `REAL / INTEGER → Float` (any real operand makes result float)
113    /// - `INTEGER / REAL → Float`
114    /// - `REAL / REAL → Float`
115    fn division_result_type(&self, left: &SqlValue, right: &SqlValue) -> ValueType;
116
117    /// Whether implicit type coercion is permissive
118    ///
119    /// Permissive coercion allows implicit conversions between types
120    /// (e.g., string to number, number to string).
121    ///
122    /// - **MySQL**: true (unless STRICT_TRANS_TABLES or similar flags set)
123    /// - **SQLite**: true (always permissive, uses type affinity)
124    fn permissive_type_coercion(&self) -> bool;
125}
126
127/// Helper to check if a SqlValue is a floating-point type
128fn is_float_value(value: &SqlValue) -> bool {
129    matches!(
130        value,
131        SqlValue::Float(_) | SqlValue::Real(_) | SqlValue::Double(_) | SqlValue::Numeric(_) /* Numeric is stored as f64, treated as float-like in SQLite */
132    )
133}
134
135impl TypeBehavior for super::SqlMode {
136    fn has_decimal_type(&self) -> bool {
137        match self {
138            super::SqlMode::MySQL { .. } => true,
139            super::SqlMode::SQLite => false,
140        }
141    }
142
143    fn uses_dynamic_typing(&self) -> bool {
144        match self {
145            super::SqlMode::MySQL { .. } => false,
146            super::SqlMode::SQLite => true,
147        }
148    }
149
150    fn division_result_type(&self, left: &SqlValue, right: &SqlValue) -> ValueType {
151        // Handle NULL operands
152        if left.is_null() || right.is_null() {
153            return ValueType::Null;
154        }
155
156        match self {
157            super::SqlMode::MySQL { flags } => {
158                // Check if SQLite division semantics are requested
159                // This is used when MySQL syntax is needed (e.g., CAST...AS SIGNED)
160                // but SQLite division behavior is required for compatibility
161                if flags.sqlite_division_semantics {
162                    // Use SQLite's integer-preserving division
163                    if is_float_value(left) || is_float_value(right) {
164                        ValueType::Float
165                    } else {
166                        ValueType::Integer
167                    }
168                } else {
169                    // Standard MySQL: always returns Numeric (exact decimal) for division
170                    // to preserve precision regardless of operand types
171                    ValueType::Numeric
172                }
173            }
174            super::SqlMode::SQLite => {
175                // SQLite uses type affinity:
176                // - If either operand is a real/float type, result is Float
177                // - Otherwise (both integer), result is Integer (truncated)
178                if is_float_value(left) || is_float_value(right) {
179                    ValueType::Float
180                } else {
181                    ValueType::Integer
182                }
183            }
184        }
185    }
186
187    fn permissive_type_coercion(&self) -> bool {
188        match self {
189            // MySQL is permissive by default (would need to check mode flags for strict mode)
190            // For now, return true as default MySQL behavior
191            super::SqlMode::MySQL { .. } => true,
192
193            // SQLite is always permissive with type affinity
194            super::SqlMode::SQLite => true,
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::SqlMode;
203
204    #[test]
205    fn test_has_decimal_type() {
206        assert!(SqlMode::MySQL { flags: Default::default() }.has_decimal_type());
207        assert!(!SqlMode::SQLite.has_decimal_type());
208        // Default is MySQL
209        assert!(SqlMode::default().has_decimal_type());
210    }
211
212    #[test]
213    fn test_uses_dynamic_typing() {
214        assert!(!SqlMode::MySQL { flags: Default::default() }.uses_dynamic_typing());
215        assert!(SqlMode::SQLite.uses_dynamic_typing());
216        // Default is MySQL
217        assert!(!SqlMode::default().uses_dynamic_typing());
218    }
219
220    #[test]
221    fn test_permissive_type_coercion() {
222        assert!(SqlMode::MySQL { flags: Default::default() }.permissive_type_coercion());
223        assert!(SqlMode::SQLite.permissive_type_coercion());
224    }
225
226    #[test]
227    fn test_mysql_division_result_type() {
228        let mode = SqlMode::MySQL { flags: Default::default() };
229
230        // MySQL always returns Numeric for division
231        assert_eq!(
232            mode.division_result_type(&SqlValue::Integer(5), &SqlValue::Integer(2)),
233            ValueType::Numeric
234        );
235
236        assert_eq!(
237            mode.division_result_type(&SqlValue::Bigint(10), &SqlValue::Bigint(3)),
238            ValueType::Numeric
239        );
240
241        assert_eq!(
242            mode.division_result_type(&SqlValue::Float(5.5), &SqlValue::Integer(2)),
243            ValueType::Numeric
244        );
245
246        assert_eq!(
247            mode.division_result_type(&SqlValue::Integer(100), &SqlValue::Float(2.5)),
248            ValueType::Numeric
249        );
250
251        assert_eq!(
252            mode.division_result_type(&SqlValue::Numeric(7.5), &SqlValue::Numeric(2.5)),
253            ValueType::Numeric
254        );
255
256        // NULL handling
257        assert_eq!(
258            mode.division_result_type(&SqlValue::Null, &SqlValue::Integer(2)),
259            ValueType::Null
260        );
261
262        assert_eq!(
263            mode.division_result_type(&SqlValue::Integer(5), &SqlValue::Null),
264            ValueType::Null
265        );
266    }
267
268    #[test]
269    fn test_mysql_with_sqlite_division_semantics() {
270        use crate::MySqlModeFlags;
271
272        // MySQL with sqlite_division_semantics flag uses SQLite's integer division
273        let flags = MySqlModeFlags { sqlite_division_semantics: true, ..Default::default() };
274        let mode = SqlMode::MySQL { flags };
275
276        // int / int → Integer (truncated, like SQLite)
277        assert_eq!(
278            mode.division_result_type(&SqlValue::Integer(5), &SqlValue::Integer(2)),
279            ValueType::Integer
280        );
281
282        assert_eq!(
283            mode.division_result_type(&SqlValue::Bigint(10), &SqlValue::Bigint(3)),
284            ValueType::Integer
285        );
286
287        // any real operand → Float (like SQLite)
288        assert_eq!(
289            mode.division_result_type(&SqlValue::Float(5.0), &SqlValue::Integer(2)),
290            ValueType::Float
291        );
292
293        assert_eq!(
294            mode.division_result_type(&SqlValue::Integer(10), &SqlValue::Float(2.0)),
295            ValueType::Float
296        );
297
298        // NULL handling still works
299        assert_eq!(
300            mode.division_result_type(&SqlValue::Null, &SqlValue::Integer(2)),
301            ValueType::Null
302        );
303    }
304
305    #[test]
306    fn test_sqlite_division_result_type() {
307        let mode = SqlMode::SQLite;
308
309        // SQLite: int / int → Integer (truncated)
310        assert_eq!(
311            mode.division_result_type(&SqlValue::Integer(5), &SqlValue::Integer(2)),
312            ValueType::Integer
313        );
314
315        assert_eq!(
316            mode.division_result_type(&SqlValue::Bigint(10), &SqlValue::Bigint(3)),
317            ValueType::Integer
318        );
319
320        // SQLite: any real operand → Float
321        assert_eq!(
322            mode.division_result_type(&SqlValue::Float(5.0), &SqlValue::Integer(2)),
323            ValueType::Float
324        );
325
326        assert_eq!(
327            mode.division_result_type(&SqlValue::Integer(10), &SqlValue::Float(2.0)),
328            ValueType::Float
329        );
330
331        assert_eq!(
332            mode.division_result_type(&SqlValue::Real(7.5), &SqlValue::Real(2.5)),
333            ValueType::Float
334        );
335
336        assert_eq!(
337            mode.division_result_type(&SqlValue::Double(5.0), &SqlValue::Integer(2)),
338            ValueType::Float
339        );
340
341        assert_eq!(
342            mode.division_result_type(&SqlValue::Numeric(5.0), &SqlValue::Integer(2)),
343            ValueType::Float
344        );
345
346        // NULL handling
347        assert_eq!(
348            mode.division_result_type(&SqlValue::Null, &SqlValue::Integer(2)),
349            ValueType::Null
350        );
351
352        assert_eq!(
353            mode.division_result_type(&SqlValue::Integer(5), &SqlValue::Null),
354            ValueType::Null
355        );
356    }
357
358    #[test]
359    fn test_value_type_enum() {
360        // Ensure all ValueType variants are distinct
361        assert_ne!(ValueType::Integer, ValueType::Numeric);
362        assert_ne!(ValueType::Numeric, ValueType::Float);
363        assert_ne!(ValueType::Integer, ValueType::Float);
364        assert_ne!(ValueType::Integer, ValueType::Text);
365        assert_ne!(ValueType::Integer, ValueType::Blob);
366        assert_ne!(ValueType::Integer, ValueType::Null);
367    }
368}