sqlint/connector/mysql/
conversion.rs

1use crate::{
2    ast::Value,
3    connector::{queryable::TakeRow, TypeIdentifier},
4    error::{Error, ErrorKind},
5};
6#[cfg(feature = "chrono")]
7use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc};
8use mysql_async::{
9    self as my,
10    consts::{ColumnFlags, ColumnType},
11};
12use std::convert::TryFrom;
13
14pub fn conv_params(params: &[Value<'_>]) -> crate::Result<my::Params> {
15    if params.is_empty() {
16        // If we don't use explicit 'Empty',
17        // mysql crashes with 'internal error: entered unreachable code'
18        Ok(my::Params::Empty)
19    } else {
20        let mut values = Vec::with_capacity(params.len());
21
22        for pv in params {
23            let res = match pv {
24                Value::Int32(i) => i.map(|i| my::Value::Int(i as i64)),
25                Value::Int64(i) => i.map(my::Value::Int),
26                Value::Float(f) => f.map(my::Value::Float),
27                Value::Double(f) => f.map(my::Value::Double),
28                Value::Text(s) => s.clone().map(|s| my::Value::Bytes((*s).as_bytes().to_vec())),
29                Value::Bytes(bytes) => bytes.clone().map(|bytes| my::Value::Bytes(bytes.into_owned())),
30                Value::Enum(s) => s.clone().map(|s| my::Value::Bytes((*s).as_bytes().to_vec())),
31                Value::Boolean(b) => b.map(|b| my::Value::Int(b as i64)),
32                Value::Char(c) => c.map(|c| my::Value::Bytes(vec![c as u8])),
33                Value::Xml(s) => s.as_ref().map(|s| my::Value::Bytes((s).as_bytes().to_vec())),
34                Value::Array(_) => {
35                    let msg = "Arrays are not supported in MySQL.";
36                    let kind = ErrorKind::conversion(msg);
37
38                    let mut builder = Error::builder(kind);
39                    builder.set_original_message(msg);
40
41                    return Err(builder.build());
42                }
43                #[cfg(feature = "bigdecimal")]
44                Value::Numeric(f) => f.as_ref().map(|f| my::Value::Bytes(f.to_string().as_bytes().to_vec())),
45                #[cfg(feature = "json")]
46                Value::Json(s) => match s {
47                    Some(ref s) => {
48                        let json = serde_json::to_string(s)?;
49                        let bytes = json.into_bytes();
50
51                        Some(my::Value::Bytes(bytes))
52                    }
53                    None => None,
54                },
55                #[cfg(feature = "uuid")]
56                Value::Uuid(u) => u.map(|u| my::Value::Bytes(u.hyphenated().to_string().into_bytes())),
57                #[cfg(feature = "chrono")]
58                Value::Date(d) => {
59                    d.map(|d| my::Value::Date(d.year() as u16, d.month() as u8, d.day() as u8, 0, 0, 0, 0))
60                }
61                #[cfg(feature = "chrono")]
62                Value::Time(t) => {
63                    t.map(|t| my::Value::Time(false, 0, t.hour() as u8, t.minute() as u8, t.second() as u8, 0))
64                }
65                #[cfg(feature = "chrono")]
66                Value::DateTime(dt) => dt.map(|dt| {
67                    my::Value::Date(
68                        dt.year() as u16,
69                        dt.month() as u8,
70                        dt.day() as u8,
71                        dt.hour() as u8,
72                        dt.minute() as u8,
73                        dt.second() as u8,
74                        dt.timestamp_subsec_micros(),
75                    )
76                }),
77            };
78
79            match res {
80                Some(val) => values.push(val),
81                None => values.push(my::Value::NULL),
82            }
83        }
84
85        Ok(my::Params::Positional(values))
86    }
87}
88
89impl TypeIdentifier for my::Column {
90    fn is_real(&self) -> bool {
91        use ColumnType::*;
92
93        matches!(self.column_type(), MYSQL_TYPE_DECIMAL | MYSQL_TYPE_NEWDECIMAL)
94    }
95
96    fn is_float(&self) -> bool {
97        use ColumnType::*;
98
99        matches!(self.column_type(), MYSQL_TYPE_FLOAT)
100    }
101
102    fn is_double(&self) -> bool {
103        use ColumnType::*;
104
105        matches!(self.column_type(), MYSQL_TYPE_DOUBLE)
106    }
107
108    fn is_int32(&self) -> bool {
109        use ColumnType::*;
110
111        let is_unsigned = self.flags().intersects(ColumnFlags::UNSIGNED_FLAG);
112
113        // https://dev.mysql.com/doc/internals/en/binary-protocol-value.html#packet-ProtocolBinary
114        // MYSQL_TYPE_TINY  = i8
115        // MYSQL_TYPE_SHORT = i16
116        // MYSQL_TYPE_YEAR  = i16
117        // SIGNED MYSQL_TYPE_LONG  = i32
118        // SIGNED MYSQL_TYPE_INT24 = i32
119        matches!(
120            (self.column_type(), is_unsigned),
121            (MYSQL_TYPE_TINY, _)
122                | (MYSQL_TYPE_SHORT, _)
123                | (MYSQL_TYPE_YEAR, _)
124                | (MYSQL_TYPE_LONG, false)
125                | (MYSQL_TYPE_INT24, false)
126        )
127    }
128
129    fn is_int64(&self) -> bool {
130        use ColumnType::*;
131
132        let is_unsigned = self.flags().intersects(ColumnFlags::UNSIGNED_FLAG);
133
134        // https://dev.mysql.com/doc/internals/en/binary-protocol-value.html#packet-ProtocolBinary
135        // MYSQL_TYPE_LONGLONG = i64
136        // UNSIGNED MYSQL_TYPE_LONG = u32
137        // UNSIGNED MYSQL_TYPE_INT24 = u32
138        matches!(
139            (self.column_type(), is_unsigned),
140            (MYSQL_TYPE_LONGLONG, _) | (MYSQL_TYPE_LONG, true) | (MYSQL_TYPE_INT24, true)
141        )
142    }
143
144    fn is_datetime(&self) -> bool {
145        use ColumnType::*;
146
147        matches!(
148            self.column_type(),
149            MYSQL_TYPE_TIMESTAMP | MYSQL_TYPE_DATETIME | MYSQL_TYPE_TIMESTAMP2 | MYSQL_TYPE_DATETIME2
150        )
151    }
152
153    fn is_time(&self) -> bool {
154        use ColumnType::*;
155
156        matches!(self.column_type(), MYSQL_TYPE_TIME | MYSQL_TYPE_TIME2)
157    }
158
159    fn is_date(&self) -> bool {
160        use ColumnType::*;
161
162        matches!(self.column_type(), MYSQL_TYPE_DATE | MYSQL_TYPE_NEWDATE)
163    }
164
165    fn is_text(&self) -> bool {
166        use ColumnType::*;
167
168        let is_defined_text =
169            matches!(self.column_type(), MYSQL_TYPE_VARCHAR | MYSQL_TYPE_VAR_STRING | MYSQL_TYPE_STRING);
170
171        let is_bytes_but_text = matches!(
172            self.column_type(),
173            MYSQL_TYPE_TINY_BLOB | MYSQL_TYPE_MEDIUM_BLOB | MYSQL_TYPE_LONG_BLOB | MYSQL_TYPE_BLOB
174        ) && self.character_set() != 63;
175
176        is_defined_text || is_bytes_but_text
177    }
178
179    fn is_bytes(&self) -> bool {
180        use ColumnType::*;
181
182        let is_a_blob = matches!(
183            self.column_type(),
184            MYSQL_TYPE_TINY_BLOB | MYSQL_TYPE_MEDIUM_BLOB | MYSQL_TYPE_LONG_BLOB | MYSQL_TYPE_BLOB
185        ) && self.character_set() == 63;
186
187        let is_bits = self.column_type() == MYSQL_TYPE_BIT && self.column_length() > 1;
188
189        is_a_blob || is_bits
190    }
191
192    fn is_bool(&self) -> bool {
193        self.column_type() == ColumnType::MYSQL_TYPE_BIT && self.column_length() == 1
194    }
195
196    fn is_json(&self) -> bool {
197        self.column_type() == ColumnType::MYSQL_TYPE_JSON
198    }
199
200    fn is_enum(&self) -> bool {
201        self.flags() == ColumnFlags::ENUM_FLAG || self.column_type() == ColumnType::MYSQL_TYPE_ENUM
202    }
203
204    fn is_null(&self) -> bool {
205        self.column_type() == ColumnType::MYSQL_TYPE_NULL
206    }
207}
208
209impl TakeRow for my::Row {
210    fn take_result_row(&mut self) -> crate::Result<Vec<Value<'static>>> {
211        fn convert(row: &mut my::Row, i: usize) -> crate::Result<Value<'static>> {
212            let value = row.take(i).ok_or_else(|| {
213                let msg = "Index out of bounds";
214                let kind = ErrorKind::conversion(msg);
215
216                Error::builder(kind).build()
217            })?;
218
219            let column = row.columns_ref().get(i).ok_or_else(|| {
220                let msg = "Index out of bounds";
221                let kind = ErrorKind::conversion(msg);
222
223                Error::builder(kind).build()
224            })?;
225
226            let res = match value {
227                // JSON is returned as bytes.
228                #[cfg(feature = "json")]
229                my::Value::Bytes(b) if column.is_json() => {
230                    serde_json::from_slice(&b).map(Value::json).map_err(|_| {
231                        let msg = "Unable to convert bytes to JSON";
232                        let kind = ErrorKind::conversion(msg);
233
234                        Error::builder(kind).build()
235                    })?
236                }
237                my::Value::Bytes(b) if column.is_enum() => {
238                    let s = String::from_utf8(b)?;
239                    Value::enum_variant(s)
240                }
241                // NEWDECIMAL returned as bytes. See https://mariadb.com/kb/en/resultset-row/#decimal-binary-encoding
242                #[cfg(feature = "bigdecimal")]
243                my::Value::Bytes(b) if column.is_real() => {
244                    let s = String::from_utf8(b).map_err(|_| {
245                        let msg = "Could not convert NEWDECIMAL from bytes to String.";
246                        let kind = ErrorKind::conversion(msg);
247
248                        Error::builder(kind).build()
249                    })?;
250
251                    let dec = s.parse().map_err(|_| {
252                        let msg = "Could not convert NEWDECIMAL string to a BigDecimal.";
253                        let kind = ErrorKind::conversion(msg);
254
255                        Error::builder(kind).build()
256                    })?;
257
258                    Value::numeric(dec)
259                }
260                my::Value::Bytes(b) if column.is_bool() => match b.as_slice() {
261                    [0] => Value::boolean(false),
262                    _ => Value::boolean(true),
263                },
264                // https://dev.mysql.com/doc/internals/en/character-set.html
265                my::Value::Bytes(b) if column.character_set() == 63 => Value::bytes(b),
266                my::Value::Bytes(s) => Value::text(String::from_utf8(s)?),
267                my::Value::Int(i) if column.is_int64() => Value::int64(i),
268                my::Value::Int(i) => Value::int32(i as i32),
269                my::Value::UInt(i) => Value::int64(i64::try_from(i).map_err(|_| {
270                    let msg = "Unsigned integers larger than 9_223_372_036_854_775_807 are currently not handled.";
271                    let kind = ErrorKind::value_out_of_range(msg);
272
273                    Error::builder(kind).build()
274                })?),
275                my::Value::Float(f) => Value::from(f),
276                my::Value::Double(f) => Value::from(f),
277                #[cfg(feature = "chrono")]
278                my::Value::Date(year, month, day, hour, min, sec, micro) => {
279                    if day == 0 || month == 0 {
280                        let msg = format!(
281                            "The column `{}` contained an invalid datetime value with either day or month set to zero.",
282                            column.name_str()
283                        );
284                        let kind = ErrorKind::value_out_of_range(msg);
285                        return Err(Error::builder(kind).build());
286                    }
287
288                    let time = NaiveTime::from_hms_micro_opt(hour.into(), min.into(), sec.into(), micro).unwrap();
289
290                    let date = NaiveDate::from_ymd_opt(year.into(), month.into(), day.into()).unwrap();
291                    let dt = NaiveDateTime::new(date, time);
292
293                    Value::datetime(DateTime::<Utc>::from_utc(dt, Utc))
294                }
295                #[cfg(feature = "chrono")]
296                my::Value::Time(is_neg, days, hours, minutes, seconds, micros) => {
297                    if is_neg {
298                        let kind = ErrorKind::conversion("Failed to convert a negative time");
299                        return Err(Error::builder(kind).build());
300                    }
301
302                    if days != 0 {
303                        let kind = ErrorKind::conversion("Failed to read a MySQL `time` as duration");
304                        return Err(Error::builder(kind).build());
305                    }
306
307                    let time =
308                        NaiveTime::from_hms_micro_opt(hours.into(), minutes.into(), seconds.into(), micros).unwrap();
309                    Value::time(time)
310                }
311                my::Value::NULL => match column {
312                    t if t.is_bool() => Value::Boolean(None),
313                    t if t.is_enum() => Value::Enum(None),
314                    t if t.is_null() => Value::Int32(None),
315                    t if t.is_int64() => Value::Int64(None),
316                    t if t.is_int32() => Value::Int32(None),
317                    t if t.is_float() => Value::Float(None),
318                    t if t.is_double() => Value::Double(None),
319                    t if t.is_text() => Value::Text(None),
320                    t if t.is_bytes() => Value::Bytes(None),
321                    #[cfg(feature = "bigdecimal")]
322                    t if t.is_real() => Value::Numeric(None),
323                    #[cfg(feature = "chrono")]
324                    t if t.is_datetime() => Value::DateTime(None),
325                    #[cfg(feature = "chrono")]
326                    t if t.is_time() => Value::Time(None),
327                    #[cfg(feature = "chrono")]
328                    t if t.is_date() => Value::Date(None),
329                    #[cfg(feature = "json")]
330                    t if t.is_json() => Value::Json(None),
331                    typ => {
332                        let msg = format!("Value of type {typ:?} is not supported with the current configuration");
333
334                        let kind = ErrorKind::conversion(msg);
335                        return Err(Error::builder(kind).build());
336                    }
337                },
338                #[cfg(not(feature = "chrono"))]
339                typ => {
340                    let msg = format!("Value of type {:?} is not supported with the current configuration", typ);
341
342                    let kind = ErrorKind::conversion(msg);
343                    Err(Error::builder(kind).build())?
344                }
345            };
346
347            Ok(res)
348        }
349
350        let mut row = Vec::with_capacity(self.len());
351
352        for i in 0..self.len() {
353            row.push(convert(self, i)?);
354        }
355
356        Ok(row)
357    }
358}