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}