Skip to main content

rustlavel_db/mysql/
types.rs

1//! Turning MySQL's column types into [`Value`], and bound parameters back.
2//!
3//! MySQL answers a `COM_QUERY` in the *text* protocol, where every column is an
4//! ASCII string, and a `COM_STMT_EXECUTE` in the *binary* protocol, where every
5//! column is packed to its own width. The driver uses both — text for DDL,
6//! binary for anything with parameters — so both directions live here.
7
8use crate::mysql::protocol::{Column, Reader};
9use crate::value::Value;
10use rustlavel_core::{Json, Result};
11
12// The type bytes a `ColumnDefinition41` can carry. They are fixed by the wire
13// protocol rather than by a catalogue, which is why hard-coding them is safe.
14pub const DECIMAL: u8 = 0x00;
15pub const TINY: u8 = 0x01;
16pub const SHORT: u8 = 0x02;
17pub const LONG: u8 = 0x03;
18pub const FLOAT: u8 = 0x04;
19pub const DOUBLE: u8 = 0x05;
20pub const NULL: u8 = 0x06;
21pub const TIMESTAMP: u8 = 0x07;
22pub const LONGLONG: u8 = 0x08;
23pub const INT24: u8 = 0x09;
24pub const DATE: u8 = 0x0A;
25pub const TIME: u8 = 0x0B;
26pub const DATETIME: u8 = 0x0C;
27pub const YEAR: u8 = 0x0D;
28pub const VARCHAR: u8 = 0x0F;
29pub const BIT: u8 = 0x10;
30pub const JSON: u8 = 0xF5;
31pub const NEWDECIMAL: u8 = 0xF6;
32pub const ENUM: u8 = 0xF7;
33pub const SET: u8 = 0xF8;
34pub const TINY_BLOB: u8 = 0xF9;
35pub const MEDIUM_BLOB: u8 = 0xFA;
36pub const LONG_BLOB: u8 = 0xFB;
37pub const BLOB: u8 = 0xFC;
38pub const VAR_STRING: u8 = 0xFD;
39pub const STRING: u8 = 0xFE;
40pub const GEOMETRY: u8 = 0xFF;
41
42/// Decode one column of a text-protocol row.
43///
44/// Note what does *not* happen here: a `tinyint(1)` comes back as an integer
45/// and stays one. The MySQL dialect answers `booleans_are_integers()` with
46/// `true` precisely because the wire cannot tell a `boolean` column from a
47/// one-digit number, and a driver that guessed would turn a legitimate
48/// `tinyint(1)` counter into `false` the moment it held zero.
49///
50/// DECIMAL, DATE, DATETIME and TIMESTAMP stay as text, exactly as the
51/// PostgreSQL driver keeps NUMERIC and timestamps as text: `decimal` exists to
52/// hold a value `f64` cannot, so converting would throw away the reason the
53/// column was chosen, and the framework has no date type of its own yet, so
54/// there is nothing better than the server's own rendering to convert into.
55pub fn decode_text(column: &Column, raw: Option<&[u8]>) -> Value {
56    let Some(bytes) = raw else { return Value::Null };
57
58    match column.column_type {
59        TINY | SHORT | LONG | INT24 | LONGLONG | YEAR => decode_integer_text(bytes, column),
60        // Never a float: see the note above about precision.
61        DECIMAL | NEWDECIMAL => Value::Text(String::from_utf8_lossy(bytes).into_owned()),
62        FLOAT | DOUBLE => {
63            let text = String::from_utf8_lossy(bytes);
64            text.parse::<f64>().map_or_else(|_| Value::Text(text.into_owned()), Value::Float)
65        }
66        JSON => {
67            let text = String::from_utf8_lossy(bytes);
68            Json::parse(&text).map_or_else(|_| Value::Text(text.into_owned()), Value::Json)
69        }
70        BIT => Value::Int(bits_to_int(bytes)),
71        NULL => Value::Null,
72        _ if column.is_binary() && is_string_type(column.column_type) => {
73            Value::Bytes(bytes.to_vec())
74        }
75        _ => Value::Text(String::from_utf8_lossy(bytes).into_owned()),
76    }
77}
78
79/// Decode one column of a binary-protocol row, advancing the reader past it.
80///
81/// The caller has already consulted the NULL bitmap: a NULL column occupies no
82/// bytes at all here, so this is only ever asked about a present value.
83pub fn decode_binary(column: &Column, reader: &mut Reader<'_>) -> Result<Value> {
84    Ok(match column.column_type {
85        NULL => Value::Null,
86        TINY => {
87            let byte = reader.u8()?;
88            // Signedness is a column flag, not a separate type, so the same
89            // byte means 255 or -1 depending on how the column was declared.
90            if column.is_unsigned() { Value::Int(byte as i64) } else { Value::Int(byte as i8 as i64) }
91        }
92        SHORT | YEAR => {
93            let value = reader.u16()?;
94            if column.is_unsigned() { Value::Int(value as i64) } else { Value::Int(value as i16 as i64) }
95        }
96        LONG | INT24 => {
97            let value = reader.u32()?;
98            if column.is_unsigned() { Value::Int(value as i64) } else { Value::Int(value as i32 as i64) }
99        }
100        LONGLONG => {
101            let value = reader.u64()?;
102            // An unsigned bigint above i64::MAX has no home in `Value::Int`;
103            // it becomes text rather than silently wrapping to a negative.
104            if column.is_unsigned() && value > i64::MAX as u64 {
105                Value::Text(value.to_string())
106            } else {
107                Value::Int(value as i64)
108            }
109        }
110        FLOAT => Value::Float(f32::from_le_bytes(reader.take(4)?.try_into().expect("4 bytes")) as f64),
111        DOUBLE => Value::Float(f64::from_le_bytes(reader.take(8)?.try_into().expect("8 bytes"))),
112        DATE | DATETIME | TIMESTAMP => Value::Text(decode_binary_datetime(reader, column.column_type)?),
113        TIME => Value::Text(decode_binary_time(reader)?),
114        // A decimal arrives as digits, and the server tags it with the binary
115        // collation — which would make it a blob if it fell through below.
116        DECIMAL | NEWDECIMAL => {
117            Value::Text(String::from_utf8_lossy(reader.lenenc_bytes()?).into_owned())
118        }
119        JSON => {
120            let bytes = reader.lenenc_bytes()?;
121            let text = String::from_utf8_lossy(bytes);
122            Json::parse(&text).map_or_else(|_| Value::Text(text.into_owned()), Value::Json)
123        }
124        BIT => Value::Int(bits_to_int(reader.lenenc_bytes()?)),
125        _ => {
126            let bytes = reader.lenenc_bytes()?;
127            // Only a genuine string or blob column becomes bytes; the binary
128            // collation on anything else means "not text I chose", not "binary
129            // data the caller wants back as bytes".
130            if column.is_binary() && is_string_type(column.column_type) {
131                Value::Bytes(bytes.to_vec())
132            } else {
133                Value::Text(String::from_utf8_lossy(bytes).into_owned())
134            }
135        }
136    })
137}
138
139/// The type byte and unsigned flag a bound parameter is sent with.
140///
141/// Deliberately coarse: every integer goes as `bigint` and every string as
142/// `var_string`, and the server narrows them to the column's real type. Sending
143/// the widest type that can hold the value means the driver never has to guess
144/// what the statement will do with it.
145pub fn bind_type(value: &Value) -> (u8, bool) {
146    match value {
147        Value::Null => (NULL, false),
148        // MySQL has no boolean; `tinyint(1)` is what the dialect emits, and 1
149        // and 0 are what the server compares against.
150        Value::Bool(_) => (TINY, false),
151        Value::Int(_) => (LONGLONG, false),
152        Value::Float(_) => (DOUBLE, false),
153        Value::Text(_) | Value::Json(_) => (VAR_STRING, false),
154        Value::Bytes(_) => (BLOB, false),
155    }
156}
157
158/// Append a bound parameter's binary form.
159///
160/// A NULL writes nothing: it is carried entirely by the NULL bitmap, which is
161/// why this is a no-op rather than an error.
162pub fn encode_bind(value: &Value, out: &mut Vec<u8>) {
163    match value {
164        Value::Null => {}
165        Value::Bool(flag) => out.push(u8::from(*flag)),
166        Value::Int(number) => out.extend_from_slice(&number.to_le_bytes()),
167        Value::Float(number) => out.extend_from_slice(&number.to_le_bytes()),
168        Value::Text(text) => encode_lenenc(text.as_bytes(), out),
169        Value::Json(json) => encode_lenenc(json.to_string().as_bytes(), out),
170        Value::Bytes(bytes) => encode_lenenc(bytes, out),
171    }
172}
173
174/// The name this column type has in SQL, for diagnostics.
175pub fn type_name(column_type: u8) -> &'static str {
176    match column_type {
177        DECIMAL | NEWDECIMAL => "decimal",
178        TINY => "tinyint",
179        SHORT => "smallint",
180        LONG => "int",
181        FLOAT => "float",
182        DOUBLE => "double",
183        NULL => "null",
184        TIMESTAMP => "timestamp",
185        LONGLONG => "bigint",
186        INT24 => "mediumint",
187        DATE => "date",
188        TIME => "time",
189        DATETIME => "datetime",
190        YEAR => "year",
191        VARCHAR | VAR_STRING => "varchar",
192        BIT => "bit",
193        JSON => "json",
194        ENUM => "enum",
195        SET => "set",
196        TINY_BLOB | MEDIUM_BLOB | LONG_BLOB | BLOB => "blob",
197        STRING => "char",
198        GEOMETRY => "geometry",
199        _ => "unknown",
200    }
201}
202
203fn is_string_type(column_type: u8) -> bool {
204    matches!(
205        column_type,
206        VARCHAR | VAR_STRING | STRING | TINY_BLOB | MEDIUM_BLOB | LONG_BLOB | BLOB | GEOMETRY
207    )
208}
209
210fn decode_integer_text(bytes: &[u8], column: &Column) -> Value {
211    let text = String::from_utf8_lossy(bytes);
212
213    // An unsigned bigint can exceed i64; it stays text rather than wrapping.
214    if column.is_unsigned()
215        && let Ok(large) = text.parse::<u64>()
216    {
217        return if large > i64::MAX as u64 {
218            Value::Text(text.into_owned())
219        } else {
220            Value::Int(large as i64)
221        };
222    }
223
224    text.parse::<i64>().map_or_else(|_| Value::Text(text.into_owned()), Value::Int)
225}
226
227/// A `bit` column arrives as big-endian bytes of whatever width it was declared.
228fn bits_to_int(bytes: &[u8]) -> i64 {
229    bytes.iter().fold(0i64, |accumulated, byte| (accumulated << 8) | *byte as i64)
230}
231
232/// `DATE`, `DATETIME` and `TIMESTAMP` in binary form: a length byte, then as
233/// many of the fields as the value needs.
234fn decode_binary_datetime(reader: &mut Reader<'_>, column_type: u8) -> Result<String> {
235    let length = reader.u8()?;
236    let date_only = column_type == DATE;
237
238    if length == 0 {
239        // The zero date. MySQL renders it this way too, rather than as an error.
240        return Ok(if date_only { "0000-00-00".into() } else { "0000-00-00 00:00:00".into() });
241    }
242
243    let year = reader.u16()?;
244    let month = reader.u8()?;
245    let day = reader.u8()?;
246    let date = format!("{year:04}-{month:02}-{day:02}");
247
248    if length == 4 {
249        return Ok(if date_only { date } else { format!("{date} 00:00:00") });
250    }
251
252    let hour = reader.u8()?;
253    let minute = reader.u8()?;
254    let second = reader.u8()?;
255    let time = format!("{hour:02}:{minute:02}:{second:02}");
256
257    if length == 7 {
258        return Ok(format!("{date} {time}"));
259    }
260
261    let microseconds = reader.u32()?;
262    Ok(format!("{date} {time}.{microseconds:06}"))
263}
264
265/// `TIME` in binary form. It is a duration, not a clock reading, so it can be
266/// negative and can run past 24 hours — which is why the days field exists.
267fn decode_binary_time(reader: &mut Reader<'_>) -> Result<String> {
268    let length = reader.u8()?;
269    if length == 0 {
270        return Ok("00:00:00".into());
271    }
272
273    let negative = reader.u8()? == 1;
274    let days = reader.u32()?;
275    let hour = reader.u8()? as u32;
276    let minute = reader.u8()?;
277    let second = reader.u8()?;
278    let sign = if negative { "-" } else { "" };
279    let hours = days * 24 + hour;
280
281    if length == 8 {
282        return Ok(format!("{sign}{hours:02}:{minute:02}:{second:02}"));
283    }
284
285    let microseconds = reader.u32()?;
286    Ok(format!("{sign}{hours:02}:{minute:02}:{second:02}.{microseconds:06}"))
287}
288
289fn encode_lenenc(bytes: &[u8], out: &mut Vec<u8>) {
290    match bytes.len() as u64 {
291        length @ 0..=0xFA => out.push(length as u8),
292        length @ 0xFB..=0xFFFF => {
293            out.push(0xFC);
294            out.extend_from_slice(&(length as u16).to_le_bytes());
295        }
296        length @ 0x1_0000..=0xFF_FFFF => {
297            out.push(0xFD);
298            out.extend_from_slice(&(length as u32).to_le_bytes()[..3]);
299        }
300        length => {
301            out.push(0xFE);
302            out.extend_from_slice(&length.to_le_bytes());
303        }
304    }
305    out.extend_from_slice(bytes);
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use crate::mysql::protocol::{CHARSET_BINARY, CHARSET_UTF8MB4, UNSIGNED_FLAG};
312
313    fn column(column_type: u8) -> Column {
314        Column { column_type, charset: CHARSET_UTF8MB4 as u16, ..Column::default() }
315    }
316
317    fn unsigned(column_type: u8) -> Column {
318        Column { flags: UNSIGNED_FLAG, ..column(column_type) }
319    }
320
321    fn binary(column_type: u8) -> Column {
322        Column { charset: CHARSET_BINARY, ..column(column_type) }
323    }
324
325    #[test]
326    fn decodes_the_scalar_types_from_text() {
327        assert_eq!(decode_text(&column(LONG), Some(b"42")), Value::Int(42));
328        assert_eq!(decode_text(&column(LONGLONG), Some(b"-7")), Value::Int(-7));
329        assert_eq!(decode_text(&column(DOUBLE), Some(b"1.5")), Value::Float(1.5));
330        assert_eq!(
331            decode_text(&column(VAR_STRING), Some(b"hello")),
332            Value::Text("hello".into())
333        );
334    }
335
336    #[test]
337    fn a_null_column_decodes_to_null_whatever_its_type() {
338        assert_eq!(decode_text(&column(LONG), None), Value::Null);
339        assert_eq!(decode_text(&column(VAR_STRING), None), Value::Null);
340    }
341
342    #[test]
343    fn a_tinyint_one_stays_an_integer_because_the_dialect_says_so() {
344        // The dialect's `booleans_are_integers()` is true for MySQL: nothing on
345        // the wire separates `boolean` from a one-digit number, so guessing
346        // would break a genuine tinyint counter.
347        use crate::dialect::{Dialect, MySql};
348        assert!(MySql.booleans_are_integers());
349
350        assert_eq!(decode_text(&column(TINY), Some(b"1")), Value::Int(1));
351        assert_eq!(decode_text(&column(TINY), Some(b"0")), Value::Int(0));
352
353        let mut reader = Reader::new(&[1]);
354        assert_eq!(decode_binary(&column(TINY), &mut reader).unwrap(), Value::Int(1));
355    }
356
357    #[test]
358    fn decimals_and_timestamps_stay_text_so_precision_survives() {
359        assert_eq!(
360            decode_text(&column(NEWDECIMAL), Some(b"12345.678901234567890")),
361            Value::Text("12345.678901234567890".into())
362        );
363        assert_eq!(
364            decode_text(&column(DATETIME), Some(b"2026-08-29 10:00:00.123456")),
365            Value::Text("2026-08-29 10:00:00.123456".into())
366        );
367        assert_eq!(
368            decode_text(&column(DATE), Some(b"2026-08-29")),
369            Value::Text("2026-08-29".into())
370        );
371    }
372
373    #[test]
374    fn a_decimal_is_text_even_though_the_server_calls_it_binary() {
375        // MySQL tags DECIMAL with the binary collation, so the plain
376        // "binary collation means bytes" rule would turn money into a blob.
377        let money = Column { charset: CHARSET_BINARY, ..column(NEWDECIMAL) };
378
379        assert_eq!(decode_text(&money, Some(b"12345.6789")), Value::Text("12345.6789".into()));
380
381        let mut reader = Reader::new(b"\x0a12345.6789");
382        assert_eq!(
383            decode_binary(&money, &mut reader).unwrap(),
384            Value::Text("12345.6789".into())
385        );
386    }
387
388    #[test]
389    fn decodes_json_columns_into_parsed_values() {
390        match decode_text(&column(JSON), Some(br#"{"a":1}"#)) {
391            Value::Json(json) => assert_eq!(json.get("a").unwrap().as_i64(), Some(1)),
392            other => panic!("expected parsed JSON, got {other:?}"),
393        }
394    }
395
396    #[test]
397    fn a_binary_collation_makes_a_string_column_bytes() {
398        assert_eq!(
399            decode_text(&binary(BLOB), Some(&[0xDE, 0xAD])),
400            Value::Bytes(vec![0xDE, 0xAD])
401        );
402        // The same type byte with a text collation is text.
403        assert_eq!(decode_text(&column(BLOB), Some(b"note")), Value::Text("note".into()));
404    }
405
406    #[test]
407    fn signedness_comes_from_the_column_flag_not_the_type() {
408        let mut reader = Reader::new(&[0xFF]);
409        assert_eq!(decode_binary(&column(TINY), &mut reader).unwrap(), Value::Int(-1));
410
411        let mut reader = Reader::new(&[0xFF]);
412        assert_eq!(decode_binary(&unsigned(TINY), &mut reader).unwrap(), Value::Int(255));
413
414        assert_eq!(decode_text(&unsigned(LONGLONG), Some(b"255")), Value::Int(255));
415    }
416
417    #[test]
418    fn an_unsigned_bigint_too_large_for_i64_stays_text_rather_than_wrapping() {
419        let huge = u64::MAX;
420        assert_eq!(
421            decode_text(&unsigned(LONGLONG), Some(huge.to_string().as_bytes())),
422            Value::Text(huge.to_string())
423        );
424
425        let bytes = huge.to_le_bytes();
426        let mut reader = Reader::new(&bytes);
427        assert_eq!(
428            decode_binary(&unsigned(LONGLONG), &mut reader).unwrap(),
429            Value::Text(huge.to_string())
430        );
431    }
432
433    #[test]
434    fn decodes_binary_integers_and_floats() {
435        let big = 9_000_000_000i64.to_le_bytes();
436        let mut reader = Reader::new(&big);
437        assert_eq!(
438            decode_binary(&column(LONGLONG), &mut reader).unwrap(),
439            Value::Int(9_000_000_000)
440        );
441
442        let double = 1.5f64.to_le_bytes();
443        let mut reader = Reader::new(&double);
444        assert_eq!(decode_binary(&column(DOUBLE), &mut reader).unwrap(), Value::Float(1.5));
445
446        let single = 0.5f32.to_le_bytes();
447        let mut reader = Reader::new(&single);
448        assert_eq!(decode_binary(&column(FLOAT), &mut reader).unwrap(), Value::Float(0.5));
449    }
450
451    #[test]
452    fn decodes_a_binary_datetime_at_each_of_its_lengths() {
453        // Length 0: the zero date.
454        let mut reader = Reader::new(&[0]);
455        assert_eq!(
456            decode_binary(&column(DATETIME), &mut reader).unwrap(),
457            Value::Text("0000-00-00 00:00:00".into())
458        );
459
460        // Length 4: a date, which a DATE column renders without a time.
461        let date = [4u8, 0xEA, 0x07, 8, 29];
462        let mut reader = Reader::new(&date);
463        assert_eq!(
464            decode_binary(&column(DATE), &mut reader).unwrap(),
465            Value::Text("2026-08-29".into())
466        );
467        let mut reader = Reader::new(&date);
468        assert_eq!(
469            decode_binary(&column(DATETIME), &mut reader).unwrap(),
470            Value::Text("2026-08-29 00:00:00".into())
471        );
472
473        // Length 7: to the second.
474        let mut reader = Reader::new(&[7u8, 0xEA, 0x07, 8, 29, 10, 30, 5]);
475        assert_eq!(
476            decode_binary(&column(TIMESTAMP), &mut reader).unwrap(),
477            Value::Text("2026-08-29 10:30:05".into())
478        );
479
480        // Length 11: with microseconds.
481        let mut full = vec![11u8, 0xEA, 0x07, 8, 29, 10, 30, 5];
482        full.extend_from_slice(&123_456u32.to_le_bytes());
483        let mut reader = Reader::new(&full);
484        assert_eq!(
485            decode_binary(&column(DATETIME), &mut reader).unwrap(),
486            Value::Text("2026-08-29 10:30:05.123456".into())
487        );
488    }
489
490    #[test]
491    fn a_binary_time_is_a_duration_so_it_can_be_negative_and_pass_a_day() {
492        let mut payload = vec![8u8, 1];
493        payload.extend_from_slice(&2u32.to_le_bytes()); // two days
494        payload.extend_from_slice(&[3, 4, 5]);
495
496        let mut reader = Reader::new(&payload);
497        assert_eq!(
498            decode_binary(&column(TIME), &mut reader).unwrap(),
499            Value::Text("-51:04:05".into())
500        );
501    }
502
503    #[test]
504    fn a_bit_column_reads_as_the_number_its_bits_spell() {
505        assert_eq!(decode_text(&column(BIT), Some(&[0x01, 0x00])), Value::Int(256));
506
507        let mut reader = Reader::new(&[2, 0x01, 0x00]);
508        assert_eq!(decode_binary(&column(BIT), &mut reader).unwrap(), Value::Int(256));
509    }
510
511    #[test]
512    fn binds_each_value_as_the_widest_type_that_holds_it() {
513        assert_eq!(bind_type(&Value::Null), (NULL, false));
514        assert_eq!(bind_type(&Value::Bool(true)), (TINY, false));
515        assert_eq!(bind_type(&Value::Int(1)), (LONGLONG, false));
516        assert_eq!(bind_type(&Value::Float(1.0)), (DOUBLE, false));
517        assert_eq!(bind_type(&Value::Text("a".into())), (VAR_STRING, false));
518        assert_eq!(bind_type(&Value::Bytes(vec![1])), (BLOB, false));
519    }
520
521    #[test]
522    fn encodes_bound_parameters_in_binary() {
523        let mut out = Vec::new();
524        encode_bind(&Value::Int(42), &mut out);
525        assert_eq!(out, 42i64.to_le_bytes());
526
527        let mut out = Vec::new();
528        encode_bind(&Value::Text("ada".into()), &mut out);
529        assert_eq!(out, b"\x03ada");
530
531        let mut out = Vec::new();
532        encode_bind(&Value::Bool(true), &mut out);
533        assert_eq!(out, [1]);
534
535        // A NULL is carried by the bitmap alone.
536        let mut out = Vec::new();
537        encode_bind(&Value::Null, &mut out);
538        assert!(out.is_empty());
539    }
540
541    #[test]
542    fn a_bound_string_longer_than_a_byte_length_still_encodes() {
543        let long = "x".repeat(300);
544        let mut out = Vec::new();
545        encode_bind(&Value::Text(long.clone()), &mut out);
546
547        assert_eq!(out[0], 0xFC);
548        assert_eq!(u16::from_le_bytes([out[1], out[2]]), 300);
549        assert_eq!(&out[3..], long.as_bytes());
550    }
551
552    #[test]
553    fn a_hostile_string_is_encoded_as_data_not_as_syntax() {
554        // The bytes go over with a length in front of them; there is no
555        // quoting step that could be got wrong.
556        let hostile = "'; drop table users; --";
557        let mut out = Vec::new();
558        encode_bind(&Value::Text(hostile.into()), &mut out);
559
560        assert_eq!(out[0] as usize, hostile.len());
561        assert_eq!(&out[1..], hostile.as_bytes());
562    }
563
564    #[test]
565    fn names_the_types_it_knows() {
566        assert_eq!(type_name(LONGLONG), "bigint");
567        assert_eq!(type_name(NEWDECIMAL), "decimal");
568        assert_eq!(type_name(JSON), "json");
569        assert_eq!(type_name(0x77), "unknown");
570    }
571}